// Copyright 2016 Twitter. All rights reserved.
//
// 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.twitter.heron.common.utils.logging;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import com.twitter.heron.common.basics.ByteAmount;
/**
* A helper class to init corresponding LOGGER setting
* loggerInit() is required to invoke before any logging
* <p>
* Credits: https://blogs.oracle.com/nickstephen/entry/java_redirecting_system_out_and
*/
public final class LoggingHelper {
private static final String FORMAT_PROP_KEY = "java.util.logging.SimpleFormatter.format";
private static final String DEFAULT_FORMAT = "[%1$tF %1$tT %1$tz] [%4$s] %3$s: %5$s %6$s %n";
private LoggingHelper() {
}
/**
* Init java util logging with default format
*
* @param level the Level of message to log
* @param isRedirectStdOutErr whether we redirect std out&err
*/
public static void loggerInit(Level level, boolean isRedirectStdOutErr) throws IOException {
loggerInit(level, isRedirectStdOutErr, DEFAULT_FORMAT);
}
/**
* Init java util logging
*
* @param level the Level of message to log
* @param isRedirectStdOutErr whether we redirect std out&err
* @param format the format to log
*/
public static void loggerInit(Level level, boolean isRedirectStdOutErr, String format)
throws IOException {
// Set the java util logging format
setLoggingFormat(format);
// Configure the root logger and its handlers so that all the
// derived loggers will inherit the properties
Logger rootLogger = Logger.getLogger("");
for (Handler handler : rootLogger.getHandlers()) {
handler.setLevel(level);
}
rootLogger.setLevel(level);
if (rootLogger.getLevel().intValue() < Level.WARNING.intValue()) {
// zookeeper logging scares me. if people want this, we can patch to config-drive this
Logger.getLogger("org.apache.zookeeper").setLevel(Level.WARNING);
}
if (isRedirectStdOutErr) {
// Remove ConsoleHandler if present, to avoid StackOverflowError.
// ConsoleHandler writes to System.err and since we are redirecting
// System.err to Logger, it results in an infinite loop.
for (Handler handler : rootLogger.getHandlers()) {
if (handler instanceof ConsoleHandler) {
rootLogger.removeHandler(handler);
}
}
// now rebind stdout/stderr to logger
Logger logger;
LoggingOutputStream los;
logger = Logger.getLogger("stdout");
los = new LoggingOutputStream(logger, StdOutErrLevel.STDOUT);
System.setOut(new PrintStream(los, true));
logger = Logger.getLogger("stderr");
los = new LoggingOutputStream(logger, StdOutErrLevel.STDERR);
System.setErr(new PrintStream(los, true));
}
}
protected static void setLoggingFormat(String format) {
System.setProperty(FORMAT_PROP_KEY, format);
}
public static void addLoggingHandler(Handler handler) {
Logger.getLogger("").addHandler(handler);
}
/**
* Initialize a <tt>FileHandler</tt> to write to a set of files
* with optional append. When (approximately) the given limit has
* been written to one file, another file will be opened. The
* output will cycle through a set of count files.
* The pattern of file name should be: ${processId}.log.index
* <p>
* The <tt>FileHandler</tt> is configured based on <tt>LogManager</tt>
* properties (or their default values) except that the given pattern
* argument is used as the filename pattern, the file limit is
* set to the limit argument, and the file count is set to the
* given count argument, and the append mode is set to the given
* <tt>append</tt> argument.
* <p>
* The count must be at least 1.
*
* @param limit the maximum number of bytes to write to any one file
* @param count the number of files to use
* @param append specifies append mode
* @throws IOException if there are IO problems opening the files.
* @throws SecurityException if a security manager exists and if
* the caller does not have <tt>LoggingPermission("control")</tt>.
* @throws IllegalArgumentException if {@code limit < 0}, or {@code count < 1}.
* @throws IllegalArgumentException if pattern is an empty string
*/
public static FileHandler getFileHandler(String processId,
String loggingDir,
boolean append,
ByteAmount limit,
int count) throws IOException, SecurityException {
String pattern = loggingDir + "/" + processId + ".log.%g";
FileHandler fileHandler = new FileHandler(pattern, (int) limit.asBytes(), count, append);
fileHandler.setFormatter(new SimpleFormatter());
fileHandler.setEncoding(StandardCharsets.UTF_8.toString());
return fileHandler;
}
public static final class StdOutErrLevel extends Level {
private static final long serialVersionUID = -3442332825945855738L;
/**
* Level for STDOUT activity.
*/
public static final Level STDOUT =
new StdOutErrLevel("STDOUT", Level.INFO.intValue() + 53);
/**
* Level for STDERR activity
*/
public static final Level STDERR =
new StdOutErrLevel("STDERR", Level.INFO.intValue() + 54);
/**
* Private constructor
*/
private StdOutErrLevel(String name, int value) {
super(name, value);
}
/**
* Method to avoid creating duplicate instances when deserializing the
* object.
*
* @return the singleton instance of this <code>Level</code> value in this
* classloader
* @throws java.io.ObjectStreamException If unable to deserialize
*/
protected Object readResolve()
throws ObjectStreamException {
if (this.intValue() == STDOUT.intValue()) {
return STDOUT;
}
if (this.intValue() == STDERR.intValue()) {
return STDERR;
}
throw new InvalidObjectException("Unknown instance :" + this);
}
}
/**
* An OutputStream that writes contents to a Logger upon each call to flush()
*/
public static class LoggingOutputStream extends ByteArrayOutputStream {
private String lineSeparator;
private Logger logger;
private Level level;
/**
* Constructor
*
* @param logger Logger to write to
* @param level Level at which to write the log message
*/
public LoggingOutputStream(Logger logger, Level level) {
super();
this.logger = logger;
this.level = level;
lineSeparator = System.getProperty("line.separator");
}
/**
* upon flush() write the existing contents of the OutputStream
* to the logger as a log record.
*
* @throws java.io.IOException in case of error
*/
public void flush() throws IOException {
String record;
synchronized (this) {
super.flush();
record = this.toString();
super.reset();
if (record.length() == 0 || record.equals(lineSeparator)) {
// avoid empty records
return;
}
logger.logp(level, "", "", record);
}
}
}
}