com.google.cloud.logging.LoggingHandler Maven / Gradle / Ivy
Show all versions of gcloud-java-logging Show documentation
/*
* Copyright 2016 Google Inc. 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.cloud.logging;
import static com.google.common.base.MoreObjects.firstNonNull;
import com.google.cloud.MonitoredResource;
import com.google.cloud.logging.Logging.WriteOption;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
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 java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
* A logging handler that synchronously outputs logs generated with {@link java.util.logging.Logger}
* to Stackdriver Logging.
*
* Java logging levels (see {@link java.util.logging.Level}) are mapped to the following Google
* Stackdriver Logging severities:
*
*
* Java Level Stackdriver Logging Severity
* SEVERE ERROR
* WARNING WARNING
* INFO INFO
* CONFIG INFO
* FINE DEBUG
* FINER DEBUG
* FINEST DEBUG
*
*
* Original Java logging levels are added as labels (with {@code levelName} and
* {@code levelValue} keys, respectively) to the corresponding Stackdriver Logging {@link LogEntry}.
* You can read entry labels using {@link LogEntry#labels()}. To use logging levels that correspond
* to Stackdriver Logging severities you can use {@link LoggingLevel}.
*
*
Configuration: By default each {@code LoggingHandler} is initialized using the
* following {@code LogManager} configuration properties (that you can set in the
* {@code logging.properties} file). If properties are not defined (or have invalid values) then the
* specified default values are used.
*
* - {@code com.google.cloud.logging.LoggingHandler.log} the log name (defaults to
* {@code java.log}).
*
- {@code com.google.cloud.logging.LoggingHandler.level} specifies the default level for the
* handler (defaults to {@code Level.INFO}).
*
- {@code com.google.cloud.logging.LoggingHandler.filter} specifies the name of a {@link Filter}
* class to use (defaults to no filter).
*
- {@code com.google.cloud.logging.LoggingHandler.formatter} specifies the name of a
* {@link Formatter} class to use (defaults to {@link SimpleFormatter}).
*
- {@code com.google.cloud.logging.LoggingHandler.flushSize} specifies the maximum size of the
* log buffer. Once reached, logs are transmitted to the Stackdriver Logging service (defaults
* to 1).
*
- {@code com.google.cloud.logging.LoggingHandler.flushLevel} specifies the flush log level.
* When a log with this level is published, logs are transmitted to the Stackdriver Logging
* service (defaults to {@link LoggingLevel#ERROR}).
*
*
* To add a {@code LoggingHandler} to an existing {@link Logger} and be sure to avoid infinite
* recursion when logging, use the {@link #addHandler(Logger, LoggingHandler)} method. Alternatively
* you can add the handler via {@code logging.properties}. For example using the following line:
*
* {@code com.example.mypackage.handlers=com.google.cloud.logging.LoggingHandler}
*
*/
public class LoggingHandler extends Handler {
private static final String HANDLERS_PROPERTY = "handlers";
private static final String ROOT_LOGGER_NAME = "";
private static final String[] NO_HANDLERS = new String[0];
private static final Set EXCLUDED_LOGGERS = ImmutableSet.of("io.grpc", "io.netty",
"com.google.api.client.http", "sun.net.www.protocol.http");
private final LoggingOptions options;
private final List buffer = new LinkedList<>();
private final WriteOption[] writeOptions;
private Logging logging;
private Level flushLevel;
private long flushSize;
/**
* Creates an handler that publishes messages to Stackdriver Logging.
*/
public LoggingHandler() {
this(null, null, null);
}
/**
* Creates a handler that publishes messages to Stackdriver Logging.
*
* @param log the name of the log to which log entries are written
*/
public LoggingHandler(String log) {
this(log, null, null);
}
/**
* Creates a handler that publishes messages to Stackdriver Logging.
*
* @param log the name of the log to which log entries are written
* @param options options for the Stackdriver Logging service
*/
public LoggingHandler(String log, LoggingOptions options) {
this(log, options, null);
}
/**
* Creates a handler that publishes messages to Stackdriver Logging.
*
* @param log the name of the log to which log entries are written
* @param options options for the Stackdriver Logging service
* @param monitoredResource the monitored resource to which log entries refer
*/
public LoggingHandler(String log, LoggingOptions options, MonitoredResource monitoredResource) {
LogConfigHelper helper = new LogConfigHelper();
String className = getClass().getName();
this.options = options != null ? options : LoggingOptions.defaultInstance();
this.flushLevel = helper.getLevelProperty(className + ".flushLevel", LoggingLevel.ERROR);
this.flushSize = helper.getLongProperty(className + ".flushSize", 1L);
setLevel(helper.getLevelProperty(className + ".level", Level.INFO));
setFilter(helper.getFilterProperty(className + ".filter", null));
setFormatter(helper.getFormatterProperty(className + ".formatter", new SimpleFormatter()));
String logName = firstNonNull(log, helper.getProperty(className + ".log", "java.log"));
MonitoredResource resource = firstNonNull(monitoredResource, defaultResource());
writeOptions = new WriteOption[]{WriteOption.logName(logName), WriteOption.resource(resource)};
maskLoggers();
}
private static void maskLoggers() {
for (String loggerName : EXCLUDED_LOGGERS) {
Logger logger = Logger.getLogger(loggerName);
// We remove the Clould Logging handler if it has been registered for a logger that should be
// masked
List loggingHandlers = getLoggingHandlers(logger);
for (LoggingHandler loggingHandler : loggingHandlers) {
logger.removeHandler(loggingHandler);
}
// We mask ancestors if they have a Stackdriver Logging Handler registered
Logger currentLogger = logger;
Logger ancestor = currentLogger.getParent();
boolean masked = false;
while (ancestor != null && !masked) {
if (hasLoggingHandler(ancestor)) {
currentLogger.setUseParentHandlers(false);
masked = true;
}
currentLogger = ancestor;
ancestor = ancestor.getParent();
}
}
}
private static List getLoggingHandlers(Logger logger) {
ImmutableList.Builder builder = ImmutableList.builder();
for (Handler handler : logger.getHandlers()) {
if (handler instanceof LoggingHandler) {
builder.add((LoggingHandler) handler);
}
}
return builder.build();
}
private static boolean hasLoggingHandler(Logger logger) {
// look for Stackdriver Logging handler registered with addHandler()
for (Handler handler : logger.getHandlers()) {
if (handler instanceof LoggingHandler) {
return true;
}
}
// look for Stackdriver Logging handler registered via logging.properties
String loggerName = logger.getName();
String propertyName = loggerName.equals(ROOT_LOGGER_NAME)
? HANDLERS_PROPERTY : loggerName + "." + HANDLERS_PROPERTY;
String handlersProperty = LogManager.getLogManager().getProperty(propertyName);
String[] handlers = handlersProperty != null ? handlersProperty.split(",") : NO_HANDLERS;
for (String handlerName : handlers) {
if (handlerName.contains(LoggingHandler.class.getPackage().getName())) {
return true;
}
}
return false;
}
private MonitoredResource defaultResource() {
return MonitoredResource.of("global", ImmutableMap.of("project_id", options.projectId()));
}
private static class LogConfigHelper {
private final LogManager manager = LogManager.getLogManager();
String getProperty(String name, String defaultValue) {
return firstNonNull(manager.getProperty(name), defaultValue);
}
long getLongProperty(String name, long defaultValue) {
try {
return Long.parseLong(manager.getProperty(name));
} catch (NumberFormatException ex) {
// If the level does not exist we fall back to default value
}
return defaultValue;
}
Level getLevelProperty(String name, Level defaultValue) {
String stringLevel = manager.getProperty(name);
if (stringLevel == null) {
return defaultValue;
}
try {
return Level.parse(stringLevel);
} catch (IllegalArgumentException ex) {
// If the level does not exist we fall back to default value
}
return defaultValue;
}
Filter getFilterProperty(String name, Filter defaultValue) {
String stringFilter = manager.getProperty(name);
try {
if (stringFilter != null) {
Class clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
return (Filter) clz.newInstance();
}
} catch (Exception ex) {
// If we cannot create the filter we fall back to default value
}
return defaultValue;
}
Formatter getFormatterProperty(String name, Formatter defaultValue) {
String stringFilter = manager.getProperty(name);
try {
if (stringFilter != null) {
Class clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
return (Formatter) clz.newInstance();
}
} catch (Exception ex) {
// If we cannot create the filter we fall back to default value
}
return defaultValue;
}
}
/**
* Returns an instance of the logging service.
*/
Logging logging() {
if (logging == null) {
logging = options.service();
}
return logging;
}
@Override
public synchronized void publish(LogRecord record) {
// check that the log record should be logged
if (!isLoggable(record)) {
return;
}
LogEntry entry = entryFor(record);
if (entry != null) {
buffer.add(entry);
}
if (buffer.size() >= flushSize || record.getLevel().intValue() >= flushLevel.intValue()) {
flush();
}
}
private LogEntry entryFor(LogRecord record) {
String payload;
try {
payload = getFormatter().format(record);
} catch (Exception ex) {
// Formatting can fail but we should not throw an exception, we report the error instead
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return null;
}
Level level = record.getLevel();
Map labels = ImmutableMap.of(
"levelName", level.getName(),
"levelValue", String.valueOf(level.intValue()));
return LogEntry.builder(Payload.StringPayload.of(payload))
.labels(labels)
.severity(severityFor(level))
.build();
}
private static Severity severityFor(Level level) {
if (level instanceof LoggingLevel) {
return ((LoggingLevel) level).severity();
}
switch (level.intValue()) {
// FINEST
case 300:
return Severity.DEBUG;
// FINER
case 400:
return Severity.DEBUG;
// FINE
case 500:
return Severity.DEBUG;
// CONFIG
case 700:
return Severity.INFO;
// INFO
case 800:
return Severity.INFO;
// WARNING
case 900:
return Severity.WARNING;
// SEVERE
case 1000:
return Severity.ERROR;
default:
return Severity.DEFAULT;
}
}
/**
* Writes the provided list of log entries to Stackdriver Logging. Override this method to change
* how entries should be written.
*/
void write(List entries, WriteOption... options) {
logging().write(entries, options);
}
@Override
public synchronized void flush() {
try {
write(buffer, writeOptions);
} catch (Exception ex) {
// writing can fail but we should not throw an exception, we report the error instead
reportError(null, ex, ErrorManager.FLUSH_FAILURE);
} finally {
buffer.clear();
}
}
/**
* Closes the handler and the associated {@link Logging} object.
*/
@Override
public synchronized void close() throws SecurityException {
if (logging != null) {
try {
logging.close();
} catch (Exception ex) {
// ignore
}
}
logging = null;
}
/**
* Sets the flush log level. When a log with this level is published, the log buffer is
* transmitted to the Stackdriver Logging service, regardless of its size. If not set,
* {@link LoggingLevel#ERROR} is used.
*/
public synchronized Level setFlushLevel(Level flushLevel) {
this.flushLevel = flushLevel;
return flushLevel;
}
/**
* Sets the maximum size of the log buffer. Once the maximum size of the buffer is reached, logs
* are transmitted to the Stackdriver Logging service. If not set, a log is sent to the service as
* soon as published.
*/
public synchronized long setFlushSize(long flushSize) {
this.flushSize = flushSize;
return flushSize;
}
/**
* Adds the provided {@code LoggingHandler} to {@code logger}. Use this method to register Cloud
* Logging handlers instead of {@link Logger#addHandler(Handler)} to avoid infinite recursion
* when logging.
*/
public static void addHandler(Logger logger, LoggingHandler handler) {
logger.addHandler(handler);
maskLoggers();
}
}