/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.logging;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.ObjectName;
import org.apache.log4j.Level;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.xml.DOMConfigurator;
import org.jboss.logging.log4j.JDKLevel;
import org.jboss.logging.util.LoggerStream;
import org.jboss.logging.util.OnlyOnceErrorHandler;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.util.Strings;
import org.jboss.util.ThrowableHandler;
import org.jboss.util.ThrowableListener;
import org.jboss.util.stream.Streams;
/**
* Initializes the Log4j logging framework. Supports XML and standard
* configuration file formats. Defaults to using 'log4j.xml' read
* from a system resource.
*
* <p>Sets up a {@link ThrowableListener} to adapt unhandled
* throwables to a logger.
*
* <p>Installs {@link LoggerStream} adapters for <tt>System.out</tt> and
* <tt>System.err</tt> to catch and redirect calls to Log4j.
*
* @jmx:mbean name="jboss.system:type=Log4jService,service=Logging"
* extends="org.jboss.system.ServiceMBean"
*
* @version <tt>$Revision: 85674 $</tt>
* @author <a href="mailto:phox@galactica.it">Fulco Muriglio</a>
* @author <a href="mailto:Scott_Stark@displayscape.com">Scott Stark</a>
* @author <a href="mailto:davidjencks@earthlink.net">David Jencks</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
*/
public class Log4jService
extends ServiceMBeanSupport
implements Log4jServiceMBean
{
/**
* The default url for the configuration file. Reads the value
* from the system property <tt>org.jboss.logging.Log4jService.configURL</tt>
* or if that is not set defaults to <tt>resource:log4j.xml</tt>.
*/
public static final String DEFAULT_URL =
System.getProperty(Log4jService.class.getName() + ".configURL", "resource:log4j.xml");
/**
* Default flag to enable/disable cacthing System.out. Reads the value
* from the system property <tt>org.jboss.logging.Log4jService.catchSystemOut</tt>
* or if not set defaults to <tt>true</tt>.
*/
public static final boolean CATCH_SYSTEM_OUT =
getBoolean(Log4jService.class.getName() + ".catchSystemOut", true);
/**
* Default flag to enable/disable cacthing System.err. Reads the value
* from the system property <tt>org.jboss.logging.Log4jService.catchSystemErr</tt>
* or if not set defaults to <tt>true</tt>.
*/
public static final boolean CATCH_SYSTEM_ERR =
getBoolean(Log4jService.class.getName() + ".catchSystemErr", true);
/**
* Default value for system property {@link #JBOSS_SERVER_LOG_THRESHOLD_PROPERTY}
* if it is not otherwise configured.
*/
public static final String DEFAULT_JBOSS_SERVER_LOG_THRESHOLD = "DEBUG";
/** Helper to get boolean value from system property or use default if not set. */
private static boolean getBoolean(String name, boolean defaultValue)
{
String value = System.getProperty(name, null);
if (value == null)
return defaultValue;
return new Boolean(value).booleanValue();
}
/** The URL to the configuration file. */
private URL configURL;
/** The time in seconds between checking for new config. */
private int refreshPeriod;
private ThrowableListenerLoggingAdapter throwableAdapter;
/** The previous value of System.out. */
private PrintStream out;
/** The previous value of System.err. */
private PrintStream err;
/**
* Flag to enable/disable adapting <tt>System.out</tt> to the
* <tt>STDOUT</tt> logger.
*/
private boolean catchSystemOut = CATCH_SYSTEM_OUT;
/**
* Flag to enable/disable adapting <tt>System.out</tt> to the
* <tt>STDERR</tt> logger.
*/
private boolean catchSystemErr = CATCH_SYSTEM_ERR;
/** The org.apache.log4j.helpers.LogLog.setQuietMode flag setting */
private boolean log4jQuietMode = true;
private String defaultJBossServerLogLevel = DEFAULT_JBOSS_SERVER_LOG_THRESHOLD;
/** The URL watch timer (in daemon mode). */
private Timer timer;
/** The specialized timer task to watch our config file. */
private URLWatchTimerTask timerTask;
/**
* A flag to enable start/stop to work as expected,
* but still use create to init early.
*/
private boolean initialized;
/**
* Uses defaults.
*
* @jmx:managed-constructor
*
* @throws MalformedURLException Could not create URL from default (propbably
* a problem with overridden properties).
*/
public Log4jService() throws MalformedURLException
{
this(DEFAULT_URL, 60);
}
/**
* @jmx:managed-constructor
*
* @param url The configuration URL.
*/
public Log4jService(final URL url)
{
this(url, 60);
}
/**
* @jmx:managed-constructor
*
* @param url The configuration URL.
*/
public Log4jService(final String url) throws MalformedURLException
{
this(Strings.toURL(url), 60);
}
/**
* @jmx:managed-constructor
*
* @param url The configuration URL.
* @param refreshPeriod The refreshPeriod in seconds to wait between each check.
*/
public Log4jService(final String url, final int refreshPeriod)
throws MalformedURLException
{
this(Strings.toURL(url), refreshPeriod);
}
/**
* @jmx:managed-constructor
*
* @param url The configuration URL.
* @param refreshPeriod The refreshPeriod in seconds to wait between each check.
*/
public Log4jService(final URL url, final int refreshPeriod)
{
this.configURL = url;
this.refreshPeriod = refreshPeriod;
}
/**
* Set the catch <tt>System.out</tt> flag.
*
* @jmx:managed-attribute
*
* @param flag True to enable, false to disable.
*/
public void setCatchSystemOut(final boolean flag)
{
this.catchSystemOut = flag;
}
/**
* Get the catch <tt>System.out</tt> flag.
*
* @jmx:managed-attribute
*
* @return True if enabled, false if disabled.
*/
public boolean getCatchSystemOut()
{
return catchSystemOut;
}
/**
* Set the catch <tt>System.err</tt> flag.
*
* @jmx:managed-attribute
*
* @param flag True to enable, false to disable.
*/
public void setCatchSystemErr(final boolean flag)
{
this.catchSystemErr = flag;
}
/**
* Get the catch <tt>System.err</tt> flag.
*
* @jmx:managed-attribute
*
* @return True if enabled, false if disabled.
*/
public boolean getCatchSystemErr()
{
return catchSystemErr;
}
/**
* Get the org.apache.log4j.helpers.LogLog.setQuietMode flag
*
* @jmx:managed-attribute
*
* @return True if enabled, false if disabled.
*/
public boolean getLog4jQuietMode()
{
return log4jQuietMode;
}
/**
* Set the org.apache.log4j.helpers.LogLog.setQuietMode flag
*
* @jmx:managed-attribute
*
* @return True if enabled, false if disabled.
*/
public void setLog4jQuietMode(boolean flag)
{
this.log4jQuietMode = flag;
}
/**
* Get the refresh period.
*
* @jmx:managed-attribute
*/
public int getRefreshPeriod()
{
return refreshPeriod;
}
/**
* Set the refresh period.
*
* @jmx:managed-attribute
*/
public void setRefreshPeriod(final int refreshPeriod)
{
this.refreshPeriod = refreshPeriod;
}
/**
* Get the Log4j configuration URL.
*
* @jmx:managed-attribute
*/
public URL getConfigurationURL()
{
return configURL;
}
/**
* Set the Log4j configuration URL.
*
* @jmx:managed-attribute
*/
public void setConfigurationURL(final URL url)
{
this.configURL = url;
}
/**
* {@inheritDoc}
*
* @jmx:managed-attribute
*/
public String getDefaultJBossServerLogThreshold()
{
return defaultJBossServerLogLevel;
}
/**
* {@inheritDoc}
*
* @jmx:managed-attribute
*/
public void setDefaultJBossServerLogThreshold(String level)
{
this.defaultJBossServerLogLevel = level;
}
/**
* Sets the level for a logger of the give name.
*
* <p>Values are trimmed before used.
*
* @jmx:managed-operation
*
* @param name The name of the logger to change level
* @param levelName The name of the level to change the logger to.
*/
public void setLoggerLevel(final String name, final String levelName)
{
org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(name.trim());
Level level = JDKLevel.toLevel(levelName.trim());
logger.setLevel(level);
log.info("Level set to " + level + " for " + name);
}
/**
* Sets the levels of each logger specified by the given comma
* seperated list of logger names.
*
* @jmx:managed-operation
*
* @see #setLoggerLevel
*
* @param list A comma seperated list of logger names.
* @param levelName The name of the level to change the logger to.
*/
public void setLoggerLevels(final String list, final String levelName)
{
StringTokenizer stok = new StringTokenizer(list, ",");
while (stok.hasMoreTokens()) {
String name = stok.nextToken();
setLoggerLevel(name, levelName);
}
}
/**
* Gets the level of the logger of the give name.
*
* @jmx:managed-operation
*
* @param name The name of the logger to inspect.
*/
public String getLoggerLevel(final String name)
{
org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(name);
Level level = logger.getLevel();
if (level != null)
return level.toString();
return null;
}
/**
* Force the logging system to reconfigure.
*
* @jmx:managed-operation
*/
public void reconfigure() throws IOException
{
if (timerTask == null)
throw new IllegalStateException("Service stopped or not started");
timerTask.reconfigure();
}
/**
* Hack to reconfigure and change the URL. This is needed until we
* have a JMX HTML Adapter that can use PropertyEditor to coerce.
*
* @jmx:managed-operation
*
* @param url The new configuration url
*/
public void reconfigure(final String url) throws IOException, MalformedURLException
{
setConfigurationURL(Strings.toURL(url));
reconfigure();
}
private void installSystemAdapters()
{
org.apache.log4j.Logger logger;
// Install catchers
if (catchSystemOut)
{
logger = org.apache.log4j.Logger.getLogger("STDOUT");
out = System.out;
System.setOut(new LoggerStream(logger, Level.INFO, out));
log.debug("Installed System.out adapter");
}
if (catchSystemErr)
{
logger = org.apache.log4j.Logger.getLogger("STDERR");
err = System.err;
OnlyOnceErrorHandler.setOutput(err);
System.setErr(new LoggerStream(logger, Level.ERROR, err));
log.debug("Installed System.err adapter");
}
}
private void uninstallSystemAdapters()
{
// Remove System adapters
if (out != null)
{
System.out.flush();
System.setOut(out);
log.debug("Removed System.out adapter");
out = null;
}
if (err != null)
{
System.err.flush();
System.setErr(err);
log.debug("Removed System.err adapter");
err = null;
}
}
///////////////////////////////////////////////////////////////////////////
// Concrete Service Overrides //
///////////////////////////////////////////////////////////////////////////
protected ObjectName getObjectName(MBeanServer server, ObjectName name)
throws MalformedObjectNameException
{
return name == null ? OBJECT_NAME : name;
}
private void setup() throws Exception
{
if (initialized) return;
establishDefaultLoggingLevelSystemProperty();
timerTask = new URLWatchTimerTask();
timerTask.run();
timer = new Timer("Timer-" + getName(), true);
timer.schedule(timerTask, 1000 * refreshPeriod, 1000 * refreshPeriod);
// Make sure the root Logger has loaded
org.apache.log4j.Logger.getRootLogger();
// Install listener for unhandled throwables to turn them into log messages
throwableAdapter = new ThrowableListenerLoggingAdapter();
ThrowableHandler.addThrowableListener(throwableAdapter);
log.debug("Added ThrowableListener: " + throwableAdapter);
initialized = true;
}
private void establishDefaultLoggingLevelSystemProperty()
{
try
{
if (defaultJBossServerLogLevel != null
&& SysPropertyActions.getProperty(JBOSS_SERVER_LOG_THRESHOLD_PROPERTY, null) == null)
{
SysPropertyActions.setProperty(JBOSS_SERVER_LOG_THRESHOLD_PROPERTY,
defaultJBossServerLogLevel);
}
}
catch (SecurityException e)
{
log.warn("Cannot configure system property " + JBOSS_SERVER_LOG_THRESHOLD_PROPERTY +
" -- " + e.getLocalizedMessage());
}
}
protected void createService() throws Exception
{
setup();
}
protected void startService() throws Exception
{
setup();
}
protected void stopService() throws Exception
{
timer.cancel();
timer = null;
timerTask.cancel();
timerTask = null;
// Remove throwable adapter
ThrowableHandler.removeThrowableListener(throwableAdapter);
throwableAdapter = null;
uninstallSystemAdapters();
// allow start to re-initalize
initialized = false;
}
protected void emitReconfigureNotification()
{
// emit a reconfigure notification with the configURL
Notification n = new Notification(RECONFIGURE_NOTIFICATION_TYPE,
this, getNextNotificationSequenceNumber(), "Log4j subsystem reconfigured");
n.setUserData(configURL);
super.sendNotification(n);
}
///////////////////////////////////////////////////////////////////////////
// ThrowableListener Adapter //
///////////////////////////////////////////////////////////////////////////
/**
* Adapts ThrowableHandler to the Loggger interface. Using nested
* class instead of anoynmous class for better logger naming.
*/
private static class ThrowableListenerLoggingAdapter
implements ThrowableListener
{
private Logger log = Logger.getLogger(ThrowableListenerLoggingAdapter.class);
public void onThrowable(int type, Throwable t)
{
switch (type)
{
default:
// if type is not valid then make it any error
case ThrowableHandler.Type.ERROR:
log.error("Unhandled Throwable", t);
break;
case ThrowableHandler.Type.WARNING:
log.warn("Unhandled Throwable", t);
break;
case ThrowableHandler.Type.UNKNOWN:
// these could be red-herrings, so log them as trace
log.trace("Ynhandled Throwable; status is unknown", t);
break;
}
}
}
///////////////////////////////////////////////////////////////////////////
// URL Watching Timer Task //
///////////////////////////////////////////////////////////////////////////
/**
* A timer task to check when a URL changes (based on
* last modified time) and reconfigure Log4j.
*/
private class URLWatchTimerTask
extends TimerTask
{
private Logger log = Logger.getLogger(URLWatchTimerTask.class);
private long lastConfigured = -1;
public void run()
{
log.trace("Checking if configuration changed");
boolean trace = log.isTraceEnabled();
try
{
URLConnection conn = configURL.openConnection();
if (trace)
log.trace("connection: " + conn);
long lastModified = conn.getLastModified();
if (trace)
{
log.trace("last configured: " + lastConfigured);
log.trace("last modified: " + lastModified);
}
if (lastConfigured < lastModified)
{
reconfigure(conn);
}
}
catch (Exception e)
{
log.warn("Failed to check URL: " + configURL, e);
}
}
public void reconfigure() throws IOException
{
URLConnection conn = configURL.openConnection();
reconfigure(conn);
}
private void reconfigure(final URLConnection conn)
{
log.debug("Configuring from URL: " + configURL);
boolean xml = false;
boolean trace = log.isTraceEnabled();
// check if the url is xml
String contentType = conn.getContentType();
if (trace)
log.trace("content type: " + contentType);
if (contentType == null)
{
String filename = configURL.getFile().toLowerCase();
if (trace) log.trace("filename: " + filename);
xml = filename.endsWith(".xml");
}
else
{
xml = contentType.equalsIgnoreCase("text/xml");
xml |= contentType.equalsIgnoreCase("application/xml");
}
if (trace)
log.trace("reconfiguring; xml=" + xml);
// Dump our config if trace is enabled
if (trace)
{
try
{
java.io.InputStream is = conn.getInputStream();
Streams.copy(is, System.out);
}
catch (Exception e)
{
log.error("Failed to dump config", e);
}
}
// need to uninstall adapters to avoid problems
uninstallSystemAdapters();
if (xml)
{
DOMConfigurator.configure(configURL);
}
else
{
PropertyConfigurator.configure(configURL);
}
/* Set the LogLog.QuietMode. As of log4j1.2.8 this needs to be set to
avoid deadlock on exception at the appender level. See bug#696819.
*/
LogLog.setQuietMode(log4jQuietMode);
// but make sure they get reinstalled again
installSystemAdapters();
// and then remember when we were last changed
lastConfigured = System.currentTimeMillis();
// notify other mbeans that might be interested
emitReconfigureNotification();
}
}
}