/* * Copyright (c) 2012-2014 Spotify AB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.spotify.logging; import static ch.qos.logback.classic.Level.OFF; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.System.getenv; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.filter.ThresholdFilter; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; import com.getsentry.raven.logback.SentryAppender; import com.google.common.base.Charsets; import com.spotify.logging.logback.MillisecondPrecisionSyslogAppender; import java.io.File; import java.lang.management.ManagementFactory; import org.slf4j.LoggerFactory; /** * Base configurator of logback for spotify services/tools. LoggingConfigurator.configureDefaults() * should generally be called as soon as possible on start-up. The configured logging backend is * logback. If the SPOTIFY_SYSLOG_HOST or SPOTIFY_SYSLOG_PORT environment variable is defined, * configureDefaults() will use the syslog appender, otherwise it will use the console appender. * * <p> One aspect of the logging is that we setup a general uncaught exception handler to log * uncaught exceptions at info level, if syslog is chosen as logging backend. * * @see JewelCliLoggingConfigurator For some integration with JewelCLI */ public class LoggingConfigurator { public static final String DEFAULT_IDENT = "java"; public static final String SPOTIFY_HOSTNAME = "SPOTIFY_HOSTNAME"; public static final String SPOTIFY_SYSLOG_HOST = "SPOTIFY_SYSLOG_HOST"; public static final String SPOTIFY_SYSLOG_PORT = "SPOTIFY_SYSLOG_PORT"; public enum Level { OFF(ch.qos.logback.classic.Level.OFF), ERROR(ch.qos.logback.classic.Level.ERROR), WARN(ch.qos.logback.classic.Level.WARN), INFO(ch.qos.logback.classic.Level.INFO), DEBUG(ch.qos.logback.classic.Level.DEBUG), TRACE(ch.qos.logback.classic.Level.TRACE), ALL(ch.qos.logback.classic.Level.ALL); final ch.qos.logback.classic.Level logbackLevel; Level(ch.qos.logback.classic.Level logbackLevel) { this.logbackLevel = logbackLevel; } } /** * Mute all logging. */ public static void configureNoLogging() { final Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); final LoggerContext context = rootLogger.getLoggerContext(); // Clear context, removing all appenders context.reset(); // Set logging level to OFF for (final Logger logger : context.getLoggerList()) { if (logger != rootLogger) { logger.setLevel(null); } } rootLogger.setLevel(OFF); } /** * Configure logging with default behaviour and log to stderr. Uses the {@link #DEFAULT_IDENT} * logging identity. Uses INFO logging level. If the SPOTIFY_SYSLOG_HOST or SPOTIFY_SYSLOG_PORT * environment variable is defined, the syslog appender will be used, otherwise console appender * will be. */ public static void configureDefaults() { configureDefaults(DEFAULT_IDENT); } /** * Configure logging with default behaviour and log to stderr using INFO logging level. If the * SPOTIFY_SYSLOG_HOST or SPOTIFY_SYSLOG_PORT environment variable is defined, the syslog * appender will be used, otherwise console appender will be. * * @param ident The logging identity. */ public static void configureDefaults(final String ident) { configureDefaults(ident, Level.INFO); } /** * Configure logging with default behaviour and log to stderr. If the SPOTIFY_SYSLOG_HOST or * SPOTIFY_SYSLOG_PORT environment variable is defined, the syslog appender will be used, * otherwise console appender will be. * * @param ident The logging identity. * @param level logging level to use. */ public static void configureDefaults(final String ident, final Level level) { // Call configureSyslogDefaults if the SPOTIFY_SYSLOG_HOST or SPOTIFY_SYSLOG_PORT env var is // set. If this causes a problem, we could introduce a configureConsoleDefaults method which // users could call instead to avoid this behavior. final String syslogHost = getSyslogHost(); final int syslogPort = getSyslogPort(); if (syslogHost != null || syslogPort != -1) { configureSyslogDefaults(ident, level, syslogHost, syslogPort); return; } final Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // Setup context final LoggerContext context = setupLoggerContext(rootLogger, ident); // Setup stderr output rootLogger.addAppender(getStdErrAppender(context)); // Setup logging level rootLogger.setLevel(level.logbackLevel); // Log uncaught exceptions UncaughtExceptionLogger.setDefaultUncaughtExceptionHandler(); } /** * Configure logging with default behavior and log to syslog using INFO logging level. * * @param ident Syslog ident to use. */ public static void configureSyslogDefaults(final String ident) { configureSyslogDefaults(ident, Level.INFO); } /** * Configure logging with default behavior and log to syslog. * * @param ident Syslog ident to use. * @param level logging level to use. */ public static void configureSyslogDefaults(final String ident, final Level level) { final String syslogHost = getenv(SPOTIFY_SYSLOG_HOST); final String port = getenv(SPOTIFY_SYSLOG_PORT); final int syslogPort = port == null ? -1 : Integer.valueOf(port); configureSyslogDefaults(ident, level, syslogHost, syslogPort); } /** * Configure logging with default behavior and log to syslog. * * @param ident Syslog ident to use. * @param level logging level to use. * @param host Hostname or IP address of syslog host. * @param port Port to connect to syslog on. */ public static void configureSyslogDefaults(final String ident, final Level level, final String host, final int port) { configureSyslogDefaults(ident, level, host, port, Logger.ROOT_LOGGER_NAME); } /** * Configure logging with default behavior and log to syslog. * * @param ident Syslog ident to use. * @param level logging level to use. * @param host Hostname or IP address of syslog host. * @param port Port to connect to syslog on. * @param loggerName Name of the logger to which the syslog appender will be added */ public static void configureSyslogDefaults(final String ident, final Level level, final String host, final int port, final String loggerName) { final Logger logger = (Logger) LoggerFactory.getLogger(loggerName); // Setup context final LoggerContext context = setupLoggerContext(logger, ident); // Setup syslog output logger.addAppender(getSyslogAppender(context, host, port)); // Setup logging level logger.setLevel(level.logbackLevel); // Log uncaught exceptions UncaughtExceptionLogger.setDefaultUncaughtExceptionHandler(); } /** * Add a sentry appender for error log event. * @param dsn the sentry dsn to use (as produced by the sentry webinterface). * @return the configured sentry appender. */ public static SentryAppender addSentryAppender(final String dsn) { return addSentryAppender(dsn, Level.ERROR); } /** * Add a sentry appender. * @param dsn the sentry dsn to use (as produced by the sentry webinterface). * @param logLevelThreshold the threshold for log events to be sent to sentry. * @return the configured sentry appender. */ public static SentryAppender addSentryAppender(final String dsn, Level logLevelThreshold) { final Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); final LoggerContext context = rootLogger.getLoggerContext(); SentryAppender appender = new SentryAppender(); appender.setDsn(dsn); appender.setContext(context); ThresholdFilter levelFilter = new ThresholdFilter(); levelFilter.setLevel(logLevelThreshold.logbackLevel.toString()); levelFilter.start(); appender.addFilter(levelFilter); appender.start(); rootLogger.addAppender(appender); return appender; } /** * Create a stderr appender. * * @param context The logger context to use. * @return An appender writing to stderr. */ private static Appender<ILoggingEvent> getStdErrAppender(final LoggerContext context) { // Setup format final PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setContext(context); encoder.setPattern( "%date{HH:mm:ss.SSS} %property{ident}[%property{pid}]: %-5level [%thread] %logger{0}: %msg%n"); encoder.setCharset(Charsets.UTF_8); encoder.start(); // Setup stderr appender final ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<ILoggingEvent>(); appender.setTarget("System.err"); appender.setName("stderr"); appender.setEncoder(encoder); appender.setContext(context); appender.start(); return appender; } /** * Create a syslog appender. The appender will use the facility local0. If host is null or an * empty string, default to "localhost". If port is less than 0, default to 514. * * @param context The logger context to use. * @param host The host running the syslog daemon. * @param port The port to connect to. * @return An appender that writes to syslog. */ static Appender<ILoggingEvent> getSyslogAppender(final LoggerContext context, final String host, final int port) { final String h = isNullOrEmpty(host) ? "localhost" : host; final int p = port < 0 ? 514 : port; final MillisecondPrecisionSyslogAppender appender = new MillisecondPrecisionSyslogAppender(); appender.setFacility("LOCAL0"); appender.setSyslogHost(h); appender.setPort(p); appender.setName("syslog"); appender.setCharset(Charsets.UTF_8); appender.setContext(context); appender.setSuffixPattern( "%property{ident}[%property{pid}]: %msg"); appender.setStackTracePattern( "%property{ident}[%property{pid}]: " + CoreConstants.TAB); appender.start(); return appender; } /** * This is not a public interface and only here to be called from JewelCliLoggingConfigurator. All * JewelCli specific functionality should be moved to JewelCliLoggingConfigurator, but that * requires some work in defining an interface for *it* to use to configure this cmdline/config * format agnostic class. * * The implementation was moved here from JewelCliLoggingConfigurator as part of creating this * class for use in a non_jewelCli (argot, scala) project and so essentially constitutes an * improvement rather than a regression, but at some point the work should be put in to define a * nice programatical interface to configure spotify logging options. * * @deprecated Don't use, see docs. */ static void configure(final JewelCliLoggingOptions opts) { // Use logback config file to setup logging if specified, discarding any other logging options. if (!opts.logFileName().isEmpty()) { configure(new File(opts.logFileName()), opts.ident()); return; } final Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // Log uncaught exceptions UncaughtExceptionLogger.setDefaultUncaughtExceptionHandler(); // Setup context final LoggerContext context = setupLoggerContext(rootLogger, opts.ident()); // See if syslog host was specified via command line or environment variable. // The command line value takes precedence, which defaults to an empty string. String syslogHost = opts.syslogHost(); if (isNullOrEmpty(syslogHost)) { syslogHost = getSyslogHost(); } // See if syslog port was specified via command line or environment variable. // The command line value takes precedence, which defaults to -1. int syslogPort = opts.syslogPort(); if (syslogPort < 0) { syslogPort = getSyslogPort(); } // Setup syslog logging if (opts.syslog() || syslogHost != null || syslogPort > 0) { rootLogger.addAppender(getSyslogAppender(context, syslogHost, syslogPort)); } else { rootLogger.addAppender(getStdErrAppender(context)); } // Setup default logging level rootLogger.setLevel(Level.INFO.logbackLevel); // Setup logging levels if (opts.error()) { rootLogger.setLevel(Level.ERROR.logbackLevel); } if (opts.warn()) { rootLogger.setLevel(Level.WARN.logbackLevel); } if (opts.info()) { rootLogger.setLevel(Level.INFO.logbackLevel); } if (opts.debug()) { rootLogger.setLevel(Level.DEBUG.logbackLevel); } if (opts.trace()) { rootLogger.setLevel(Level.TRACE.logbackLevel); } } /** * Configure logging using a logback configuration file. * * @param file A logback configuration file. */ public static void configure(final File file) { configure(file, DEFAULT_IDENT); } /** * Configure logging using a logback configuration file. * * @param file A logback configuration file. * @param defaultIdent Fallback logging identity, used if not specified in config file. */ public static void configure(final File file, final String defaultIdent) { final Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // Setup context final LoggerContext context = rootLogger.getLoggerContext(); context.reset(); // Log uncaught exceptions UncaughtExceptionLogger.setDefaultUncaughtExceptionHandler(); // Load logging configuration from file try { final JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(file); } catch (JoranException je) { // StatusPrinter will handle this } context.putProperty("pid", getMyPid()); final String hostname = getSpotifyHostname(); if (hostname != null) { context.putProperty("hostname", hostname); } final String ident = context.getProperty("ident"); if (ident == null) { context.putProperty("ident", defaultIdent); } StatusPrinter.printInCaseOfErrorsOrWarnings(context); } private static LoggerContext setupLoggerContext(Logger rootLogger, String ident) { final LoggerContext context = rootLogger.getLoggerContext(); context.reset(); context.putProperty("ident", ident); context.putProperty("pid", getMyPid()); context.putProperty("hostname", getSpotifyHostname()); return context; } // TODO (bjorn): We probably want to move this to the utilities project. // Also, the portability of this function is not guaranteed. private static String getMyPid() { String pid = "0"; try { final String nameStr = ManagementFactory.getRuntimeMXBean().getName(); // XXX (bjorn): Really stupid parsing assuming that nameStr will be of the form // "pid@hostname", which is probably not guaranteed. pid = nameStr.split("@")[0]; } catch (RuntimeException e) { // Fall through. } return pid; } private static String getSyslogHost() { return emptyToNull(getenv(SPOTIFY_SYSLOG_HOST)); } private static int getSyslogPort() { final String port = getenv(SPOTIFY_SYSLOG_PORT); return isNullOrEmpty(port) ? -1 : Integer.valueOf(port); } private static String getSpotifyHostname() { return emptyToNull(getenv(SPOTIFY_HOSTNAME)); } }