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.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 }