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.logging.log4j.jul; 18 19 //note: NO import of Logger, Level, LogManager to prevent conflicts JUL/log4j 20 import java.beans.PropertyChangeEvent; 21 import java.beans.PropertyChangeListener; 22 import java.util.Enumeration; 23 import java.util.HashSet; 24 import java.util.Map; 25 import java.util.Set; 26 import java.util.logging.LogRecord; 27 28 import org.apache.logging.log4j.core.LoggerContext; 29 import org.apache.logging.log4j.core.config.Configuration; 30 import org.apache.logging.log4j.core.config.LoggerConfig; 31 import org.apache.logging.log4j.spi.ExtendedLogger; 32 import org.apache.logging.log4j.status.StatusLogger; 33 34 35 /** 36 * Bridge from JUL to log4j2.<br> 37 * This is an alternative to log4j.jul.LogManager (running as complete JUL replacement), 38 * especially useful for webapps running on a container for which the LogManager cannot or 39 * should not be used.<br><br> 40 * 41 * Installation/usage:<ul> 42 * <li> Declaratively inside JUL's <code>logging.properties</code>:<br> 43 * <code>handlers = org.apache.logging.log4j.jul.Log4jBridgeHandler</code><br> 44 * (and typically also: <code>org.apache.logging.log4j.jul.Log4jBridgeHandler.propagateLevels = true</code> )<br> 45 * Note: in a webapp running on Tomcat, you may create a <code>WEB-INF/classes/logging.properties</code> 46 * file to configure JUL for this webapp only: configured handlers and log levels affect your webapp only! 47 * This file is then the <i>complete</i> JUL configuration, so JUL's defaults (e.g. log level INFO) apply 48 * for stuff not explicitly defined therein. 49 * <li> Programmatically by calling <code>install()</code> method, 50 * e.g. inside ServletContextListener static-class-init. or contextInitialized() 51 * </ul> 52 * Configuration (in JUL's <code>logging.properties</code>):<ul> 53 * <li> Log4jBridgeHandler.<code>suffixToAppend</code><br> 54 * String, suffix to append to JUL logger names, to easily recognize bridged log messages. 55 * A dot "." is automatically prepended, so configuration for the basis logger is used<br> 56 * Example: <code>Log4jBridgeHandler.suffixToAppend = _JUL</code><br> 57 * Useful, for example, if you use JSF because it logs exceptions and throws them afterwards; 58 * you can easily recognize the duplicates with this (or concentrate on the non-JUL-logs). 59 * <li> Log4jBridgeHandler.<code>propagateLevels</code> boolean, "true" to automatically propagate log4j log levels to JUL. 60 * <li> Log4jBridgeHandler.<code>sysoutDebug</code> boolean, perform some (developer) debug output to sysout 61 * </ul> 62 * 63 * Log levels are translated with {@link LevelTranslator}, see also 64 * <a href="https://logging.apache.org/log4j/2.x/log4j-jul/index.html#Default_Level_Conversions">log4j doc</a>.<br><br> 65 * 66 * Restrictions:<ul> 67 * <li> Manually given source/location info in JUL (e.g. entering(), exiting(), throwing(), logp(), logrb() ) 68 * will NOT be considered, i.e. gets lost in log4j logging. 69 * <li> Log levels of JUL have to be adjusted according to log4j log levels: 70 * Either by using "propagateLevels" (preferred), or manually by specifying them explicitly, 71 * i.e. logging.properties and log4j2.xml have some redundancies. 72 * <li> Only JUL log events that are allowed according to the JUL log level get to this handler and thus to log4j. 73 * This is only relevant and important if you NOT use "propagateLevels". 74 * If you set <code>.level = SEVERE</code> only error logs will be seen by this handler and thus log4j 75 * - even if the corresponding log4j log level is ALL.<br> 76 * On the other side, you should NOT set <code>.level = FINER or FINEST</code> if the log4j level is higher. 77 * In this case a lot of JUL log events would be generated, sent via this bridge to log4j and thrown away by the latter.<br> 78 * Note: JUL's default log level (i.e. none specified in logger.properties) is INFO. 79 * </ul> 80 * 81 * (Credits: idea and concept originate from org.slf4j.bridge.SLF4JBridgeHandler; 82 * level propagation idea originates from logback/LevelChangePropagator; 83 * but no source code has been copied) 84 */ 85 public class Log4jBridgeHandler extends java.util.logging.Handler implements PropertyChangeListener { 86 private static final org.apache.logging.log4j.Logger SLOGGER = StatusLogger.getLogger(); 87 88 // the caller of the logging is java.util.logging.Logger (for location info) 89 private static final String FQCN = java.util.logging.Logger.class.getName(); 90 private static final String UNKNOWN_LOGGER_NAME = "unknown.jul.logger"; 91 private static final java.util.logging.Formatter julFormatter = new java.util.logging.SimpleFormatter(); 92 93 private boolean doDebugOutput = false; 94 private String julSuffixToAppend = null; 95 //not needed: private boolean installAsLevelPropagator = false; 96 97 98 /** 99 * Adds a new Log4jBridgeHandler instance to JUL's root logger. 100 * This is a programmatic alternative to specify 101 * <code>handlers = org.apache.logging.log4j.jul.Log4jBridgeHandler</code> 102 * and its configuration in logging.properties.<br> 103 * @param removeHandlersForRootLogger true to remove all other installed handlers on JUL root level 104 */ 105 public static void install(boolean removeHandlersForRootLogger, String suffixToAppend, boolean propagateLevels) { 106 java.util.logging.Logger rootLogger = getJulRootLogger(); 107 if (removeHandlersForRootLogger) { 108 for (java.util.logging.Handler hdl : rootLogger.getHandlers()) { 109 rootLogger.removeHandler(hdl); 110 } 111 } 112 rootLogger.addHandler(new Log4jBridgeHandler(false, suffixToAppend, propagateLevels)); 113 // note: filter-level of Handler defaults to ALL, so nothing to do here 114 } 115 116 private static java.util.logging.Logger getJulRootLogger() { 117 return java.util.logging.LogManager.getLogManager().getLogger(""); 118 } 119 120 121 /** Initialize this handler by reading out JUL configuration. */ 122 public Log4jBridgeHandler() { 123 final java.util.logging.LogManager julLogMgr = java.util.logging.LogManager.getLogManager(); 124 final String className = this.getClass().getName(); 125 init(Boolean.parseBoolean(julLogMgr.getProperty(className + ".sysoutDebug")), 126 julLogMgr.getProperty(className + ".appendSuffix"), 127 Boolean.parseBoolean(julLogMgr.getProperty(className + ".propagateLevels")) ); 128 129 } 130 131 /** Initialize this handler with given configuration. */ 132 public Log4jBridgeHandler(boolean debugOutput, String suffixToAppend, boolean propagateLevels) { 133 init(debugOutput, suffixToAppend, propagateLevels); 134 } 135 136 137 /** Perform init. of this handler with given configuration (typical use is for constructor). */ 138 protected void init(boolean debugOutput, String suffixToAppend, boolean propagateLevels) { 139 this.doDebugOutput = debugOutput; 140 if (debugOutput) { 141 new Exception("DIAGNOSTIC ONLY (sysout): Log4jBridgeHandler instance created (" + this + ")") 142 .printStackTrace(System.out); // is no error thus no syserr 143 } 144 145 if (suffixToAppend != null) { 146 suffixToAppend = suffixToAppend.trim(); // remove spaces 147 if (suffixToAppend.isEmpty()) { 148 suffixToAppend = null; 149 } else if (suffixToAppend.charAt(0) != '.') { // always make it a sub-logger 150 suffixToAppend = '.' + suffixToAppend; 151 } 152 } 153 this.julSuffixToAppend = suffixToAppend; 154 155 //not needed: this.installAsLevelPropagator = propagateLevels; 156 if (propagateLevels) { 157 @SuppressWarnings("resource") // no need to close the AutoCloseable ctx here 158 LoggerContext context = LoggerContext.getContext(false); 159 context.addPropertyChangeListener(this); 160 propagateLogLevels(context.getConfiguration()); 161 // note: java.util.logging.LogManager.addPropertyChangeListener() could also 162 // be set here, but a call of JUL.readConfiguration() will be done on purpose 163 } 164 165 SLOGGER.debug("Log4jBridgeHandler init. with: suffix='{}', lvlProp={}, instance={}", 166 suffixToAppend, propagateLevels, this); 167 } 168 169 170 @Override 171 public void close() { 172 // cleanup and remove listener and JUL logger references 173 julLoggerRefs = null; 174 LoggerContext.getContext(false).removePropertyChangeListener(this); 175 if (doDebugOutput) { 176 System.out.println("sysout: Log4jBridgeHandler close(): " + this); 177 } 178 } 179 180 181 @Override 182 public void publish(LogRecord record) { 183 if (record == null) { // silently ignore null records 184 return; 185 } 186 187 org.apache.logging.log4j.Logger log4jLogger = getLog4jLogger(record); 188 String msg = julFormatter.formatMessage(record); // use JUL's implementation to get real msg 189 /* log4j allows nulls: 190 if (msg == null) { 191 // JUL allows nulls, but other log system may not 192 msg = "<null log msg>"; 193 } */ 194 org.apache.logging.log4j.Level log4jLevel = LevelTranslator.toLevel(record.getLevel()); 195 Throwable thrown = record.getThrown(); 196 if (log4jLogger instanceof ExtendedLogger) { 197 // relevant for location information 198 try { 199 ((ExtendedLogger) log4jLogger).logIfEnabled(FQCN, log4jLevel, null, msg, thrown); 200 } catch (NoClassDefFoundError e) { 201 // sometimes there are problems with log4j.ExtendedStackTraceElement, so try a workaround 202 log4jLogger.warn("Log4jBridgeHandler: ignored exception when calling 'ExtendedLogger': {}", e.toString()); 203 log4jLogger.log(log4jLevel, msg, thrown); 204 } 205 } else { 206 log4jLogger.log(log4jLevel, msg, thrown); 207 } 208 } 209 210 211 @Override 212 public void flush() { 213 // nothing to do 214 } 215 216 217 /** 218 * Return the log4j-Logger instance that will be used for logging. 219 * Handles null name case and appends configured suffix. 220 */ 221 private org.apache.logging.log4j.Logger getLog4jLogger(LogRecord record) { 222 String name = record.getLoggerName(); 223 if (name == null) { 224 name = UNKNOWN_LOGGER_NAME; 225 } else if (julSuffixToAppend != null) { 226 name += julSuffixToAppend; 227 } 228 return org.apache.logging.log4j.LogManager.getLogger(name); 229 } 230 231 232 ///// log level propagation code 233 234 235 @Override 236 // impl. for PropertyChangeListener 237 public void propertyChange(PropertyChangeEvent evt) { 238 SLOGGER.debug("Log4jBridgeHandler.propertyChange(): {}", evt); 239 if (LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName()) && evt.getNewValue() instanceof Configuration) { 240 propagateLogLevels((Configuration) evt.getNewValue()); 241 } 242 } 243 244 245 /** Save "hard" references to configured JUL loggers. (is lazy init.) */ 246 private Set<java.util.logging.Logger> julLoggerRefs; 247 /** Perform developer tests? (Should be unused/outcommented for real code) */ 248 //private static final boolean DEVTEST = false; 249 250 251 private void propagateLogLevels(Configuration config) { 252 SLOGGER.debug("Log4jBridgeHandler.propagateLogLevels(): {}", config); 253 // clear or init. saved JUL logger references 254 // JUL loggers have to be explicitly referenced because JUL internally uses 255 // weak references so not instantiated loggers may be garbage collected 256 // and their level config gets lost then. 257 if (julLoggerRefs == null) { 258 julLoggerRefs = new HashSet<>(); 259 } else { 260 julLoggerRefs.clear(); 261 } 262 263 //if (DEVTEST) debugPrintJulLoggers("Start of propagation"); 264 // walk through all log4j configured loggers and set JUL level accordingly 265 final Map<String, LoggerConfig> log4jLoggers = config.getLoggers(); 266 //java.util.List<String> outTxt = new java.util.ArrayList<>(); // DEVTEST / DEV-DEBUG ONLY 267 for (LoggerConfig lcfg : log4jLoggers.values()) { 268 java.util.logging.Logger julLog = java.util.logging.Logger.getLogger(lcfg.getName()); // this also fits for root = "" 269 java.util.logging.Level julLevel = LevelTranslator.toJavaLevel(lcfg.getLevel()); // lcfg.getLevel() never returns null 270 julLog.setLevel(julLevel); 271 julLoggerRefs.add(julLog); // save an explicit reference to prevent GC 272 //if (DEVTEST) outTxt.add("propagating '" + lcfg.getName() + "' / " + lcfg.getLevel() + " -> " + julLevel); 273 } // for 274 //if (DEVTEST) java.util.Collections.sort(outTxt, String.CASE_INSENSITIVE_ORDER); 275 //if (DEVTEST) for (String s : outTxt) System.out.println("+ " + s); 276 //if (DEVTEST) debugPrintJulLoggers("After propagation"); 277 278 // cleanup JUL: reset all log levels not explicitly given by log4j 279 // This has to happen after propagation because JUL creates and inits. the loggers lazily 280 // so a nested logger might be created during the propagation-for-loop above and gets 281 // its JUL-configured level not until then. 282 final java.util.logging.LogManager julMgr = java.util.logging.LogManager.getLogManager(); 283 for (Enumeration<String> en = julMgr.getLoggerNames(); en.hasMoreElements(); ) { 284 java.util.logging.Logger julLog = julMgr.getLogger(en.nextElement()); 285 if (julLog != null && julLog.getLevel() != null && !"".equals(julLog.getName()) 286 && !log4jLoggers.containsKey(julLog.getName()) ) { 287 julLog.setLevel(null); 288 } 289 } // for 290 //if (DEVTEST) debugPrintJulLoggers("After JUL cleanup"); 291 } 292 293 294 /* DEV-DEBUG ONLY (comment out for release) *xx/ 295 private void debugPrintJulLoggers(String infoStr) { 296 if (!DEVTEST) return; 297 java.util.logging.LogManager julMgr = java.util.logging.LogManager.getLogManager(); 298 System.out.println("sysout: " + infoStr + " - for " + julMgr); 299 java.util.List<String> txt = new java.util.ArrayList<>(); 300 int n = 1; 301 for (Enumeration<String> en = julMgr.getLoggerNames(); en.hasMoreElements(); ) { 302 String ln = en.nextElement(); 303 java.util.logging.Logger lg = julMgr.getLogger(ln); 304 if (lg == null) { 305 txt.add("(!null-Logger '" + ln + "') #" + n); 306 } else if (lg.getLevel() == null) { 307 txt.add("(null-Level Logger '" + ln + "') #" + n); 308 } else { 309 txt.add("Logger '" + ln + "', lvl = " + lg.getLevel() + " #" + n); 310 } 311 n++; 312 } // for 313 java.util.Collections.sort(txt, String.CASE_INSENSITIVE_ORDER); 314 for (String s : txt) { 315 System.out.println(" - " + s); 316 } 317 } /**/ 318 319 }