001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.logging.logback;
018
019import java.net.URL;
020import java.security.CodeSource;
021import java.security.ProtectionDomain;
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Set;
025import java.util.logging.Handler;
026import java.util.logging.LogManager;
027
028import ch.qos.logback.classic.Level;
029import ch.qos.logback.classic.LoggerContext;
030import ch.qos.logback.classic.joran.JoranConfigurator;
031import ch.qos.logback.classic.jul.LevelChangePropagator;
032import ch.qos.logback.classic.turbo.TurboFilter;
033import ch.qos.logback.classic.util.ContextInitializer;
034import ch.qos.logback.core.joran.spi.JoranException;
035import ch.qos.logback.core.spi.FilterReply;
036import ch.qos.logback.core.status.Status;
037import org.slf4j.ILoggerFactory;
038import org.slf4j.Logger;
039import org.slf4j.Marker;
040import org.slf4j.bridge.SLF4JBridgeHandler;
041import org.slf4j.impl.StaticLoggerBinder;
042
043import org.springframework.boot.logging.LogFile;
044import org.springframework.boot.logging.LogLevel;
045import org.springframework.boot.logging.LoggerConfiguration;
046import org.springframework.boot.logging.LoggingInitializationContext;
047import org.springframework.boot.logging.LoggingSystem;
048import org.springframework.boot.logging.LoggingSystemProperties;
049import org.springframework.boot.logging.Slf4JLoggingSystem;
050import org.springframework.core.env.Environment;
051import org.springframework.util.Assert;
052import org.springframework.util.ResourceUtils;
053import org.springframework.util.StringUtils;
054
055/**
056 * {@link LoggingSystem} for <a href="http://logback.qos.ch">logback</a>.
057 *
058 * @author Phillip Webb
059 * @author Dave Syer
060 * @author Andy Wilkinson
061 * @author Ben Hale
062 */
063public class LogbackLoggingSystem extends Slf4JLoggingSystem {
064
065        private static final String CONFIGURATION_FILE_PROPERTY = "logback.configurationFile";
066
067        private static final LogLevels<Level> LEVELS = new LogLevels<>();
068
069        static {
070                LEVELS.map(LogLevel.TRACE, Level.TRACE);
071                LEVELS.map(LogLevel.TRACE, Level.ALL);
072                LEVELS.map(LogLevel.DEBUG, Level.DEBUG);
073                LEVELS.map(LogLevel.INFO, Level.INFO);
074                LEVELS.map(LogLevel.WARN, Level.WARN);
075                LEVELS.map(LogLevel.ERROR, Level.ERROR);
076                LEVELS.map(LogLevel.FATAL, Level.ERROR);
077                LEVELS.map(LogLevel.OFF, Level.OFF);
078        }
079
080        private static final TurboFilter FILTER = new TurboFilter() {
081
082                @Override
083                public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger,
084                                Level level, String format, Object[] params, Throwable t) {
085                        return FilterReply.DENY;
086                }
087
088        };
089
090        public LogbackLoggingSystem(ClassLoader classLoader) {
091                super(classLoader);
092        }
093
094        @Override
095        protected String[] getStandardConfigLocations() {
096                return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
097                                "logback.xml" };
098        }
099
100        @Override
101        public void beforeInitialize() {
102                LoggerContext loggerContext = getLoggerContext();
103                if (isAlreadyInitialized(loggerContext)) {
104                        return;
105                }
106                super.beforeInitialize();
107                loggerContext.getTurboFilterList().add(FILTER);
108        }
109
110        @Override
111        public void initialize(LoggingInitializationContext initializationContext,
112                        String configLocation, LogFile logFile) {
113                LoggerContext loggerContext = getLoggerContext();
114                if (isAlreadyInitialized(loggerContext)) {
115                        return;
116                }
117                super.initialize(initializationContext, configLocation, logFile);
118                loggerContext.getTurboFilterList().remove(FILTER);
119                markAsInitialized(loggerContext);
120                if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
121                        getLogger(LogbackLoggingSystem.class.getName()).warn(
122                                        "Ignoring '" + CONFIGURATION_FILE_PROPERTY + "' system property. "
123                                                        + "Please use 'logging.config' instead.");
124                }
125        }
126
127        @Override
128        protected void loadDefaults(LoggingInitializationContext initializationContext,
129                        LogFile logFile) {
130                LoggerContext context = getLoggerContext();
131                stopAndReset(context);
132                LogbackConfigurator configurator = new LogbackConfigurator(context);
133                Environment environment = initializationContext.getEnvironment();
134                context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
135                                environment.resolvePlaceholders(
136                                                "${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
137                context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN,
138                                environment.resolvePlaceholders(
139                                                "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
140                new DefaultLogbackConfiguration(initializationContext, logFile)
141                                .apply(configurator);
142                context.setPackagingDataEnabled(true);
143        }
144
145        @Override
146        protected void loadConfiguration(LoggingInitializationContext initializationContext,
147                        String location, LogFile logFile) {
148                super.loadConfiguration(initializationContext, location, logFile);
149                LoggerContext loggerContext = getLoggerContext();
150                stopAndReset(loggerContext);
151                try {
152                        configureByResourceUrl(initializationContext, loggerContext,
153                                        ResourceUtils.getURL(location));
154                }
155                catch (Exception ex) {
156                        throw new IllegalStateException(
157                                        "Could not initialize Logback logging from " + location, ex);
158                }
159                List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
160                StringBuilder errors = new StringBuilder();
161                for (Status status : statuses) {
162                        if (status.getLevel() == Status.ERROR) {
163                                errors.append((errors.length() > 0) ? String.format("%n") : "");
164                                errors.append(status.toString());
165                        }
166                }
167                if (errors.length() > 0) {
168                        throw new IllegalStateException(
169                                        String.format("Logback configuration error detected: %n%s", errors));
170                }
171        }
172
173        private void configureByResourceUrl(
174                        LoggingInitializationContext initializationContext,
175                        LoggerContext loggerContext, URL url) throws JoranException {
176                if (url.toString().endsWith("xml")) {
177                        JoranConfigurator configurator = new SpringBootJoranConfigurator(
178                                        initializationContext);
179                        configurator.setContext(loggerContext);
180                        configurator.doConfigure(url);
181                }
182                else {
183                        new ContextInitializer(loggerContext).configureByResource(url);
184                }
185        }
186
187        private void stopAndReset(LoggerContext loggerContext) {
188                loggerContext.stop();
189                loggerContext.reset();
190                if (isBridgeHandlerInstalled()) {
191                        addLevelChangePropagator(loggerContext);
192                }
193        }
194
195        private boolean isBridgeHandlerInstalled() {
196                if (!isBridgeHandlerAvailable()) {
197                        return false;
198                }
199                java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
200                Handler[] handlers = rootLogger.getHandlers();
201                return handlers.length == 1 && SLF4JBridgeHandler.class.isInstance(handlers[0]);
202        }
203
204        private void addLevelChangePropagator(LoggerContext loggerContext) {
205                LevelChangePropagator levelChangePropagator = new LevelChangePropagator();
206                levelChangePropagator.setResetJUL(true);
207                levelChangePropagator.setContext(loggerContext);
208                loggerContext.addListener(levelChangePropagator);
209        }
210
211        @Override
212        public void cleanUp() {
213                LoggerContext context = getLoggerContext();
214                markAsUninitialized(context);
215                super.cleanUp();
216                context.getStatusManager().clear();
217                context.getTurboFilterList().remove(FILTER);
218        }
219
220        @Override
221        protected void reinitialize(LoggingInitializationContext initializationContext) {
222                getLoggerContext().reset();
223                getLoggerContext().getStatusManager().clear();
224                loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
225        }
226
227        @Override
228        public List<LoggerConfiguration> getLoggerConfigurations() {
229                List<LoggerConfiguration> result = new ArrayList<>();
230                for (ch.qos.logback.classic.Logger logger : getLoggerContext().getLoggerList()) {
231                        result.add(getLoggerConfiguration(logger));
232                }
233                result.sort(CONFIGURATION_COMPARATOR);
234                return result;
235        }
236
237        @Override
238        public LoggerConfiguration getLoggerConfiguration(String loggerName) {
239                return getLoggerConfiguration(getLogger(loggerName));
240        }
241
242        private LoggerConfiguration getLoggerConfiguration(
243                        ch.qos.logback.classic.Logger logger) {
244                if (logger == null) {
245                        return null;
246                }
247                LogLevel level = LEVELS.convertNativeToSystem(logger.getLevel());
248                LogLevel effectiveLevel = LEVELS
249                                .convertNativeToSystem(logger.getEffectiveLevel());
250                String name = logger.getName();
251                if (!StringUtils.hasLength(name) || Logger.ROOT_LOGGER_NAME.equals(name)) {
252                        name = ROOT_LOGGER_NAME;
253                }
254                return new LoggerConfiguration(name, level, effectiveLevel);
255        }
256
257        @Override
258        public Set<LogLevel> getSupportedLogLevels() {
259                return LEVELS.getSupported();
260        }
261
262        @Override
263        public void setLogLevel(String loggerName, LogLevel level) {
264                ch.qos.logback.classic.Logger logger = getLogger(loggerName);
265                if (logger != null) {
266                        logger.setLevel(LEVELS.convertSystemToNative(level));
267                }
268        }
269
270        @Override
271        public Runnable getShutdownHandler() {
272                return new ShutdownHandler();
273        }
274
275        private ch.qos.logback.classic.Logger getLogger(String name) {
276                LoggerContext factory = getLoggerContext();
277                if (StringUtils.isEmpty(name) || ROOT_LOGGER_NAME.equals(name)) {
278                        name = Logger.ROOT_LOGGER_NAME;
279                }
280                return factory.getLogger(name);
281
282        }
283
284        private LoggerContext getLoggerContext() {
285                ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
286                Assert.isInstanceOf(LoggerContext.class, factory,
287                                String.format(
288                                                "LoggerFactory is not a Logback LoggerContext but Logback is on "
289                                                                + "the classpath. Either remove Logback or the competing "
290                                                                + "implementation (%s loaded from %s). If you are using "
291                                                                + "WebLogic you will need to add 'org.slf4j' to "
292                                                                + "prefer-application-packages in WEB-INF/weblogic.xml",
293                                                factory.getClass(), getLocation(factory)));
294                return (LoggerContext) factory;
295        }
296
297        private Object getLocation(ILoggerFactory factory) {
298                try {
299                        ProtectionDomain protectionDomain = factory.getClass().getProtectionDomain();
300                        CodeSource codeSource = protectionDomain.getCodeSource();
301                        if (codeSource != null) {
302                                return codeSource.getLocation();
303                        }
304                }
305                catch (SecurityException ex) {
306                        // Unable to determine location
307                }
308                return "unknown location";
309        }
310
311        private boolean isAlreadyInitialized(LoggerContext loggerContext) {
312                return loggerContext.getObject(LoggingSystem.class.getName()) != null;
313        }
314
315        private void markAsInitialized(LoggerContext loggerContext) {
316                loggerContext.putObject(LoggingSystem.class.getName(), new Object());
317        }
318
319        private void markAsUninitialized(LoggerContext loggerContext) {
320                loggerContext.removeObject(LoggingSystem.class.getName());
321        }
322
323        private final class ShutdownHandler implements Runnable {
324
325                @Override
326                public void run() {
327                        getLoggerContext().stop();
328                }
329
330        }
331
332}