/* * Copyright 2014-present Facebook, Inc. * * 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.facebook.buck.log; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.io.IOError; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; import javax.annotation.concurrent.GuardedBy; /** Implementation of Handler which writes to the console (System.err by default). */ public class ConsoleHandler extends Handler { private static final Level DEFAULT_LEVEL = Level.SEVERE; private final ConsoleHandlerState.Writer defaultOutputStreamWriter; private final ConsoleHandlerState state; @GuardedBy("this") private boolean closed; public ConsoleHandler() { this( utf8OutputStreamWriter(System.err), new LogFormatter(), getLogLevelFromProperty(LogManager.getLogManager(), DEFAULT_LEVEL), GlobalStateManager.singleton().getConsoleHandlerState()); } @VisibleForTesting ConsoleHandler( ConsoleHandlerState.Writer defaultOutputStreamWriter, Formatter formatter, Level level, ConsoleHandlerState state) { this.defaultOutputStreamWriter = defaultOutputStreamWriter; setFormatter(formatter); setLevel(level); this.state = state; } @Override public void publish(LogRecord record) { synchronized (this) { if (closed || !(isLoggable(record) || isLoggableWithRegisteredLogLevel(record)) || isBlacklisted(record)) { return; } } Iterable<ConsoleHandlerState.Writer> outputStreamWriters = getOutputStreamWritersForRecord(record); try { String formatted = getFormatter().format(record); for (ConsoleHandlerState.Writer outputStreamWriter : outputStreamWriters) { synchronized (outputStreamWriter) { outputStreamWriter.write(formatted); if (record.getLevel().intValue() >= Level.SEVERE.intValue()) { outputStreamWriter.flush(); } } } } catch (IOException e) { throw new IOError(e); } } @Override public void close() { synchronized (this) { if (!closed) { flush(); // We explicitly do not close any registered writers, so we don't close // System.err accidentally. closed = true; } } } @Override public void flush() { synchronized (this) { if (closed) { return; } } try { for (ConsoleHandlerState.Writer outputStreamWriter : state.getAllAvailableWriters()) { synchronized (outputStreamWriter) { outputStreamWriter.flush(); } } synchronized (defaultOutputStreamWriter) { defaultOutputStreamWriter.flush(); } } catch (IOException e) { throw new IOError(e); } } private static Level getLogLevelFromProperty(LogManager logManager, Level defaultLevel) { String levelStr = logManager.getProperty(ConsoleHandler.class.getName() + ".level"); if (levelStr != null) { return Level.parse(levelStr); } else { return defaultLevel; } } public static ConsoleHandlerState.Writer utf8OutputStreamWriter(OutputStream outputStream) { try { final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8"); return new ConsoleHandlerState.Writer() { @Override public void write(String line) throws IOException { outputStreamWriter.write(line); } @Override public void flush() throws IOException { outputStreamWriter.flush(); } @Override public void close() throws IOException { outputStreamWriter.close(); } }; } catch (UnsupportedEncodingException e) { throw new IOError(e); } } private boolean isLoggableWithRegisteredLogLevel(LogRecord record) { long recordThreadId = record.getThreadID(); String logRecordCommandId = state.threadIdToCommandId(recordThreadId); if (logRecordCommandId == null) { // An unregistered thread created this LogRecord, so we don't want to force logging it. return false; } Level commandIdLogLevel = state.getLogLevel(logRecordCommandId); if (commandIdLogLevel == null) { // No log level override registered for this command ID. Don't force logging it. return false; } // Level.ALL.intValue() is Integer.MIN_VALUE, so have to compare it explicitly. return commandIdLogLevel.equals(Level.ALL) || commandIdLogLevel.intValue() >= record.getLevel().intValue(); } private boolean isBlacklisted(LogRecord record) { // Guava futures internals are not very actionable to the user but we still want to have // them in the log. return record.getLoggerName() != null && record.getLoggerName().startsWith("com.google.common.util.concurrent"); } private Iterable<ConsoleHandlerState.Writer> getOutputStreamWritersForRecord(LogRecord record) { long recordThreadId = record.getThreadID(); String logRecordCommandId = state.threadIdToCommandId(recordThreadId); if (logRecordCommandId != null) { ConsoleHandlerState.Writer consoleWriter = state.getWriter(logRecordCommandId); if (consoleWriter != null) { return ImmutableSet.of(consoleWriter); } else { return ImmutableSet.of(defaultOutputStreamWriter); } } else { Iterable<ConsoleHandlerState.Writer> allConsoleWriters = state.getAllAvailableWriters(); if (Iterables.isEmpty(allConsoleWriters)) { return ImmutableSet.of(defaultOutputStreamWriter); } else { ImmutableSet.Builder<ConsoleHandlerState.Writer> builder = ImmutableSet.builder(); builder.addAll(allConsoleWriters); return builder.build(); } } } }