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.logging.log4j.jul;
018
019//note: NO import of Logger, Level, LogManager to prevent conflicts JUL/log4j
020import java.beans.PropertyChangeEvent;
021import java.beans.PropertyChangeListener;
022import java.util.Enumeration;
023import java.util.HashSet;
024import java.util.Map;
025import java.util.Set;
026import java.util.logging.LogRecord;
027
028import org.apache.logging.log4j.core.LoggerContext;
029import org.apache.logging.log4j.core.config.Configuration;
030import org.apache.logging.log4j.core.config.LoggerConfig;
031import org.apache.logging.log4j.spi.ExtendedLogger;
032import org.apache.logging.log4j.status.StatusLogger;
033
034
035/**
036 * Bridge from JUL to log4j2.<br>
037 * This is an alternative to log4j.jul.LogManager (running as complete JUL replacement),
038 * especially useful for webapps running on a container for which the LogManager cannot or
039 * should not be used.<br><br>
040 *
041 * Installation/usage:<ul>
042 * <li> Declaratively inside JUL's <code>logging.properties</code>:<br>
043 *    <code>handlers = org.apache.logging.log4j.jul.Log4jBridgeHandler</code><br>
044 *    (and typically also:   <code>org.apache.logging.log4j.jul.Log4jBridgeHandler.propagateLevels = true</code> )<br>
045 *    Note: in a webapp running on Tomcat, you may create a <code>WEB-INF/classes/logging.properties</code>
046 *    file to configure JUL for this webapp only: configured handlers and log levels affect your webapp only!
047 *    This file is then the <i>complete</i> JUL configuration, so JUL's defaults (e.g. log level INFO) apply
048 *    for stuff not explicitly defined therein.
049 * <li> Programmatically by calling <code>install()</code> method,
050 *    e.g. inside ServletContextListener static-class-init. or contextInitialized()
051 * </ul>
052 * Configuration (in JUL's <code>logging.properties</code>):<ul>
053 * <li> Log4jBridgeHandler.<code>suffixToAppend</code><br>
054 *        String, suffix to append to JUL logger names, to easily recognize bridged log messages.
055 *        A dot "." is automatically prepended, so configuration for the basis logger is used<br>
056 *        Example:  <code>Log4jBridgeHandler.suffixToAppend = _JUL</code><br>
057 *        Useful, for example, if you use JSF because it logs exceptions and throws them afterwards;
058 *        you can easily recognize the duplicates with this (or concentrate on the non-JUL-logs).
059 * <li> Log4jBridgeHandler.<code>propagateLevels</code>   boolean, "true" to automatically propagate log4j log levels to JUL.
060 * <li> Log4jBridgeHandler.<code>sysoutDebug</code>   boolean, perform some (developer) debug output to sysout
061 * </ul>
062 *
063 * Log levels are translated with {@link LevelTranslator}, see also
064 * <a href="https://logging.apache.org/log4j/2.x/log4j-jul/index.html#Default_Level_Conversions">log4j doc</a>.<br><br>
065 *
066 * Restrictions:<ul>
067 * <li> Manually given source/location info in JUL (e.g. entering(), exiting(), throwing(), logp(), logrb() )
068 *    will NOT be considered, i.e. gets lost in log4j logging.
069 * <li> Log levels of JUL have to be adjusted according to log4j log levels:
070 *      Either by using "propagateLevels" (preferred), or manually by specifying them explicitly,
071 *      i.e. logging.properties and log4j2.xml have some redundancies.
072 * <li> Only JUL log events that are allowed according to the JUL log level get to this handler and thus to log4j.
073 *      This is only relevant and important if you NOT use "propagateLevels".
074 *      If you set <code>.level = SEVERE</code> only error logs will be seen by this handler and thus log4j
075 *      - even if the corresponding log4j log level is ALL.<br>
076 *      On the other side, you should NOT set <code>.level = FINER  or  FINEST</code> if the log4j level is higher.
077 *      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>
078 *      Note: JUL's default log level (i.e. none specified in logger.properties) is INFO.
079 * </ul>
080 *
081 * (Credits: idea and concept originate from org.slf4j.bridge.SLF4JBridgeHandler;
082 *   level propagation idea originates from logback/LevelChangePropagator;
083 *   but no source code has been copied)
084 */
085public class Log4jBridgeHandler extends java.util.logging.Handler implements PropertyChangeListener {
086    private static final org.apache.logging.log4j.Logger SLOGGER = StatusLogger.getLogger();
087
088    // the caller of the logging is java.util.logging.Logger (for location info)
089    private static final String FQCN = java.util.logging.Logger.class.getName();
090    private static final String UNKNOWN_LOGGER_NAME = "unknown.jul.logger";
091    private static final java.util.logging.Formatter julFormatter = new java.util.logging.SimpleFormatter();
092
093    private boolean doDebugOutput = false;
094    private String julSuffixToAppend = null;
095    //not needed:  private boolean installAsLevelPropagator = false;
096
097
098    /**
099     * 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}