
js.log4j.LogProviderImpl Maven / Gradle / Ivy
package js.log4j;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import js.converter.ConverterException;
import js.converter.ConverterRegistry;
import js.lang.Config;
import js.lang.ConfigException;
import js.log.Log;
import js.log.LogContext;
import js.log.LogLevel;
import js.log.LogProvider;
import js.util.Strings;
import org.apache.log4j.Appender;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.Priority;
import org.apache.log4j.spi.OptionHandler;
/**
* Logging manager initialize and configure underlying log4j implementation. Current js-lib logging implementation is
* based on log4j. This class creates needed log4j instances and configure them. Configuration file is stored into
* $catalina.base/conf/log.xml
and has a proprietary format. Here is a sample configuration file, see
* {@link #config(Config)}, {@link #createAppender(Config)} and {@link #createLogger(Config, Map)} for details.
*
* This configuration declares three appenders user to write logging records to: a remote console, standard out and a
* file from logs directory. Also declares two loggers: one for all classes belonging to js
package and
* sub-packages and the second for net.dots
. One may note ${logs}
variable into
* file
parameter from file appender. It is a log4j syntax for system properties. In this particular case
* ${logs}
system property is initialized to log files directory.
*
*
* <?xml version="1.0" encoding="UTF-8"?>
* <log>
* <appender name="CON">
* <class>js.util.log.RemoteConsoleAppender</class>
* <format>%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n</format>
* <parameters>
* <port>8001</port>
* </parameters>
* </appender>
*
* <appender name="STD">
* <class>org.apache.log4j.ConsoleAppender</class>
* <format>%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n</format>
* </appender>
*
* <appender name="APP">
* <class>org.apache.log4j.RollingFileAppender</class>
* <format>%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n</format>
* <parameters>
* <encoding>UTF-8</encoding>
* <file>${logs}/app.log</file>
* <max-file-size>10MB</max-file-size>
* <max-backup-index>10</max-backup-index>
* <immediate-flush>true</immediate-flush>
* </parameters>
* </appender>
*
* <logger name="js">
* <appender>CON</appender>
* <appender>STD</appender>
* <level>OFF</level>
* </logger>
*
* <logger name="net.dots">
* <appender>CON</appender>
* <appender>STD</appender>
* <appender>APP</appender>
* <level>INFO</level>
* </logger>
* </log>
*
*
* Declaring appenders only is not enough. Without declaring loggers there is no log message recorded. The key of
* configuration is to declare loggers, that depend on appenders. Although in sample appenders are grouped and declared
* before loggers, sections order does not matter.
*
* @author Iulian Rotaru
* @version draft
*/
public final class LogProviderImpl implements LogProvider
{
private volatile boolean configured;
private LogContext logContext = new LogContextImpl();
/**
* Configure this server logging subsystem from given configuration. js-lib logging is based on log4j. This method
* takes care to create needed log4j instances and initialize them from given configuration.
*
* Logging configuration is based on log4j abstractions, appender and logger but have a proprietary syntax. Therefore
* there are two majors configuration elements: appender
and logger
. See
* {@link #createAppender(Config)} and {@link #createLogger(Config, Map)} for description. Below is a simplified
* logging configuration sample file, for a global picture.
*
*
* <log>
* <appender name="SERVER">
* <class>org.apache.log4j.RollingFileAppender</class>
* <format>%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n</format>
* <parameters>
* <file>server.log</file>
* <max-file-size>10MB</max-file-size>
* <max-backup-index>10</max-backup-index>
* </parameters>
* </appender>
*
* <logger name="org.apache">
* <appender>SERVER</appender>
* <level>DEBUG</level>
* </logger>
* </log>
*
*
* Note that log4j root logger is created internally using all appenders and level OFF
. So, if no logger
* defined no logging event is recorded. Also, logging is disabled if this method is not called.
*
* @param config configuration object.
* @throws ConfigException if configuration object is not well formed.
*/
public void config(Config config) throws ConfigException
{
if(System.getProperty("logs") == null) {
throw new ConfigException("Missing system property required for log4j implementation.");
}
ConverterRegistry.getInstance().registerConverter(Priority.class, Log4jPriorityConverter.class);
Map appenders = new HashMap();
for(Config element : config.findChildren("appender")) {
Appender appender = createAppender(element);
appenders.put(appender.getName(), appender);
}
// logger config is invoked from server class init that is called from servlet context listener
// there may be loggers created before this config is executed and those are using default configuration
// if this is the case, remove all default appenders and create those from this config
Logger rootLogger = Logger.getRootLogger();
if(configured) {
rootLogger.removeAllAppenders();
}
configured = true;
for(Appender appender : appenders.values()) {
rootLogger.addAppender(appender);
}
rootLogger.setLevel(Level.OFF);
for(Config element : config.findChildren("logger")) {
createLogger(element, appenders);
}
}
@Override
public Log getLogger(String loggerName)
{
if(!configured) {
configDefault();
}
return new LogImpl(loggerName);
}
@Override
public LogContext getLogContext()
{
return logContext;
}
// ----------------------------------------------------
/**
* Default log4j configuration used when external configuration is not performed. This is a minimal configuration that
* append to console all logging levels.
*/
private void configDefault()
{
configured = true;
ConsoleAppender appender = new ConsoleAppender();
appender.setLayout(new PatternLayout("%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n"));
appender.activateOptions();
Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
rootLogger.setLevel(Level.ERROR);
}
/**
* Create log4j appender instance and initialize it from js-lib configuration element
. Appender has a
* name
used to map appenders to loggers. Appenders map given to {@link #createLogger(Config, Map)} uses
* this appender name
as key.
*
* Every appender has a class identifying implementation and a message format describing log record. Formatting text
* is passed as it is to {@link PatternLayout}, from log4j implementation.
*
*
* <appender name="SERVER">
* <class>org.apache.log4j.RollingFileAppender</class>
* <format>%d{dd HH:mm:ss,SSS} [%t] %-5p %c %x- %m%n</format>
* <parameters>
* <file>${logs}/server.log</file>
* <max-file-size>10MB</max-file-size>
* <max-backup-index>10</max-backup-index>
* </parameters>
* </appender>
*
*
* An appender can also have a variable number of parameters, specific to appender class. See appender class API for
* supported parameters and parameters type. Parameter element name is converted to setter name, see
* {@link #getSetterName(String)} and element value used to set appender parameter. For example in above configuration
* sample, invoke RollingFileAppender#setFile("${logs}/server.log"); One may notice ${logs}
variable. It
* is a log4j notation for system properties.
*
* @param appenderConfig appender configuration element.
* @return newly created appender instance.
* @throws ConfigException if appender creation fails but not necessarily because of bad configuration.
*/
@SuppressWarnings("unchecked")
private static Appender createAppender(Config appenderConfig) throws ConfigException
{
String appenderName = appenderConfig.getAttribute("name");
if(appenderName == null) {
throw new ConfigException("Missing attribute from appender.");
}
String className = appenderConfig.getChildValue("class");
if(className == null) {
throw new ConfigException("Missing element from appender |%s|.", appenderName);
}
Class extends Appender> appenderClass;
try {
appenderClass = (Class extends Appender>)Class.forName(className);
}
catch(ClassNotFoundException e) {
throw new ConfigException("Appender class |%s| not found.", className);
}
Appender appender;
try {
appender = appenderClass.newInstance();
}
catch(Exception e) {
throw new ConfigException(e);
}
appender.setName(appenderName);
String format = appenderConfig.getChildValue("format");
if(format != null) {
appender.setLayout(new PatternLayout(format));
}
Config parameters = appenderConfig.getChild("parameters");
if(parameters != null) {
String setterName = null;
for(Config parameterConfig : parameters.getChildren()) {
String parameterName = parameterConfig.getName();
setterName = getSetterName(parameterName);
Method setter = getAppenderSetter(appenderClass, setterName);
if(!Modifier.isPublic(setter.getModifiers())) {
throw new ConfigException("Private setter |%s| on appender |%s|.", setterName, className);
}
Class>[] formalParameters = setter.getParameterTypes();
if(formalParameters.length != 1) {
throw new ConfigException("Bad format parameters count |%d| for setter |%s| on appender |%s|.", formalParameters.length, setterName, className);
}
setAppenderParameter(appender, setter, getParameterValue(parameterConfig, formalParameters[0]));
}
}
if(appender instanceof OptionHandler) {
((OptionHandler)appender).activateOptions();
}
return appender;
}
/**
* Create log4j logger instance, add appenders and set logger level. See below for sample of logger configuration
* element. A logger should have one or many appenders and a level. It is considered bad configuration if appender or
* level is missing. Also appender value should match a configured appender, that is, found in appenders
* map. Level value should be enumerated by {@link LogLevel}.
*
* Logger name
obeys log4j convention: all logger instances that starts with given name
are
* subject to this configuration element. In sample, all classes from org.apache
package and all
* sub-packages have DEBUG level and write to SERVER and CONSOLE appenders.
*
*
* <logger name="org.apache">
* <appender>SERVER</appender>
* <appender>CONSOLE</appender>
* <level>DEBUG</level>
* </logger>
*
*
* For appenders map configuration see {@link #createAppender(Config)}. Finally, created logger has appender
* additivity set to false so logger writes only to appenders explicitly configured.
*
* @param config logger configuration element,
* @param appenders appenders map, configured per logging system.
* @return newly created and initialized log4j logger.
* @throws ConfigException if configured appender is missing or is not present into given appenders
list
* or level is missing.
*/
private static Logger createLogger(Config config, Map appenders) throws ConfigException
{
String loggerName = config.getAttribute("name");
Logger logger = Logger.getLogger(loggerName);
logger.setAdditivity(false);
List appenderElements = config.findChildren("appender");
if(appenderElements.isEmpty()) {
throw new ConfigException("Invalid logger |%s|. Missing appender(s).", loggerName);
}
for(Config appenderElement : appenderElements) {
String appenderName = appenderElement.getValue();
if(appenderName == null) {
throw new ConfigException("Invalid logger |%s|. Empty appender.", loggerName);
}
Appender appender = appenders.get(appenderName);
if(appender == null) {
throw new ConfigException("Invalid logger |%s|. Refered appender |%s| is not defined.", loggerName, appenderName);
}
logger.addAppender(appender);
}
Config levelElement = config.getChild("level");
if(levelElement == null) {
throw new ConfigException("Invalid logger |%s|. Missing logger level.", loggerName);
}
String levelName = levelElement.getValue();
if(levelName == null) {
throw new ConfigException("Invalid logger |%s|. Empty level.", loggerName);
}
LogLevel logLevel = null;
try {
logLevel = LogLevel.valueOf(levelName);
}
catch(Exception e) {
throw new ConfigException("Invalid logger |%s|. Bad logger level value |%s|.", loggerName, levelName);
}
logger.setLevel(LevelMap.log4jLevel(logLevel));
return logger;
}
/**
* Get named setter method for appender class or its super-hierarchy, throwing exception if not found. This method
* tries to locate named method into appender class or all super-classes till {@link Object}. Missing method is
* considered bad configuration and throws exception.
*
* @param appenderClass appender class,
* @param setterName setter method name,
* @return appender setter method.
* @throws ConfigException if named setter method is not found.
*/
@SuppressWarnings("unchecked")
private static Method getAppenderSetter(Class extends Appender> appenderClass, String setterName) throws ConfigException
{
// not optimal but cannot access method by name since do not know formal parameters
for(Method method : appenderClass.getMethods()) {
if(method.getName().equals(setterName)) {
if(method.getParameterTypes().length == 1) {
return method;
}
}
}
Class> superClass = appenderClass.getSuperclass();
if(!superClass.equals(Object.class)) {
throw new ConfigException("Missing setter |%s| method from log4j appender |%s|.", setterName, appenderClass);
}
// do not use recursive calls counter since base Object is never too far
return getAppenderSetter((Class extends Appender>)superClass, setterName);
}
/**
* Set parameter value to log4j appender instance. This method is meant to invoke appender setter by name not to
* access private ones. When this method is invoked appender setter is guaranteed to exist - so no need to be worried
* about log4j API changes.
*
* @param appender log4j appender instance,
* @param setter existing setter method,
* @param parameter appender parameter value.
* @throws ConfigException if appender setter execution fails.
*/
private static void setAppenderParameter(Appender appender, Method setter, Object parameter) throws ConfigException
{
try {
setter.invoke(appender, new Object[]
{
parameter
});
}
catch(InvocationTargetException e) {
throw new ConfigException("Error executing setter |%s| on |%s|.", setter.getName(), appender.getClass().getCanonicalName());
}
catch(Exception e) {
// here exception can be one of: SecurityException, IllegalAccessException or IllegalArgumentException
// none of them are possible in this context
throw new IllegalStateException(e);
}
}
/**
* Create method setter name from dashed parameter name. Parameter name is loaded from logger configuration and used
* dash to separate words, if many. This method convert to camel case and prefix with set
. For example,
* returns setParameterName
for parameter-name
.
*
* @param parameterName dashed parameter name.
* @return method setter.
*/
private static String getSetterName(String parameterName)
{
StringBuilder sb = new StringBuilder("set");
// first character from parameter name should be promoted to upper case since will follow after 'set' prefix
boolean upperCase = true;
for(int i = 0; i < parameterName.length(); ++i) {
char c = parameterName.charAt(i);
if(c == '-') {
upperCase = true;
continue;
}
sb.append(upperCase ? Character.toUpperCase(c) : c);
upperCase = false;
}
return sb.toString();
}
/**
* Get parameter value from configuration object converted to requested type. This method also scans for standard
* ${...}
variable pattern and replace with system property. This feature is especially added for log4j
* logging directory that is stored into ${logs}
system environment variable. Anyway, implementation is
* generic to allow for any variable name.
*
* @param parameterConfig parameter configuration object,
* @param type value type.
* @param object instance type.
* @return value object of requested type.
* @throws ConfigException if variable exist but is not well formed.
* @throws ConverterException if there is no converter registered for requested type or string conversion fails.
*/
private static T getParameterValue(Config parameterConfig, Class type) throws ConfigException
{
String text = parameterConfig.getValue();
String variableName = getVariableName(text);
if(variableName != null) {
String variableValue = System.getProperty(variableName);
if(variableValue != null) {
text = text.replace(Strings.concat("${", variableName, '}'), variableValue);
}
}
return ConverterRegistry.getConverter().asObject(text, type);
}
/**
* Get variable name from text or null if no variable found. Scan for standard ${...}
variable pattern
* and return variable name. Returned variable name does not contain variable mark-up. This method assume there is at
* most one single variable; if more just return the first one. If none returns null.
*
* @param text text to scan for variable.
* @return variable name or null.
* @throws ConfigException if variable start mark-up was found but no closing.
*/
private static String getVariableName(String text) throws ConfigException
{
int variableStartIndex = text.indexOf("${");
if(variableStartIndex == -1) {
return null;
}
int variableEndIndex = text.indexOf('}', variableStartIndex);
if(variableEndIndex == -1) {
throw new ConfigException("Bad attribute value |%s|. Missing variable end mark.", text);
}
return text.substring(variableStartIndex + 2, variableEndIndex);
}
}