001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.log4j.config; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.lang.reflect.InvocationTargetException; 022import java.util.ArrayList; 023import java.util.Enumeration; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Properties; 029import java.util.SortedMap; 030import java.util.StringTokenizer; 031import java.util.TreeMap; 032 033import org.apache.log4j.Appender; 034import org.apache.log4j.Layout; 035import org.apache.log4j.LogManager; 036import org.apache.log4j.PatternLayout; 037import org.apache.log4j.bridge.AppenderAdapter; 038import org.apache.log4j.bridge.AppenderWrapper; 039import org.apache.log4j.helpers.OptionConverter; 040import org.apache.log4j.spi.ErrorHandler; 041import org.apache.log4j.spi.Filter; 042import org.apache.logging.log4j.core.LoggerContext; 043import org.apache.logging.log4j.core.config.Configuration; 044import org.apache.logging.log4j.core.config.ConfigurationSource; 045import org.apache.logging.log4j.core.config.LoggerConfig; 046import org.apache.logging.log4j.core.config.status.StatusConfiguration; 047import org.apache.logging.log4j.util.LoaderUtil; 048 049/** 050 * Construct a configuration based on Log4j 1 properties. 051 */ 052public class PropertiesConfiguration extends Log4j1Configuration { 053 054 private static final String CATEGORY_PREFIX = "log4j.category."; 055 private static final String LOGGER_PREFIX = "log4j.logger."; 056 private static final String ADDITIVITY_PREFIX = "log4j.additivity."; 057 private static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory"; 058 private static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"; 059 private static final String APPENDER_PREFIX = "log4j.appender."; 060 private static final String LOGGER_REF = "logger-ref"; 061 private static final String ROOT_REF = "root-ref"; 062 private static final String APPENDER_REF_TAG = "appender-ref"; 063 public static final long DEFAULT_DELAY = 60000; 064 public static final String DEBUG_KEY="log4j.debug"; 065 066 private static final String INTERNAL_ROOT_NAME = "root"; 067 068 private final Map<String, Appender> registry; 069 070 /** 071 * Constructor. 072 * @param loggerContext The LoggerContext. 073 * @param source The ConfigurationSource. 074 * @param monitorIntervalSeconds The monitoring interval in seconds. 075 */ 076 public PropertiesConfiguration(final LoggerContext loggerContext, final ConfigurationSource source, 077 int monitorIntervalSeconds) { 078 super(loggerContext, source, monitorIntervalSeconds); 079 registry = new HashMap<>(); 080 } 081 082 @Override 083 public void doConfigure() { 084 InputStream is = getConfigurationSource().getInputStream(); 085 Properties props = new Properties(); 086 try { 087 props.load(is); 088 } catch (Exception e) { 089 LOGGER.error("Could not read configuration file [{}].", getConfigurationSource().toString(), e); 090 return; 091 } 092 // If we reach here, then the config file is alright. 093 doConfigure(props); 094 } 095 096 @Override 097 public Configuration reconfigure() { 098 try { 099 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}