View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.log4j.config;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.lang.reflect.InvocationTargetException;
22  import java.util.ArrayList;
23  import java.util.Enumeration;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Properties;
29  import java.util.SortedMap;
30  import java.util.StringTokenizer;
31  import java.util.TreeMap;
32  
33  import org.apache.log4j.Appender;
34  import org.apache.log4j.Layout;
35  import org.apache.log4j.LogManager;
36  import org.apache.log4j.PatternLayout;
37  import org.apache.log4j.bridge.AppenderAdapter;
38  import org.apache.log4j.bridge.AppenderWrapper;
39  import org.apache.log4j.helpers.OptionConverter;
40  import org.apache.log4j.spi.ErrorHandler;
41  import org.apache.log4j.spi.Filter;
42  import org.apache.logging.log4j.core.LoggerContext;
43  import org.apache.logging.log4j.core.config.Configuration;
44  import org.apache.logging.log4j.core.config.ConfigurationSource;
45  import org.apache.logging.log4j.core.config.LoggerConfig;
46  import org.apache.logging.log4j.core.config.status.StatusConfiguration;
47  import org.apache.logging.log4j.util.LoaderUtil;
48  
49  /**
50   * Construct a configuration based on Log4j 1 properties.
51   */
52  public class PropertiesConfiguration  extends Log4j1Configuration {
53  
54      private static final String CATEGORY_PREFIX = "log4j.category.";
55      private static final String LOGGER_PREFIX = "log4j.logger.";
56      private static final String ADDITIVITY_PREFIX = "log4j.additivity.";
57      private static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory";
58      private static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
59      private static final String APPENDER_PREFIX = "log4j.appender.";
60      private static final String LOGGER_REF	= "logger-ref";
61      private static final String ROOT_REF		= "root-ref";
62      private static final String APPENDER_REF_TAG = "appender-ref";
63      public static final long DEFAULT_DELAY = 60000;
64      public static final String DEBUG_KEY="log4j.debug";
65  
66      private static final String INTERNAL_ROOT_NAME = "root";
67  
68      private final Map<String, Appender> registry;
69  
70      /**
71       * Constructor.
72       * @param loggerContext The LoggerContext.
73       * @param source The ConfigurationSource.
74       * @param monitorIntervalSeconds The monitoring interval in seconds.
75       */
76      public PropertiesConfiguration(final LoggerContext loggerContext, final ConfigurationSource source,
77              int monitorIntervalSeconds) {
78          super(loggerContext, source, monitorIntervalSeconds);
79          registry = new HashMap<>();
80      }
81  
82      @Override
83      public void doConfigure() {
84          InputStream is = getConfigurationSource().getInputStream();
85          Properties props = new Properties();
86          try {
87              props.load(is);
88          } catch (Exception e) {
89              LOGGER.error("Could not read configuration file [{}].", getConfigurationSource().toString(), e);
90              return;
91          }
92          // If we reach here, then the config file is alright.
93          doConfigure(props);
94      }
95  
96      @Override
97      public Configuration reconfigure() {
98          try {
99              final ConfigurationSource source = getConfigurationSource().resetInputStream();
100             if (source == null) {
101                 return null;
102             }
103             final PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory();
104             final PropertiesConfiguration config =
105                     (PropertiesConfiguration) factory.getConfiguration(getLoggerContext(), source);
106             return config == null || config.getState() != State.INITIALIZING ? null : config;
107         } catch (final IOException ex) {
108             LOGGER.error("Cannot locate file {}: {}", getConfigurationSource(), ex);
109         }
110         return null;
111     }
112 
113     /**
114      * Read configuration from a file. <b>The existing configuration is
115      * not cleared nor reset.</b> If you require a different behavior,
116      * then call {@link  LogManager#resetConfiguration
117      * resetConfiguration} method before calling
118      * <code>doConfigure</code>.
119      *
120      * <p>The configuration file consists of statements in the format
121      * <code>key=value</code>. The syntax of different configuration
122      * elements are discussed below.
123      *
124      * <p>The level value can consist of the string values OFF, FATAL,
125      * ERROR, WARN, INFO, DEBUG, ALL or a <em>custom level</em> value. A
126      * custom level value can be specified in the form
127      * level#classname. By default the repository-wide threshold is set
128      * to the lowest possible value, namely the level <code>ALL</code>.
129      * </p>
130      *
131      *
132      * <h3>Appender configuration</h3>
133      *
134      * <p>Appender configuration syntax is:
135      * <pre>
136      * # For appender named <i>appenderName</i>, set its class.
137      * # Note: The appender name can contain dots.
138      * log4j.appender.appenderName=fully.qualified.name.of.appender.class
139      *
140      * # Set appender specific options.
141      * log4j.appender.appenderName.option1=value1
142      * ...
143      * log4j.appender.appenderName.optionN=valueN
144      * </pre>
145      * <p>
146      * For each named appender you can configure its {@link Layout}. The
147      * syntax for configuring an appender's layout is:
148      * <pre>
149      * log4j.appender.appenderName.layout=fully.qualified.name.of.layout.class
150      * log4j.appender.appenderName.layout.option1=value1
151      * ....
152      * log4j.appender.appenderName.layout.optionN=valueN
153      * </pre>
154      * <p>
155      * The syntax for adding {@link Filter}s to an appender is:
156      * <pre>
157      * log4j.appender.appenderName.filter.ID=fully.qualified.name.of.filter.class
158      * log4j.appender.appenderName.filter.ID.option1=value1
159      * ...
160      * log4j.appender.appenderName.filter.ID.optionN=valueN
161      * </pre>
162      * The first line defines the class name of the filter identified by ID;
163      * subsequent lines with the same ID specify filter option - value
164      * pairs. Multiple filters are added to the appender in the lexicographic
165      * order of IDs.
166      * <p>
167      * The syntax for adding an {@link ErrorHandler} to an appender is:
168      * <pre>
169      * log4j.appender.appenderName.errorhandler=fully.qualified.name.of.errorhandler.class
170      * log4j.appender.appenderName.errorhandler.appender-ref=appenderName
171      * log4j.appender.appenderName.errorhandler.option1=value1
172      * ...
173      * log4j.appender.appenderName.errorhandler.optionN=valueN
174      * </pre>
175      *
176      * <h3>Configuring loggers</h3>
177      *
178      * <p>The syntax for configuring the root logger is:
179      * <pre>
180      * log4j.rootLogger=[level], appenderName, appenderName, ...
181      * </pre>
182      *
183      * <p>This syntax means that an optional <em>level</em> can be
184      * supplied followed by appender names separated by commas.
185      *
186      * <p>The level value can consist of the string values OFF, FATAL,
187      * ERROR, WARN, INFO, DEBUG, ALL or a <em>custom level</em> value. A
188      * custom level value can be specified in the form
189      * <code>level#classname</code>.
190      *
191      * <p>If a level value is specified, then the root level is set
192      * to the corresponding level.  If no level value is specified,
193      * then the root level remains untouched.
194      *
195      * <p>The root logger can be assigned multiple appenders.
196      *
197      * <p>Each <i>appenderName</i> (separated by commas) will be added to
198      * the root logger. The named appender is defined using the
199      * appender syntax defined above.
200      *
201      * <p>For non-root categories the syntax is almost the same:
202      * <pre>
203      * log4j.logger.logger_name=[level|INHERITED|NULL], appenderName, appenderName, ...
204      * </pre>
205      *
206      * <p>The meaning of the optional level value is discussed above
207      * in relation to the root logger. In addition however, the value
208      * INHERITED can be specified meaning that the named logger should
209      * inherit its level from the logger hierarchy.
210      *
211      * <p>If no level value is supplied, then the level of the
212      * named logger remains untouched.
213      *
214      * <p>By default categories inherit their level from the
215      * hierarchy. However, if you set the level of a logger and later
216      * decide that that logger should inherit its level, then you should
217      * specify INHERITED as the value for the level value. NULL is a
218      * synonym for INHERITED.
219      *
220      * <p>Similar to the root logger syntax, each <i>appenderName</i>
221      * (separated by commas) will be attached to the named logger.
222      *
223      * <p>See the <a href="../../../../manual.html#additivity">appender
224      * additivity rule</a> in the user manual for the meaning of the
225      * <code>additivity</code> flag.
226      *
227      *
228      * # Set options for appender named "A1".
229      * # Appender "A1" will be a SyslogAppender
230      * log4j.appender.A1=org.apache.log4j.net.SyslogAppender
231      *
232      * # The syslog daemon resides on www.abc.net
233      * log4j.appender.A1.SyslogHost=www.abc.net
234      *
235      * # A1's layout is a PatternLayout, using the conversion pattern
236      * # <b>%r %-5p %c{2} %M.%L %x - %m\n</b>. Thus, the log output will
237      * # include # the relative time since the start of the application in
238      * # milliseconds, followed by the level of the log request,
239      * # followed by the two rightmost components of the logger name,
240      * # followed by the callers method name, followed by the line number,
241      * # the nested diagnostic context and finally the message itself.
242      * # Refer to the documentation of {@link PatternLayout} for further information
243      * # on the syntax of the ConversionPattern key.
244      * log4j.appender.A1.layout=org.apache.log4j.PatternLayout
245      * log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %c{2} %M.%L %x - %m\n
246      *
247      * # Set options for appender named "A2"
248      * # A2 should be a RollingFileAppender, with maximum file size of 10 MB
249      * # using at most one backup file. A2's layout is TTCC, using the
250      * # ISO8061 date format with context printing enabled.
251      * log4j.appender.A2=org.apache.log4j.RollingFileAppender
252      * log4j.appender.A2.MaxFileSize=10MB
253      * log4j.appender.A2.MaxBackupIndex=1
254      * log4j.appender.A2.layout=org.apache.log4j.TTCCLayout
255      * log4j.appender.A2.layout.ContextPrinting=enabled
256      * log4j.appender.A2.layout.DateFormat=ISO8601
257      *
258      * # Root logger set to DEBUG using the A2 appender defined above.
259      * log4j.rootLogger=DEBUG, A2
260      *
261      * # Logger definitions:
262      * # The SECURITY logger inherits is level from root. However, it's output
263      * # will go to A1 appender defined above. It's additivity is non-cumulative.
264      * log4j.logger.SECURITY=INHERIT, A1
265      * log4j.additivity.SECURITY=false
266      *
267      * # Only warnings or above will be logged for the logger "SECURITY.access".
268      * # Output will go to A1.
269      * log4j.logger.SECURITY.access=WARN
270      *
271      *
272      * # The logger "class.of.the.day" inherits its level from the
273      * # logger hierarchy.  Output will go to the appender's of the root
274      * # logger, A2 in this case.
275      * log4j.logger.class.of.the.day=INHERIT
276      * </pre>
277      *
278      * <p>Refer to the <b>setOption</b> method in each Appender and
279      * Layout for class specific options.
280      *
281      * <p>Use the <code>#</code> or <code>!</code> characters at the
282      * beginning of a line for comments.
283      *
284      * <p>
285      */
286     private void doConfigure(Properties properties) {
287         String status = "error";
288         String value = properties.getProperty(DEBUG_KEY);
289         if (value == null) {
290             value = properties.getProperty("log4j.configDebug");
291             if (value != null) {
292                 LOGGER.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
293             }
294         }
295 
296         if (value != null) {
297             status = OptionConverter.toBoolean(value, false) ? "debug" : "error";
298         }
299 
300         final StatusConfiguration statusConfig = new StatusConfiguration().withStatus(status);
301         statusConfig.initialize();
302 
303         configureRoot(properties);
304         parseLoggers(properties);
305 
306         LOGGER.debug("Finished configuring.");
307     }
308 
309     // --------------------------------------------------------------------------
310     // Internal stuff
311     // --------------------------------------------------------------------------
312 
313     private void configureRoot(Properties props) {
314         String effectiveFrefix = ROOT_LOGGER_PREFIX;
315         String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);
316 
317         if (value == null) {
318             value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
319             effectiveFrefix = ROOT_CATEGORY_PREFIX;
320         }
321 
322         if (value == null) {
323             LOGGER.debug("Could not find root logger information. Is this OK?");
324         } else {
325             LoggerConfig root = getRootLogger();
326             parseLogger(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
327         }
328     }
329 
330     /**
331      * Parse non-root elements, such non-root categories and renderers.
332      */
333     private void parseLoggers(Properties props) {
334         Enumeration<?> enumeration = props.propertyNames();
335         while (enumeration.hasMoreElements()) {
336             String key = Objects.toString(enumeration.nextElement(), null);
337             if (key.startsWith(CATEGORY_PREFIX) || key.startsWith(LOGGER_PREFIX)) {
338                 String loggerName = null;
339                 if (key.startsWith(CATEGORY_PREFIX)) {
340                     loggerName = key.substring(CATEGORY_PREFIX.length());
341                 } else if (key.startsWith(LOGGER_PREFIX)) {
342                     loggerName = key.substring(LOGGER_PREFIX.length());
343                 }
344                 String value = OptionConverter.findAndSubst(key, props);
345                 LoggerConfig loggerConfig = getLogger(loggerName);
346                 if (loggerConfig == null) {
347                     boolean additivity = getAdditivityForLogger(props, loggerName);
348                     loggerConfig = new LoggerConfig(loggerName, org.apache.logging.log4j.Level.ERROR, additivity);
349                     addLogger(loggerName, loggerConfig);
350                 }
351                 parseLogger(props, loggerConfig, key, loggerName, value);
352             }
353         }
354     }
355 
356     /**
357      * Parse the additivity option for a non-root category.
358      */
359     private boolean getAdditivityForLogger(Properties props, String loggerName) {
360         boolean additivity = true;
361         String key = ADDITIVITY_PREFIX + loggerName;
362         String value = OptionConverter.findAndSubst(key, props);
363         LOGGER.debug("Handling {}=[{}]", key, value);
364         // touch additivity only if necessary
365         if ((value != null) && (!value.equals(""))) {
366             additivity = OptionConverter.toBoolean(value, true);
367         }
368         return additivity;
369     }
370 
371     /**
372      * This method must work for the root category as well.
373      */
374     private void parseLogger(Properties props, LoggerConfig logger, String optionKey, String loggerName, String value) {
375 
376         LOGGER.debug("Parsing for [{}] with value=[{}].", loggerName, value);
377         // We must skip over ',' but not white space
378         StringTokenizer st = new StringTokenizer(value, ",");
379 
380         // If value is not in the form ", appender.." or "", then we should set the level of the logger.
381 
382         if (!(value.startsWith(",") || value.equals(""))) {
383 
384             // just to be on the safe side...
385             if (!st.hasMoreTokens()) {
386                 return;
387             }
388 
389             String levelStr = st.nextToken();
390             LOGGER.debug("Level token is [{}].", levelStr);
391 
392             org.apache.logging.log4j.Level level = levelStr == null ? org.apache.logging.log4j.Level.ERROR :
393                     OptionConverter.convertLevel(levelStr, org.apache.logging.log4j.Level.DEBUG);
394             logger.setLevel(level);
395             LOGGER.debug("Logger {} level set to {}", loggerName, level);
396         }
397 
398         Appender appender;
399         String appenderName;
400         while (st.hasMoreTokens()) {
401             appenderName = st.nextToken().trim();
402             if (appenderName == null || appenderName.equals(",")) {
403                 continue;
404             }
405             LOGGER.debug("Parsing appender named \"{}\".", appenderName);
406             appender = parseAppender(props, appenderName);
407             if (appender != null) {
408                 LOGGER.debug("Adding appender named [{}] to loggerConfig [{}].", appenderName,
409                         logger.getName());
410                 logger.addAppender(getAppender(appenderName), null, null);
411             } else {
412                 LOGGER.debug("Appender named [{}] not found.", appenderName);
413             }
414         }
415     }
416 
417     public Appender parseAppender(Properties props, String appenderName) {
418         Appender appender = registry.get(appenderName);
419         if ((appender != null)) {
420             LOGGER.debug("Appender \"" + appenderName + "\" was already parsed.");
421             return appender;
422         }
423         // Appender was not previously initialized.
424         final String prefix = APPENDER_PREFIX + appenderName;
425         final String layoutPrefix = prefix + ".layout";
426         final String filterPrefix = APPENDER_PREFIX + appenderName + ".filter.";
427         String className = OptionConverter.findAndSubst(prefix, props);
428         appender = manager.parseAppender(appenderName, className, prefix, layoutPrefix, filterPrefix, props, this);
429         if (appender == null) {
430             appender = buildAppender(appenderName, className, prefix, layoutPrefix, filterPrefix, props);
431         } else {
432             registry.put(appenderName, appender);
433             if (appender instanceof AppenderWrapper) {
434                 addAppender(((AppenderWrapper) appender).getAppender());
435             } else {
436                 addAppender(new AppenderAdapter(appender).getAdapter());
437             }
438         }
439         return appender;
440     }
441 
442     private Appender buildAppender(final String appenderName, final String className, final String prefix,
443             final String layoutPrefix, final String filterPrefix, final Properties props) {
444         Appender appender = newInstanceOf(className, "Appender");
445         if (appender == null) {
446             return null;
447         }
448         appender.setName(appenderName);
449         appender.setLayout(parseLayout(layoutPrefix, appenderName, props));
450         final String errorHandlerPrefix = prefix + ".errorhandler";
451         String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
452         if (errorHandlerClass != null) {
453             ErrorHandler eh = parseErrorHandler(props, errorHandlerPrefix, errorHandlerClass, appender);
454             if (eh != null) {
455                 appender.setErrorHandler(eh);
456             }
457         }
458         parseAppenderFilters(props, filterPrefix, appenderName);
459         String[] keys = new String[] {
460                 layoutPrefix,
461         };
462         addProperties(appender, keys, props, prefix);
463         if (appender instanceof AppenderWrapper) {
464             addAppender(((AppenderWrapper) appender).getAppender());
465         } else {
466             addAppender(new AppenderAdapter(appender).getAdapter());
467         }
468         registry.put(appenderName, appender);
469         return appender;
470     }
471 
472     public Layout parseLayout(String layoutPrefix, String appenderName, Properties props) {
473         String layoutClass = OptionConverter.findAndSubst(layoutPrefix, props);
474         if (layoutClass == null) {
475             return null;
476         }
477         Layout layout = manager.parseLayout(layoutClass, layoutPrefix, props, this);
478         if (layout == null) {
479             layout = buildLayout(layoutPrefix, layoutClass, appenderName, props);
480         }
481         return layout;
482     }
483 
484     private Layout buildLayout(String layoutPrefix, String className, String appenderName, Properties props) {
485         Layout layout = newInstanceOf(className, "Layout");
486         if (layout == null) {
487             return null;
488         }
489         LOGGER.debug("Parsing layout options for \"{}\".", appenderName);
490         PropertySetter.setProperties(layout, props, layoutPrefix + ".");
491         LOGGER.debug("End of parsing for \"{}\".", appenderName);
492         return layout;
493     }
494 
495     public ErrorHandler parseErrorHandler(final Properties props, final String errorHandlerPrefix,
496             final String errorHandlerClass, final Appender appender) {
497         ErrorHandler eh = newInstanceOf(errorHandlerClass, "ErrorHandler");
498         final String[] keys = new String[] {
499                 errorHandlerPrefix + "." + ROOT_REF,
500                 errorHandlerPrefix + "." + LOGGER_REF,
501                 errorHandlerPrefix + "." + APPENDER_REF_TAG
502         };
503         addProperties(eh, keys, props, errorHandlerPrefix);
504         return eh;
505     }
506 
507     public void addProperties(final Object obj, final String[] keys, final Properties props, final String prefix) {
508         final Properties edited = new Properties();
509         props.stringPropertyNames().stream().filter(name -> {
510             if (name.startsWith(prefix)) {
511                 for (String key : keys) {
512                     if (name.equals(key)) {
513                         return false;
514                     }
515                 }
516                 return true;
517             }
518             return false;
519         }).forEach(name -> edited.put(name, props.getProperty(name)));
520         PropertySetter.setProperties(obj, edited, prefix + ".");
521     }
522 
523 
524     public Filter parseAppenderFilters(Properties props, String filterPrefix, String appenderName) {
525         // extract filters and filter options from props into a hashtable mapping
526         // the property name defining the filter class to a list of pre-parsed
527         // name-value pairs associated to that filter
528         int fIdx = filterPrefix.length();
529         SortedMap<String, List<NameValue>> filters = new TreeMap<>();
530         Enumeration<?> e = props.keys();
531         String name = "";
532         while (e.hasMoreElements()) {
533             String key = (String) e.nextElement();
534             if (key.startsWith(filterPrefix)) {
535                 int dotIdx = key.indexOf('.', fIdx);
536                 String filterKey = key;
537                 if (dotIdx != -1) {
538                     filterKey = key.substring(0, dotIdx);
539                     name = key.substring(dotIdx + 1);
540                 }
541                 List<NameValue> filterOpts = filters.computeIfAbsent(filterKey, k -> new ArrayList<>());
542                 if (dotIdx != -1) {
543                     String value = OptionConverter.findAndSubst(key, props);
544                     filterOpts.add(new NameValue(name, value));
545                 }
546             }
547         }
548 
549         Filter head = null;
550         Filter next = null;
551         for (Map.Entry<String, List<NameValue>> entry : filters.entrySet()) {
552             String clazz = props.getProperty(entry.getKey());
553             Filter filter = null;
554             if (clazz != null) {
555                 filter = manager.parseFilter(clazz, filterPrefix, props, this);
556                 if (filter == null) {
557                     LOGGER.debug("Filter key: [{}] class: [{}] props: {}", entry.getKey(), clazz, entry.getValue());
558                     filter = buildFilter(clazz, appenderName, entry.getValue());
559                 }
560             }
561             if (filter != null) {
562                 if (head == null) {
563                     head = filter;
564                 } else {
565                     next.setNext(filter);
566                 }
567                 next = filter;
568             }
569         }
570         return head;
571     }
572 
573     private Filter buildFilter(String className, String appenderName, List<NameValue> props) {
574         Filter filter = newInstanceOf(className, "Filter");
575         if (filter != null) {
576             PropertySetter propSetter = new PropertySetter(filter);
577             for (NameValue property : props) {
578                 propSetter.setProperty(property.key, property.value);
579             }
580             propSetter.activate();
581         }
582         return filter;
583     }
584 
585 
586     private static <T> T newInstanceOf(String className, String type) {
587         try {
588             return LoaderUtil.newInstanceOf(className);
589         } catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException |
590                 InstantiationException | InvocationTargetException ex) {
591             LOGGER.error("Unable to create {} {} due to {}:{}", type,  className,
592                     ex.getClass().getSimpleName(), ex.getMessage());
593             return null;
594         }
595     }
596 
597     private static class NameValue {
598         String key, value;
599 
600         NameValue(String key, String value) {
601             this.key = key;
602             this.value = value;
603         }
604 
605         @Override
606         public String toString() {
607             return key + "=" + value;
608         }
609     }
610 
611 }