/**
* Copyright 2015 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.apphosting.vmruntime;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
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;
/**
* {@code VmRuntimeLogHandler} is installed on the root logger. It forwards all messages on to
* {@code ApiProxy.log(ApiProxy.LogRecord)}, where they can be attached to the runtime response.
*
*/
// See //j/c/g/apphosting/runtime/security/shared/intercept/java/util/logging/DefaultHandler.java
public class VmRuntimeLogHandler extends Handler {
public static final String JAVA_UTIL_LOGGING_CONFIG_PROPERTY = "java.util.logging.config.file";
private static final Logger ROOT_LOGGER = Logger.getLogger("");
private VmRuntimeLogHandler() {
setLevel(Level.FINEST);
setFilter(new ApiProxyLogFilter());
setFormatter(new CustomFormatter());
}
/**
* Reloads logging to pick up changes to the java.util.logging.config.file system property.
*/
private static void reloadLoggingProperties(LogManager logManager) {
if (System.getProperty(JAVA_UTIL_LOGGING_CONFIG_PROPERTY) == null) {
return;
}
try {
logManager.readConfiguration();
} catch (SecurityException | IOException e) {
System.err.println("Warning: caught exception when reading logging properties.");
System.err.println(e.getClass().getName() + ": " + e.getMessage());
}
}
/**
* Test-only init method to allow unit test to use a custom logManager.
*
* @param logManager
*/
static void init(LogManager logManager) {
reloadLoggingProperties(logManager);
for (Handler handler : ROOT_LOGGER.getHandlers()) {
if (handler instanceof VmRuntimeLogHandler) {
return; // Already installed.
}
}
ROOT_LOGGER.addHandler(new VmRuntimeLogHandler());
}
/**
* Initialize the {@code VmRuntimeLogHandler} by installing it on the root logger. After this call
* all log messages are forwarded to {@code ApiProxy.log(ApiProxy.LogRecord)} so they can be
* attached to the runtime response.
*/
public static void init() {
init(LogManager.getLogManager());
}
@Override
public void publish(LogRecord record) {
if (!isLoggable(record)) {
return;
}
// The formatter isn't necessarily thread-safe, so we synchronize around it.
String message;
synchronized (this) {
try {
message = getFormatter().format(record);
} catch (Exception ex) {
// We don't want to throw an exception here, but we
// report the exception to any registered ErrorManager.
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return;
}
}
VmApiProxyEnvironment environment = getThreadLocalEnvironment();
if (environment != null) {
environment.addLogRecord(convertLogRecord(record, message));
}
}
private ApiProxy.LogRecord convertLogRecord(LogRecord record, String message) {
ApiProxy.LogRecord.Level level = convertLogLevel(record.getLevel());
long timestamp = record.getMillis() * 1000;
return new ApiProxy.LogRecord(level, timestamp, message);
}
@Override
public void flush() {
VmApiProxyEnvironment environment = getThreadLocalEnvironment();
if (environment != null) {
environment.flushLogs();
}
}
@Override
public void close() {
flush();
}
private ApiProxy.LogRecord.Level convertLogLevel(Level level) {
long intLevel = level.intValue();
if (intLevel >= Level.SEVERE.intValue()) {
return ApiProxy.LogRecord.Level.error;
} else if (intLevel >= Level.WARNING.intValue()) {
return ApiProxy.LogRecord.Level.warn;
} else if (intLevel >= Level.INFO.intValue()) {
return ApiProxy.LogRecord.Level.info;
} else {
// There's no trace, so we'll map everything below this to
// debug.
return ApiProxy.LogRecord.Level.debug;
}
}
/**
* Returns the thread local environment if it is a VmApiProxyEnvironment.
*
* @return The ThreadLocal environment or null if no VmApiProxyEnvironment is set on this thread.
*/
private VmApiProxyEnvironment getThreadLocalEnvironment() {
Environment env = ApiProxy.getCurrentEnvironment();
if (env instanceof VmApiProxyEnvironment) {
return (VmApiProxyEnvironment) env;
}
return null;
}
/**
* Filter used to exclude error messages caused by log API calls.
*
* <p>Log messages related to the log system have an unfortunate tendency to trigger infinite
* loops. For example, we must drop messages from @code{VmApiProxyDelegate} if the message is due
* to logservice. Otherwise if the subsequent log API flush fails it will trigger some logging,
* which in turn will trigger a flush, which most likely will fail again causing some logging and
* so on.
*
* <p>The following logs are therefore not forwarded to the logging API:
* <ul>
* <li> Logs from {@code VmApiProxyDelegate} related to "logservice".</li>
* <li> Any logs from {@code VmAppLogsWriter}.</li>
* </ul>
* These logs will only show up in the log file on the VM.
*/
private static final class ApiProxyLogFilter implements Filter {
@Override
public boolean isLoggable(LogRecord record) {
if (VmApiProxyDelegate.class.getName().equals(record.getLoggerName())
&& record.getMessage() != null && record.getMessage().contains("logservice")) {
return false;
} else if (VmAppLogsWriter.class.getName().equals(record.getLoggerName())) {
return false;
}
return true;
}
}
private static final class CustomFormatter extends Formatter {
/**
* Format the given LogRecord.
* @param record the log record to be formatted.
* @return a formatted log record
*/
@Override
public synchronized String format(LogRecord record) {
StringBuffer sb = new StringBuffer();
if (record.getSourceClassName() != null) {
sb.append(record.getSourceClassName());
} else {
sb.append(record.getLoggerName());
}
if (record.getSourceMethodName() != null) {
sb.append(" ");
sb.append(record.getSourceMethodName());
}
sb.append(": ");
String message = formatMessage(record);
sb.append(message);
sb.append("\n");
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception ex) {
}
}
return sb.toString();
}
}
}