package io.dropwizard.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.jmx.JMXConfigurator;
import ch.qos.logback.classic.jul.LevelChangePropagator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AsyncAppenderBase;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.util.StatusPrinter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.logback.InstrumentedAppender;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.dropwizard.jackson.Jackson;
import io.dropwizard.logback.AsyncAppenderBaseProxy;
import io.dropwizard.logging.async.AsyncAppenderFactory;
import io.dropwizard.logging.async.AsyncLoggingEventAppenderFactory;
import io.dropwizard.logging.filter.LevelFilterFactory;
import io.dropwizard.logging.filter.ThresholdLevelFilterFactory;
import io.dropwizard.logging.layout.DropwizardLayoutFactory;
import io.dropwizard.logging.layout.LayoutFactory;
import io.dropwizard.util.Lists;
import javax.annotation.Nullable;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.locks.ReentrantLock;
import static java.util.Objects.requireNonNull;
@JsonTypeName("default")
public class DefaultLoggingFactory implements LoggingFactory {
private static final ReentrantLock MBEAN_REGISTRATION_LOCK = new ReentrantLock();
private static final ReentrantLock CHANGE_LOGGER_CONTEXT_LOCK = new ReentrantLock();
@NotNull
private String level = "INFO";
@NotNull
private Map<String, JsonNode> loggers = Collections.emptyMap();
@Valid
@NotNull
private List<AppenderFactory<ILoggingEvent>> appenders = Collections.singletonList(new ConsoleAppenderFactory<>());
@JsonIgnore
private final LoggerContext loggerContext;
@JsonIgnore
private final PrintStream ;
@JsonIgnore
@Nullable
private volatile String loggerName;
public DefaultLoggingFactory() {
this(LoggingUtil.getLoggerContext(), System.err);
}
DefaultLoggingFactory(LoggerContext loggerContext, PrintStream configurationErrorsStream) {
super();
this.loggerName = null;
this.loggerContext = requireNonNull(loggerContext);
this.configurationErrorsStream = requireNonNull(configurationErrorsStream);
}
LoggerContext getLoggerContext() {
return loggerContext;
}
PrintStream () {
return configurationErrorsStream;
}
void clear() {
final String name = loggerName;
if (name != null) {
CHANGE_LOGGER_CONTEXT_LOCK.lock();
try {
loggerContext.stop();
final Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logger.detachAndStopAllAppenders();
configureLoggers(name);
} finally {
CHANGE_LOGGER_CONTEXT_LOCK.unlock();
}
StatusPrinter.setPrintStream(System.out);
}
}
@JsonProperty
public String getLevel() {
return level;
}
@JsonProperty
public void setLevel(String level) {
this.level = level;
}
@JsonProperty
public Map<String, JsonNode> getLoggers() {
return loggers;
}
@JsonProperty
public void setLoggers(Map<String, JsonNode> loggers) {
this.loggers = new HashMap<>(loggers);
}
@JsonProperty
public List<AppenderFactory<ILoggingEvent>> getAppenders() {
return appenders;
}
@JsonProperty
public void setAppenders(List<AppenderFactory<ILoggingEvent>> appenders) {
this.appenders = new ArrayList<>(appenders);
}
@Override
public void configure(MetricRegistry metricRegistry, String name) {
LoggingUtil.hijackJDKLogging();
CHANGE_LOGGER_CONTEXT_LOCK.lock();
final Logger root;
try {
root = configureLoggers(name);
} finally {
CHANGE_LOGGER_CONTEXT_LOCK.unlock();
}
loggerName = name;
final LevelFilterFactory<ILoggingEvent> levelFilterFactory = new ThresholdLevelFilterFactory();
final AsyncAppenderFactory<ILoggingEvent> asyncAppenderFactory = new AsyncLoggingEventAppenderFactory();
final LayoutFactory<ILoggingEvent> layoutFactory = new DropwizardLayoutFactory();
for (AppenderFactory<ILoggingEvent> output : appenders) {
root.addAppender(output.build(loggerContext, name, layoutFactory, levelFilterFactory, asyncAppenderFactory));
}
StatusPrinter.setPrintStream(configurationErrorsStream);
try {
StatusPrinter.printIfErrorsOccured(loggerContext);
} finally {
StatusPrinter.setPrintStream(System.out);
}
final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
MBEAN_REGISTRATION_LOCK.lock();
try {
final ObjectName objectName = new ObjectName("io.dropwizard:type=Logging");
if (!server.isRegistered(objectName)) {
server.registerMBean(new JMXConfigurator(loggerContext,
server,
objectName),
objectName);
}
} catch (MalformedObjectNameException | InstanceAlreadyExistsException |
NotCompliantMBeanException | MBeanRegistrationException e) {
throw new RuntimeException(e);
} finally {
MBEAN_REGISTRATION_LOCK.unlock();
}
configureInstrumentation(root, metricRegistry);
}
@Override
public void stop() {
CHANGE_LOGGER_CONTEXT_LOCK.lock();
try {
final Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
final List<Appender<ILoggingEvent>> appenders = Lists.of(logger.iteratorForAppenders());
for (Appender<ILoggingEvent> appender : appenders) {
if (appender instanceof AsyncAppenderBase) {
flushAppender((AsyncAppenderBase<?>) appender);
} else if (appender instanceof AsyncAppenderBaseProxy) {
flushAppender(((AsyncAppenderBaseProxy<?>) appender).getAppender());
}
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} finally {
CHANGE_LOGGER_CONTEXT_LOCK.unlock();
}
}
@Override
public void reset() {
CHANGE_LOGGER_CONTEXT_LOCK.lock();
try {
loggerContext.stop();
final Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logger.detachAndStopAllAppenders();
final DropwizardLayout formatter = new DropwizardLayout(loggerContext, TimeZone.getDefault());
formatter.start();
final LayoutWrappingEncoder<ILoggingEvent> layoutEncoder = new LayoutWrappingEncoder<>();
layoutEncoder.setLayout(formatter);
final ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
consoleAppender.addFilter(new ThresholdLevelFilterFactory().build(Level.INFO));
consoleAppender.setEncoder(layoutEncoder);
consoleAppender.setContext(loggerContext);
consoleAppender.start();
logger.addAppender(consoleAppender);
loggerContext.start();
} finally {
CHANGE_LOGGER_CONTEXT_LOCK.unlock();
}
}
private void flushAppender(AsyncAppenderBase<?> appender) throws InterruptedException {
int timeWaiting = 0;
while (timeWaiting < appender.getMaxFlushTime() && appender.getNumberOfElementsInQueue() > 0) {
Thread.sleep(100);
timeWaiting += 100;
}
if (appender.getNumberOfElementsInQueue() > 0) {
appender.addWarn(appender.getNumberOfElementsInQueue() + " events may be discarded");
}
}
private void configureInstrumentation(Logger root, MetricRegistry metricRegistry) {
final InstrumentedAppender appender = new InstrumentedAppender(metricRegistry);
appender.setContext(loggerContext);
appender.start();
root.addAppender(appender);
}
private Logger configureLoggers(String name) {
final Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
loggerContext.reset();
final LevelChangePropagator propagator = new LevelChangePropagator();
propagator.setContext(loggerContext);
propagator.setResetJUL(true);
loggerContext.addListener(propagator);
root.setLevel(toLevel(level));
final LevelFilterFactory<ILoggingEvent> levelFilterFactory = new ThresholdLevelFilterFactory();
final AsyncAppenderFactory<ILoggingEvent> asyncAppenderFactory = new AsyncLoggingEventAppenderFactory();
final LayoutFactory<ILoggingEvent> layoutFactory = new DropwizardLayoutFactory();
for (Map.Entry<String, JsonNode> entry : loggers.entrySet()) {
final Logger logger = loggerContext.getLogger(entry.getKey());
final JsonNode jsonNode = entry.getValue();
if (jsonNode.isTextual() || jsonNode.isBoolean()) {
logger.setLevel(toLevel(jsonNode.asText()));
} else if (jsonNode.isObject()) {
final LoggerConfiguration configuration;
try {
configuration = Jackson.newObjectMapper().treeToValue(jsonNode, LoggerConfiguration.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Wrong format of logger '" + entry.getKey() + "'", e);
}
logger.setLevel(toLevel(configuration.getLevel()));
logger.setAdditive(configuration.isAdditive());
for (AppenderFactory<ILoggingEvent> appender : configuration.getAppenders()) {
logger.addAppender(appender.build(loggerContext, name, layoutFactory, levelFilterFactory, asyncAppenderFactory));
}
} else {
throw new IllegalArgumentException("Unsupported format of logger '" + entry.getKey() + "'");
}
}
return root;
}
static Level toLevel(@Nullable String text) {
if ("false".equalsIgnoreCase(text)) {
return Level.OFF;
} else if ("true".equalsIgnoreCase(text)) {
return Level.ALL;
}
return Level.toLevel(text, Level.INFO);
}
@Override
public String toString() {
return "DefaultLoggingFactory{"
+ "level=" + level
+ ", loggers=" + loggers
+ ", appenders=" + appenders
+ '}';
}
}