001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.logging;
023:
024: import java.io.IOException;
025: import java.io.PrintStream;
026: import java.net.MalformedURLException;
027: import java.net.URL;
028: import java.net.URLConnection;
029: import java.util.StringTokenizer;
030: import java.util.Timer;
031: import java.util.TimerTask;
032:
033: import javax.management.MBeanServer;
034: import javax.management.MalformedObjectNameException;
035: import javax.management.Notification;
036: import javax.management.ObjectName;
037:
038: import org.apache.log4j.Level;
039: import org.apache.log4j.PropertyConfigurator;
040: import org.apache.log4j.helpers.LogLog;
041: import org.apache.log4j.xml.DOMConfigurator;
042: import org.jboss.logging.util.LoggerStream;
043: import org.jboss.logging.util.OnlyOnceErrorHandler;
044: import org.jboss.system.ServiceMBeanSupport;
045: import org.jboss.util.Strings;
046: import org.jboss.util.ThrowableHandler;
047: import org.jboss.util.ThrowableListener;
048: import org.jboss.util.stream.Streams;
049:
050: /**
051: * Initializes the Log4j logging framework. Supports XML and standard
052: * configuration file formats. Defaults to using 'log4j.xml' read
053: * from a system resource.
054: *
055: * <p>Sets up a {@link ThrowableListener} to adapt unhandled
056: * throwables to a logger.
057: *
058: * <p>Installs {@link LoggerStream} adapters for <tt>System.out</tt> and
059: * <tt>System.err</tt> to catch and redirect calls to Log4j.
060: *
061: * @jmx:mbean name="jboss.system:type=Log4jService,service=Logging"
062: * extends="org.jboss.system.ServiceMBean"
063: *
064: * @version <tt>$Revision: 57209 $</tt>
065: * @author <a href="mailto:phox@galactica.it">Fulco Muriglio</a>
066: * @author <a href="mailto:Scott_Stark@displayscape.com">Scott Stark</a>
067: * @author <a href="mailto:davidjencks@earthlink.net">David Jencks</a>
068: * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
069: */
070: public class Log4jService extends ServiceMBeanSupport implements
071: Log4jServiceMBean {
072: /**
073: * The default url for the configuration file. Reads the value
074: * from the system property <tt>org.jboss.logging.Log4jService.configURL</tt>
075: * or if that is not set defaults to <tt>resource:log4j.xml</tt>.
076: */
077: public static final String DEFAULT_URL = System.getProperty(
078: Log4jService.class.getName() + ".configURL",
079: "resource:log4j.xml");
080:
081: /**
082: * Default flag to enable/disable cacthing System.out. Reads the value
083: * from the system property <tt>org.jboss.logging.Log4jService.catchSystemOut</tt>
084: * or if not set defaults to <tt>true</tt>.
085: */
086: public static final boolean CATCH_SYSTEM_OUT = getBoolean(
087: Log4jService.class.getName() + ".catchSystemOut", true);
088:
089: /**
090: * Default flag to enable/disable cacthing System.err. Reads the value
091: * from the system property <tt>org.jboss.logging.Log4jService.catchSystemErr</tt>
092: * or if not set defaults to <tt>true</tt>.
093: */
094: public static final boolean CATCH_SYSTEM_ERR = getBoolean(
095: Log4jService.class.getName() + ".catchSystemErr", true);
096:
097: /** Helper to get boolean value from system property or use default if not set. */
098: private static boolean getBoolean(String name, boolean defaultValue) {
099: String value = System.getProperty(name, null);
100: if (value == null)
101: return defaultValue;
102: return new Boolean(value).booleanValue();
103: }
104:
105: /** The URL to the configuration file. */
106: private URL configURL;
107:
108: /** The time in seconds between checking for new config. */
109: private int refreshPeriod;
110:
111: private ThrowableListenerLoggingAdapter throwableAdapter;
112:
113: /** The previous value of System.out. */
114: private PrintStream out;
115:
116: /** The previous value of System.err. */
117: private PrintStream err;
118:
119: /**
120: * Flag to enable/disable adapting <tt>System.out</tt> to the
121: * <tt>STDOUT</tt> logger.
122: */
123: private boolean catchSystemOut = CATCH_SYSTEM_OUT;
124:
125: /**
126: * Flag to enable/disable adapting <tt>System.out</tt> to the
127: * <tt>STDERR</tt> logger.
128: */
129: private boolean catchSystemErr = CATCH_SYSTEM_ERR;
130:
131: /** The org.apache.log4j.helpers.LogLog.setQuietMode flag setting */
132: private boolean log4jQuietMode = true;
133:
134: /** The URL watch timer (in daemon mode). */
135: private Timer timer = new Timer(true);
136:
137: /** The specialized timer task to watch our config file. */
138: private URLWatchTimerTask timerTask;
139:
140: /**
141: * A flag to enable start/stop to work as expected,
142: * but still use create to init early.
143: */
144: private boolean initialized;
145:
146: /**
147: * Uses defaults.
148: *
149: * @jmx:managed-constructor
150: *
151: * @throws MalformedURLException Could not create URL from default (propbably
152: * a problem with overridden properties).
153: */
154: public Log4jService() throws MalformedURLException {
155: this (DEFAULT_URL, 60);
156: }
157:
158: /**
159: * @jmx:managed-constructor
160: *
161: * @param url The configuration URL.
162: */
163: public Log4jService(final URL url) {
164: this (url, 60);
165: }
166:
167: /**
168: * @jmx:managed-constructor
169: *
170: * @param url The configuration URL.
171: */
172: public Log4jService(final String url) throws MalformedURLException {
173: this (Strings.toURL(url), 60);
174: }
175:
176: /**
177: * @jmx:managed-constructor
178: *
179: * @param url The configuration URL.
180: * @param refreshPeriod The refreshPeriod in seconds to wait between each check.
181: */
182: public Log4jService(final String url, final int refreshPeriod)
183: throws MalformedURLException {
184: this (Strings.toURL(url), refreshPeriod);
185: }
186:
187: /**
188: * @jmx:managed-constructor
189: *
190: * @param url The configuration URL.
191: * @param refreshPeriod The refreshPeriod in seconds to wait between each check.
192: */
193: public Log4jService(final URL url, final int refreshPeriod) {
194: this .configURL = url;
195: this .refreshPeriod = refreshPeriod;
196: }
197:
198: /**
199: * Set the catch <tt>System.out</tt> flag.
200: *
201: * @jmx:managed-attribute
202: *
203: * @param flag True to enable, false to disable.
204: */
205: public void setCatchSystemOut(final boolean flag) {
206: this .catchSystemOut = flag;
207: }
208:
209: /**
210: * Get the catch <tt>System.out</tt> flag.
211: *
212: * @jmx:managed-attribute
213: *
214: * @return True if enabled, false if disabled.
215: */
216: public boolean getCatchSystemOut() {
217: return catchSystemOut;
218: }
219:
220: /**
221: * Set the catch <tt>System.err</tt> flag.
222: *
223: * @jmx:managed-attribute
224: *
225: * @param flag True to enable, false to disable.
226: */
227: public void setCatchSystemErr(final boolean flag) {
228: this .catchSystemErr = flag;
229: }
230:
231: /**
232: * Get the catch <tt>System.err</tt> flag.
233: *
234: * @jmx:managed-attribute
235: *
236: * @return True if enabled, false if disabled.
237: */
238: public boolean getCatchSystemErr() {
239: return catchSystemErr;
240: }
241:
242: /**
243: * Get the org.apache.log4j.helpers.LogLog.setQuietMode flag
244: *
245: * @jmx:managed-attribute
246: *
247: * @return True if enabled, false if disabled.
248: */
249: public boolean getLog4jQuietMode() {
250: return log4jQuietMode;
251: }
252:
253: /**
254: * Set the org.apache.log4j.helpers.LogLog.setQuietMode flag
255: *
256: * @jmx:managed-attribute
257: *
258: * @return True if enabled, false if disabled.
259: */
260: public void setLog4jQuietMode(boolean flag) {
261: this .log4jQuietMode = flag;
262: }
263:
264: /**
265: * Get the refresh period.
266: *
267: * @jmx:managed-attribute
268: */
269: public int getRefreshPeriod() {
270: return refreshPeriod;
271: }
272:
273: /**
274: * Set the refresh period.
275: *
276: * @jmx:managed-attribute
277: */
278: public void setRefreshPeriod(final int refreshPeriod) {
279: this .refreshPeriod = refreshPeriod;
280: }
281:
282: /**
283: * Get the Log4j configuration URL.
284: *
285: * @jmx:managed-attribute
286: */
287: public URL getConfigurationURL() {
288: return configURL;
289: }
290:
291: /**
292: * Set the Log4j configuration URL.
293: *
294: * @jmx:managed-attribute
295: */
296: public void setConfigurationURL(final URL url) {
297: this .configURL = url;
298: }
299:
300: /**
301: * Sets the level for a logger of the give name.
302: *
303: * <p>Values are trimmed before used.
304: *
305: * @jmx:managed-operation
306: *
307: * @param name The name of the logger to change level
308: * @param levelName The name of the level to change the logger to.
309: */
310: public void setLoggerLevel(final String name, final String levelName) {
311: org.apache.log4j.Logger logger = org.apache.log4j.Logger
312: .getLogger(name.trim());
313: Level level = XLevel.toLevel(levelName.trim());
314:
315: logger.setLevel(level);
316: log.info("Level set to " + level + " for " + name);
317: }
318:
319: /**
320: * Sets the levels of each logger specified by the given comma
321: * seperated list of logger names.
322: *
323: * @jmx:managed-operation
324: *
325: * @see #setLoggerLevel
326: *
327: * @param list A comma seperated list of logger names.
328: * @param levelName The name of the level to change the logger to.
329: */
330: public void setLoggerLevels(final String list,
331: final String levelName) {
332: StringTokenizer stok = new StringTokenizer(list, ",");
333:
334: while (stok.hasMoreTokens()) {
335: String name = stok.nextToken();
336: setLoggerLevel(name, levelName);
337: }
338: }
339:
340: /**
341: * Gets the level of the logger of the give name.
342: *
343: * @jmx:managed-operation
344: *
345: * @param name The name of the logger to inspect.
346: */
347: public String getLoggerLevel(final String name) {
348: org.apache.log4j.Logger logger = org.apache.log4j.Logger
349: .getLogger(name);
350: Level level = logger.getLevel();
351:
352: if (level != null)
353: return level.toString();
354:
355: return null;
356: }
357:
358: /**
359: * Force the logging system to reconfigure.
360: *
361: * @jmx:managed-operation
362: */
363: public void reconfigure() throws IOException {
364: if (timerTask == null)
365: throw new IllegalStateException(
366: "Service stopped or not started");
367:
368: timerTask.reconfigure();
369: }
370:
371: /**
372: * Hack to reconfigure and change the URL. This is needed until we
373: * have a JMX HTML Adapter that can use PropertyEditor to coerce.
374: *
375: * @jmx:managed-operation
376: *
377: * @param url The new configuration url
378: */
379: public void reconfigure(final String url) throws IOException,
380: MalformedURLException {
381: setConfigurationURL(Strings.toURL(url));
382: reconfigure();
383: }
384:
385: private void installSystemAdapters() {
386: org.apache.log4j.Logger logger;
387:
388: // Install catchers
389: if (catchSystemOut) {
390: logger = org.apache.log4j.Logger.getLogger("STDOUT");
391: out = System.out;
392: System.setOut(new LoggerStream(logger, Level.INFO, out));
393: log.debug("Installed System.out adapter");
394: }
395:
396: if (catchSystemErr) {
397: logger = org.apache.log4j.Logger.getLogger("STDERR");
398: err = System.err;
399: OnlyOnceErrorHandler.setOutput(err);
400: System.setErr(new LoggerStream(logger, Level.ERROR, err));
401: log.debug("Installed System.err adapter");
402: }
403: }
404:
405: private void uninstallSystemAdapters() {
406: // Remove System adapters
407: if (out != null) {
408: System.out.flush();
409: System.setOut(out);
410: log.debug("Removed System.out adapter");
411: out = null;
412: }
413:
414: if (err != null) {
415: System.err.flush();
416: System.setErr(err);
417: log.debug("Removed System.err adapter");
418: err = null;
419: }
420: }
421:
422: ///////////////////////////////////////////////////////////////////////////
423: // Concrete Service Overrides //
424: ///////////////////////////////////////////////////////////////////////////
425:
426: protected ObjectName getObjectName(MBeanServer server,
427: ObjectName name) throws MalformedObjectNameException {
428: return name == null ? OBJECT_NAME : name;
429: }
430:
431: private void setup() throws Exception {
432: if (initialized)
433: return;
434:
435: timerTask = new URLWatchTimerTask();
436: timerTask.run();
437: timer.schedule(timerTask, 1000 * refreshPeriod,
438: 1000 * refreshPeriod);
439:
440: // Make sure the root Logger has loaded
441: org.apache.log4j.Logger.getRootLogger();
442:
443: // Install listener for unhandled throwables to turn them into log messages
444: throwableAdapter = new ThrowableListenerLoggingAdapter();
445: ThrowableHandler.addThrowableListener(throwableAdapter);
446: log.debug("Added ThrowableListener: " + throwableAdapter);
447:
448: initialized = true;
449: }
450:
451: protected void createService() throws Exception {
452: setup();
453: }
454:
455: protected void startService() throws Exception {
456: setup();
457: }
458:
459: protected void stopService() throws Exception {
460: timerTask.cancel();
461: timerTask = null;
462:
463: // Remove throwable adapter
464: ThrowableHandler.removeThrowableListener(throwableAdapter);
465: throwableAdapter = null;
466:
467: uninstallSystemAdapters();
468:
469: // allow start to re-initalize
470: initialized = false;
471: }
472:
473: protected void emitReconfigureNotification() {
474: // emit a reconfigure notification with the configURL
475: Notification n = new Notification(
476: RECONFIGURE_NOTIFICATION_TYPE, this ,
477: getNextNotificationSequenceNumber(),
478: "Log4j subsystem reconfigured");
479: n.setUserData(configURL);
480:
481: super .sendNotification(n);
482: }
483:
484: ///////////////////////////////////////////////////////////////////////////
485: // ThrowableListener Adapter //
486: ///////////////////////////////////////////////////////////////////////////
487:
488: /**
489: * Adapts ThrowableHandler to the Loggger interface. Using nested
490: * class instead of anoynmous class for better logger naming.
491: */
492: private static class ThrowableListenerLoggingAdapter implements
493: ThrowableListener {
494: private Logger log = Logger
495: .getLogger(ThrowableListenerLoggingAdapter.class);
496:
497: public void onThrowable(int type, Throwable t) {
498: switch (type) {
499: default:
500: // if type is not valid then make it any error
501:
502: case ThrowableHandler.Type.ERROR:
503: log.error("Unhandled Throwable", t);
504: break;
505:
506: case ThrowableHandler.Type.WARNING:
507: log.warn("Unhandled Throwable", t);
508: break;
509:
510: case ThrowableHandler.Type.UNKNOWN:
511: // these could be red-herrings, so log them as trace
512: log.trace("Ynhandled Throwable; status is unknown", t);
513: break;
514: }
515: }
516: }
517:
518: ///////////////////////////////////////////////////////////////////////////
519: // URL Watching Timer Task //
520: ///////////////////////////////////////////////////////////////////////////
521:
522: /**
523: * A timer task to check when a URL changes (based on
524: * last modified time) and reconfigure Log4j.
525: */
526: private class URLWatchTimerTask extends TimerTask {
527: private Logger log = Logger.getLogger(URLWatchTimerTask.class);
528:
529: private long lastConfigured = -1;
530:
531: public void run() {
532: log.trace("Checking if configuration changed");
533:
534: boolean trace = log.isTraceEnabled();
535:
536: try {
537: URLConnection conn = configURL.openConnection();
538: if (trace)
539: log.trace("connection: " + conn);
540:
541: long lastModified = conn.getLastModified();
542: if (trace) {
543: log.trace("last configured: " + lastConfigured);
544: log.trace("last modified: " + lastModified);
545: }
546:
547: if (lastConfigured < lastModified) {
548: reconfigure(conn);
549: }
550: } catch (Exception e) {
551: log.warn("Failed to check URL: " + configURL, e);
552: }
553: }
554:
555: public void reconfigure() throws IOException {
556: URLConnection conn = configURL.openConnection();
557: reconfigure(conn);
558: }
559:
560: private void reconfigure(final URLConnection conn) {
561: log.info("Configuring from URL: " + configURL);
562:
563: boolean xml = false;
564: boolean trace = log.isTraceEnabled();
565:
566: // check if the url is xml
567: String contentType = conn.getContentType();
568: if (trace)
569: log.trace("content type: " + contentType);
570:
571: if (contentType == null) {
572: String filename = configURL.getFile().toLowerCase();
573: if (trace)
574: log.trace("filename: " + filename);
575:
576: xml = filename.endsWith(".xml");
577: } else {
578: xml = contentType.equalsIgnoreCase("text/xml");
579: xml |= contentType.equalsIgnoreCase("application/xml");
580: }
581: if (trace)
582: log.trace("reconfiguring; xml=" + xml);
583:
584: // Dump our config if trace is enabled
585: if (trace) {
586: try {
587: java.io.InputStream is = conn.getInputStream();
588: Streams.copy(is, System.out);
589: } catch (Exception e) {
590: log.error("Failed to dump config", e);
591: }
592: }
593:
594: // need to uninstall adapters to avoid problems
595: uninstallSystemAdapters();
596:
597: if (xml) {
598: DOMConfigurator.configure(configURL);
599: } else {
600: PropertyConfigurator.configure(configURL);
601: }
602:
603: /* Set the LogLog.QuietMode. As of log4j1.2.8 this needs to be set to
604: avoid deadlock on exception at the appender level. See bug#696819.
605: */
606: LogLog.setQuietMode(log4jQuietMode);
607:
608: // but make sure they get reinstalled again
609: installSystemAdapters();
610:
611: // and then remember when we were last changed
612: lastConfigured = System.currentTimeMillis();
613:
614: // notify other mbeans that might be interested
615: emitReconfigureNotification();
616: }
617: }
618: }
|