package com.twitter.common.logging; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; /** * @author Franco Callari * @author Keith Tsui. */ public class RootLogConfigTest { private static class FakeHandler extends Handler { boolean hasPublished = false; FakeHandler() { reset(); } void reset() { hasPublished = false; } public void publish(LogRecord record) { hasPublished = true; } public void flush() {} public void close() throws SecurityException {} } // Dummy classes used by the tests for --vmodule. private static class ClassA { static Logger logger = Logger.getLogger(ClassA.class.getName()); } private static class ClassB { static Logger logger = Logger.getLogger(ClassB.class.getName()); } private ByteArrayOutputStream fakeErrorLog; private PrintStream errPrintStream; Logger fakeRootLogger, testLogger; FakeHandler fakeFileLog; ConsoleHandler consoleHandler; private void assertHasLoggedToStderr() { errPrintStream.flush(); assertTrue(fakeErrorLog.size() > 0); } private void assertHasNotLoggedToStderr() { errPrintStream.flush(); assertEquals(fakeErrorLog.size(), 0); } private void assertHasLoggedToFile() { assertTrue(fakeFileLog.hasPublished); } private void assertHasNotLoggedToFile() { assertFalse(fakeFileLog.hasPublished); } // Passes if logger log at least at the given level, directing to stderr. private void assertErrorLogAtLevel(Logger logger, Level level) { List<Level> levels = Arrays.asList( Level.SEVERE, Level.WARNING, Level.INFO, Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST); for (Level l : levels) { logger.log(l, "Message"); if (level.intValue() <= l.intValue()) { assertHasLoggedToStderr(); } else { assertHasNotLoggedToStderr(); } resetLogs(); } } // Passes if logger does not allow verbose logging. private void assertNoVerboseLogging(Logger logger) { logger.config("Config"); logger.fine("Fine"); logger.finer("Finer"); logger.finest("Finest"); assertHasNotLoggedToStderr(); assertHasNotLoggedToFile(); } private void resetLogs() { errPrintStream.flush(); fakeErrorLog.reset(); fakeFileLog.reset(); } // The following two methods are used to inject our fake root logger, so to avoid test flakyness // due to background threads. private RootLogConfig.Configuration getConfig() { return RootLogConfig.builder().rootLoggerName(fakeRootLogger.getName()); } private void setFakeRootForLogger(Logger logger) { Preconditions.checkArgument(logger != fakeRootLogger); logger.setUseParentHandlers(true); logger.setParent(fakeRootLogger); } @Before public void setUp() { // Intercept stderr (this must be done first). fakeErrorLog = new ByteArrayOutputStream(); errPrintStream = new PrintStream(fakeErrorLog); System.setErr(errPrintStream); // Create other members consoleHandler = new ConsoleHandler(); fakeFileLog = new FakeHandler(); // Emulate default setup (just a console handler), but avoiding the use // of the global root logger so not to get a flaky test due to background threads. fakeRootLogger = Logger.getLogger("FakeRoot-" + UUID.randomUUID().toString()); fakeRootLogger.setUseParentHandlers(false); for (Handler h : fakeRootLogger.getHandlers()) { fakeRootLogger.removeHandler(h); } fakeRootLogger.addHandler(consoleHandler); testLogger = Logger.getLogger(RootLogConfigTest.class.getName()); testLogger.setUseParentHandlers(true); testLogger.setParent(fakeRootLogger); setFakeRootForLogger(ClassA.logger); setFakeRootForLogger(ClassB.logger); resetLogs(); } @Test public void testDefaultConfig() { // Verify that default info, warning, severe logging goes to stderr, no logging below info. assertErrorLogAtLevel(testLogger, Level.INFO); resetLogs(); assertNoVerboseLogging(testLogger); } @Test public void testLogToStderr() { // Add a fake handler, verify that it works along with the console. fakeRootLogger.addHandler(fakeFileLog); testLogger.info("Info"); assertHasLoggedToStderr(); assertHasLoggedToFile(); // Configure logtostderr getConfig().logToStderr(true).apply(); resetLogs(); // Verify that severe, warning, info logs go to stderr only. testLogger.severe("Severe"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); testLogger.warning("Warning"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); testLogger.info("Info"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); assertNoVerboseLogging(testLogger); } @Test public void testAlsoLogToStderr() { // Add a fake handler, remove console handler, verify that it works. fakeRootLogger.removeHandler(consoleHandler); fakeRootLogger.addHandler(fakeFileLog); testLogger.info("Info"); assertHasNotLoggedToStderr(); assertHasLoggedToFile(); resetLogs(); // Configure alsologtostderr getConfig().alsoLogToStderr(true).apply(); resetLogs(); // Verify that severe, warning, info logs go to both. testLogger.severe("Severe"); assertHasLoggedToStderr(); assertHasLoggedToFile(); resetLogs(); testLogger.warning("Warning"); assertHasLoggedToStderr(); assertHasLoggedToFile(); resetLogs(); testLogger.info("Info"); assertHasLoggedToStderr(); assertHasLoggedToFile(); resetLogs(); assertNoVerboseLogging(testLogger); } @Test public void testLogToStderrOverridesAlsoLogToStderr() { // Add a fake handler, remove console handler, verify that it works. fakeRootLogger.removeHandler(consoleHandler); fakeRootLogger.addHandler(fakeFileLog); testLogger.info("Info"); assertHasNotLoggedToStderr(); assertHasLoggedToFile(); // Configure with logtostderr AND alsologtostderr getConfig().logToStderr(true).alsoLogToStderr(true).apply(); resetLogs(); // Verify that severe, warning, info logs go to stderr only. testLogger.severe("Severe"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); testLogger.warning("Warning"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); testLogger.info("Info"); assertHasLoggedToStderr(); assertHasNotLoggedToFile(); resetLogs(); assertNoVerboseLogging(testLogger); } @Test public void testUseGLogFormatter() { // Configure glogformatter. We test in "logtostderr" mode so to verify correct formatting // for both handlers. getConfig().logToStderr(true).useGLogFormatter(true).apply(); resetLogs(); testLogger.severe("Severe Log Message"); assertHasLoggedToStderr(); String output = fakeErrorLog.toString(); // Verify that it is all in one line and chope the \n. assertTrue(output.split("\n").length == 1); assertTrue(output.endsWith("\n")); output = output.replaceAll("\n", ""); // Verify that it is on glog format. assertTrue(output.matches( new StringBuilder() .append("S\\d+ ") // Level, month, day. .append("\\d\\d:\\d\\d:\\d\\d\\.\\d+ ") // Timestamp. .append("THREAD\\d\\d ") // Thread id. .append(RootLogConfigTest.class.getName() + "\\.testUseGLogFormatter: ") // Class name. .append("Severe Log Message") // Message. .toString())); } @Test public void testVlog() { // Configure with logtoStderr and vlog==FINE; getConfig().logToStderr(true).vlog(RootLogConfig.LogLevel.FINE).apply(); resetLogs(); // Verify logging at levels fine and above. assertErrorLogAtLevel(testLogger, Level.FINE); } @Test public void testVModule() { // Configure with ClassA using FINE and ClassB using WARNING; Map<Class<?>, RootLogConfig.LogLevel> vmoduleMap = ImmutableMap.of(ClassA.class, RootLogConfig.LogLevel.FINE, ClassB.class, RootLogConfig.LogLevel.WARNING); getConfig().logToStderr(true).vmodule(vmoduleMap).apply(); resetLogs(); // No verbose logging other than in ClassA and ClassB. assertNoVerboseLogging(testLogger); // ClassA logs at FINE and above assertErrorLogAtLevel(ClassA.logger, Level.FINE); resetLogs(); // ClassB logs at WARNING and above assertErrorLogAtLevel(ClassB.logger, Level.WARNING); } @Test public void testVModuleOverridesVlog() { // Configure with ClassA using FINE and ClassB using FINER; Map<Class<?>, RootLogConfig.LogLevel> vmoduleMap = ImmutableMap.of(ClassA.class, RootLogConfig.LogLevel.FINEST, ClassB.class, RootLogConfig.LogLevel.INFO); // Configure setting default vlog=FINER getConfig() .logToStderr(true).vlog(RootLogConfig.LogLevel.FINER).vmodule(vmoduleMap).apply(); resetLogs(); // Default logging is at finer level. assertErrorLogAtLevel(testLogger, Level.FINER); // ClassA logs at FINEST and above assertErrorLogAtLevel(ClassA.logger, Level.FINEST); resetLogs(); // ClassB logs at INFO and above assertErrorLogAtLevel(ClassB.logger, Level.INFO); } }