blob: b62a18805d95e66fa751eaa5da65603e6095abfd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.drill.test;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
/**
* Establishes test-specific logging without having to alter the global
* <tt>logback-test.xml</tt> file. Allows directing output to the console
* (if not already configured) and setting the log level on specific loggers
* of interest in the test. The fixture automatically restores the original
* log configuration on exit.
* <p>
* Typical usage: <pre><code>
* {@literal @}Test
* public void myTest() {
* LogFixtureBuilder logBuilder = LogFixture.builder()
* .toConsole()
* .disable() // Silence all other loggers
* .logger(ExternalSortBatch.class, Level.DEBUG);
* try (LogFixture logs = logBuilder.build()) {
* // Test code here
* }
* }</code></pre>
* <p>
* You can &ndash; and should &ndash; combine the log fixture with the
* cluster and client fixtures to have complete control over your test-time
* Drill environment.
*/
public class LogFixture implements AutoCloseable {
// Elapsed time in ms, log level, thread, logger, message.
public static final String DEFAULT_CONSOLE_FORMAT = "%r %level [%thread] [%logger] - %msg%n";
private static final String DRILL_PACKAGE_NAME = "org.apache.drill";
/**
* Memento for a logger name and level.
*/
public static class LogSpec {
String loggerName;
Level logLevel;
public LogSpec(String loggerName, Level level) {
this.loggerName = loggerName;
this.logLevel = level;
}
}
/**
* Builds the log settings to be used for a test. The log settings here
* add to those specified in a <tt>logback.xml</tt> or
* <tt>logback-test.xml</tt> file on your class path. In particular, if
* the logging configuration already redirects the Drill logger to the
* console, setting console logging here does nothing.
*/
public static class LogFixtureBuilder {
private String consoleFormat = DEFAULT_CONSOLE_FORMAT;
private boolean logToConsole;
private List<LogSpec> loggers = new ArrayList<>();
private ConsoleAppender<ILoggingEvent> appender;
/**
* Send all enabled logging to the console (if not already configured.) Some
* Drill log configuration files send the root to the console (or file), but
* the Drill loggers to Lilith. In that case, Lilith "hides" the console
* logger. Using this call adds a console logger to the Drill logger so that
* output does, in fact, go to the console regardless of the configuration
* in the Logback configuration file.
*
* @return this builder
*/
public LogFixtureBuilder toConsole() {
logToConsole = true;
return this;
}
public LogFixtureBuilder toConsole(ConsoleAppender<ILoggingEvent> appender, String format) {
this.appender = appender;
return toConsole(format);
}
/**
* Send logging to the console using the defined format.
*
* @param format valid Logback log format
* @return this builder
*/
public LogFixtureBuilder toConsole(String format) {
consoleFormat = format;
return toConsole();
}
/**
* Set a specific logger to the given level.
*
* @param loggerName name of the logger (typically used for package-level
* loggers)
* @param level the desired Logback-defined level
* @return this builder
*/
public LogFixtureBuilder logger(String loggerName, Level level) {
loggers.add(new LogSpec(loggerName, level));
return this;
}
/**
* Set a specific logger to the given level.
*
* @param loggerClass class that defines the logger (typically used for
* class-specific loggers)
* @param level the desired Logback-defined level
* @return this builder
*/
public LogFixtureBuilder logger(Class<?> loggerClass, Level level) {
loggers.add(new LogSpec(loggerClass.getName(), level));
return this;
}
/**
* Turns off all logging. If called first, you can set disable as your
* general policy, then turn back on loggers selectively for those
* of interest.
* @return this builder
*/
public LogFixtureBuilder disable() {
return rootLogger(Level.OFF);
}
/**
* Set the desired log level on the root logger.
* @param level the desired Logback log level
* @return this builder
*/
public LogFixtureBuilder rootLogger(Level level) {
loggers.add(new LogSpec(Logger.ROOT_LOGGER_NAME, level));
return this;
}
/**
* Apply the log levels and output, then return a fixture to be used
* in a try-with-resources block. The fixture automatically restores
* the original configuration on completion of the try block.
* @return the log fixture
*/
public LogFixture build() {
return new LogFixture(this);
}
}
private PatternLayoutEncoder ple;
private ConsoleAppender<ILoggingEvent> appender;
private List<LogSpec> loggers = new ArrayList<>();
private Logger drillLogger;
public LogFixture(LogFixtureBuilder builder) {
if (builder.logToConsole) {
setupConsole(builder);
}
setupLoggers(builder);
}
/**
* Creates a new log fixture builder.
* @return the log fixture builder
*/
public static LogFixtureBuilder builder() {
return new LogFixtureBuilder();
}
private void setupConsole(LogFixtureBuilder builder) {
drillLogger = (Logger)LoggerFactory.getLogger(DRILL_PACKAGE_NAME);
if (builder.appender == null && drillLogger.getAppender("STDOUT") != null) {
return;
}
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
ple = new PatternLayoutEncoder();
ple.setPattern(builder.consoleFormat);
ple.setContext(lc);
ple.start();
appender = builder.appender == null ? new ConsoleAppender<>() : builder.appender;
appender.setContext(lc);
appender.setName("Console");
appender.setEncoder(ple);
appender.start();
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.addAppender(appender);
drillLogger.addAppender(appender);
}
private void setupLoggers(LogFixtureBuilder builder) {
for (LogSpec spec : builder.loggers) {
setupLogger(spec);
}
}
private void setupLogger(LogSpec spec) {
Logger logger = (Logger)LoggerFactory.getLogger(spec.loggerName);
Level oldLevel = logger.getLevel();
logger.setLevel(spec.logLevel);
loggers.add(new LogSpec(spec.loggerName, oldLevel));
}
@Override
public void close() {
restoreLoggers();
restoreConsole();
}
private void restoreLoggers() {
for (LogSpec spec : loggers) {
Logger logger = (Logger)LoggerFactory.getLogger(spec.loggerName);
logger.setLevel(spec.logLevel);
}
}
private void restoreConsole() {
if (appender == null) {
return;
}
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.detachAppender(appender);
drillLogger.detachAppender(appender);
appender.stop();
ple.stop();
}
}