// Copyright 2018 The Bazel Authors. 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.google.devtools.build.lib.util; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.io.CountingOutputStream; import com.google.common.net.InetAddresses; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.lang.management.ManagementFactory; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; import java.util.Date; import java.util.Optional; import java.util.PriorityQueue; import java.util.TimeZone; import java.util.function.Function; import java.util.logging.ErrorManager; 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.Nullable; import javax.annotation.concurrent.GuardedBy; /** * A simple file-based logging handler that points a short symlink to the current log file and * provides an API for getting the current log file. * *
The log file path is concatenated from 4 elements: the prefix (a fixed string, typically a * directory); the pattern (allowing some % variable substitutions); the timestamp; and the * extension. * *
The handler can be configured from the JVM command line:
* -Djava.util.logging.config.file=/foo/bar/javalog.properties
*
where the javalog.properties file might contain something like
* handlers=com.google.devtools.build.lib.util.SimpleLogHandler
* com.google.devtools.build.lib.util.SimpleLogHandler.level=INFO
* com.google.devtools.build.lib.util.SimpleLogHandler.prefix=/foo/bar/logs/java.log
* com.google.devtools.build.lib.util.SimpleLogHandler.rotate_limit_bytes=1048576
* com.google.devtools.build.lib.util.SimpleLogHandler.total_limit_bytes=10485760
* com.google.devtools.build.lib.util.SimpleLogHandler.formatter=com.google.devtools.build.lib.util.SingleLineFormatter
*
*
*
The handler is thread-safe. IO operations ({@link #publish}, {@link #flush}, {@link #close}) * and {@link #getCurrentLogFilePath} block other access to the handler until completed. */ public final class SimpleLogHandler extends Handler { /** Max number of bytes to write before rotating the log. */ private final int rotateLimitBytes; /** Max number of bytes in all logs to keep before deleting oldest ones. */ private final int totalLimitBytes; /** Log file extension; the current process ID by default. */ private final String extension; /** True if the log file extension is not the process ID. */ private final boolean isStaticExtension; /** Absolute path to symbolic link to current log file. */ private final Path symlinkPath; /** Absolute path to common base name of log files. */ @VisibleForTesting final Path baseFilePath; /** Log file currently in use. */ @GuardedBy("this") private final Output output = new Output(); private static final String DEFAULT_PREFIX_STRING = "java.log"; private static final String DEFAULT_BASE_FILE_NAME_PATTERN = ".%h.%u.log.java."; /** Source for timestamps in filenames; non-static for testing. */ private final Clock clock; @VisibleForTesting static final String DEFAULT_TIMESTAMP_FORMAT = "yyyyMMdd-HHmmss."; /** * Timestamp format for log filenames; non-static because {@link SimpleDateFormat} is not * thread-safe. */ @GuardedBy("this") private final SimpleDateFormat timestampFormat = new SimpleDateFormat(DEFAULT_TIMESTAMP_FORMAT); /** * A {@link} LogHandlerQuerier for working with {@code SimpleLogHandler} instances. * *
This querier is intended for situations where the logging handler is configured on the JVM
* command line to be {@link SimpleLogHandler}, but where the code which needs to query the
* handler does not know the handler's class or cannot import it. The command line then should in
* addition specify {@code
* -Dcom.google.devtools.build.lib.util.LogHandlerQuerier.class=com.google.devtools.build.lib.util.SimpleLogHandler$HandlerQuerier}
* and an instance of {@link SimpleLogHandler.HandlerQuerier} class can then be obtained from
* {@code LogHandlerQuerier.getInstance()}.
*/
public static final class HandlerQuerier extends LogHandlerQuerier {
@Override
protected boolean canQuery(Handler handler) {
return handler instanceof SimpleLogHandler;
}
@Override
protected Optional All setters are optional; if unset, values from the JVM logging configuration or (if those
* too are unset) reasonable fallback values will be used. See individual setter documentation.
*/
public static final class Builder {
private String prefix;
private String pattern;
private String extension;
private String symlink;
private Integer rotateLimitBytes;
private Integer totalLimitBytes;
private Level logLevel;
private Formatter formatter;
private Clock clock;
public Builder setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
/**
* Sets the pattern for the log file name. The pattern may contain the following variables:
*
* The log file name will be constructed by appending the expanded pattern to the prefix and
* then by appending a timestamp and the extension.
*
* If unset, the value of "pattern" from the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, {@link #DEFAULT_BASE_FILE_NAME_PATTERN}
* will be used.
*
* @param pattern the pattern string, possibly containing If unset, the value of "extension" from the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, the process ID will be used.
*
* @param extension log file extension
* @return this {@code Builder} object
*/
public Builder setExtension(String extension) {
this.extension = extension;
return this;
}
/**
* Sets the log file symlink filename.
*
* If unset, the value of "symlink" from the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, the prefix will be used.
*
* @param symlink either symlink filename without a directory part, or an absolute path whose
* directory part matches the prefix
* @return this {@code Builder} object
*/
public Builder setSymlink(String symlink) {
this.symlink = symlink;
return this;
}
/**
* Sets the log file size limit; if unset or 0, log size is unlimited.
*
* If unset, the value of "rotate_limit_bytes" from the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, the log fie size is unlimited.
*
* @param rotateLimitBytes maximum log file size in bytes; must be >= 0; 0 means unlimited
* @return this {@code Builder} object
*/
public Builder setRotateLimitBytes(int rotateLimitBytes) {
this.rotateLimitBytes = Integer.valueOf(rotateLimitBytes);
return this;
}
/**
* Sets the total rotateLimitBytes for log files.
*
* If set, when opening a new handler or rotating a log file, the handler will scan for all
* log files with names matching the expected prefix, pattern, timestamp format, and extension,
* and delete the oldest ones to keep the total size under rotateLimitBytes.
*
* If unset, the value of "total_limit_bytes" from the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, the total log size is unlimited.
*
* @param totalLimitBytes maximum total log file size in bytes; must be >= 0; 0 means unlimited
* @return this {@code Builder} object
*/
public Builder setTotalLimitBytes(int totalLimitBytes) {
this.totalLimitBytes = Integer.valueOf(totalLimitBytes);
return this;
}
/**
* Sets the minimum level at which to log records.
*
* If unset, the level named by the "level" field in the JVM logging configuration for {@link
* SimpleLogHandler} will be used; and if that's unset, all records are logged.
*
* @param logLevel minimum log level
* @return this {@code Builder} object
*/
public Builder setLogLevel(Level logLevel) {
this.logLevel = logLevel;
return this;
}
/**
* Sets the log formatter.
*
* If unset, the class named by the "formatter" field in the JVM logging configuration for
* {@link SimpleLogHandler} will be used; and if that's unset, {@link SingleLineFormatter} will
* be used.
*
* @param formatter log formatter
* @return this {@code Builder} object
*/
public Builder setFormatter(Formatter formatter) {
this.formatter = formatter;
return this;
}
/**
* Sets the time source for timestamps in log filenames.
*
* Intended for testing. If unset, the system clock in the system timezone will be used.
*
* @param clock time source for timestamps
* @return this {@code Builder} object
*/
@VisibleForTesting
Builder setClockForTesting(Clock clock) {
this.clock = clock;
return this;
}
/** Builds a {@link SimpleLogHandler} instance. */
public SimpleLogHandler build() {
return new SimpleLogHandler(
prefix,
pattern,
extension,
symlink,
rotateLimitBytes,
totalLimitBytes,
logLevel,
formatter,
clock);
}
}
/**
* Constructs a log handler with all state taken from the JVM logging configuration or (as
* fallback) the defaults; see {@link SimpleLogHandler.Builder} documentation.
*
* @throws IllegalArgumentException if invalid JVM logging configuration values are encountered;
* see {@link SimpleLogHandler.Builder} documentation
*/
public SimpleLogHandler() {
this(null, null, null, null, null, null, null, null, null);
}
/**
* Constructs a log handler, falling back to the JVM logging configuration or (as last fallback)
* the defaults for those arguments which are null; see {@link SimpleLogHandler.Builder}
* documentation.
*
* @throws IllegalArgumentException if invalid non-null arguments or configured values are
* encountered; see {@link SimpleLogHandler.Builder} documentation
*/
private SimpleLogHandler(
@Nullable String prefix,
@Nullable String pattern,
@Nullable String extension,
@Nullable String symlink,
@Nullable Integer rotateLimitBytes,
@Nullable Integer totalLimit,
@Nullable Level logLevel,
@Nullable Formatter formatter,
@Nullable Clock clock) {
this.baseFilePath =
getBaseFilePath(
getConfiguredStringProperty(prefix, "prefix", DEFAULT_PREFIX_STRING),
getConfiguredStringProperty(pattern, "pattern", DEFAULT_BASE_FILE_NAME_PATTERN));
this.symlinkPath =
getSymlinkAbsolutePath(
this.baseFilePath.getParent(),
getConfiguredStringProperty(
symlink,
"symlink",
getConfiguredStringProperty(prefix, "prefix", DEFAULT_PREFIX_STRING)));
this.extension = getConfiguredStringProperty(extension, "extension", getPidString());
this.isStaticExtension = (getConfiguredStringProperty(extension, "extension", null) != null);
this.rotateLimitBytes = getConfiguredIntProperty(rotateLimitBytes, "rotate_limit_bytes", 0);
checkArgument(this.rotateLimitBytes >= 0, "File size limits cannot be negative");
this.totalLimitBytes = getConfiguredIntProperty(totalLimit, "total_limit_bytes", 0);
checkArgument(this.totalLimitBytes >= 0, "File size limits cannot be negative");
setLevel(getConfiguredLevelProperty(logLevel, "level", Level.ALL));
setFormatter(getConfiguredFormatterProperty(formatter, "formatter", new SingleLineFormatter()));
if (clock != null) {
this.clock = clock;
this.timestampFormat.setTimeZone(TimeZone.getTimeZone(clock.getZone()));
} else {
this.clock = Clock.system(ZoneId.systemDefault());
}
}
/**
* Returns the absolute path of the current log file if a log file is open or {@code
* Optional#empty()} otherwise.
*
* Since the log file is opened lazily, this method is expected to return {@code
* Optional#empty()} if no record has yet been published.
*/
public synchronized Optional Each log file's timestamp is determined only from the filename. The current log file will
* not be deleted.
*/
@GuardedBy("this")
private void deleteOldLogs() {
checkState(baseFilePath.isAbsolute());
PriorityQueue
*
*
* %u
will be expanded to the username
* %h
will be expanded to the hostname
* %%
will be expanded to %
* %u
, %h
,
* %%
variables as above
* @return this {@code Builder} object
*/
public Builder setPattern(String pattern) {
this.pattern = pattern;
return this;
}
/**
* Sets the log file extension.
*
*