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 }