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