/*
* 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.
*/
package com.google.apphosting.logging;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import com.google.gson.stream.JsonWriter;
/**
* A Formatter for use with java.util.logging that outputs LogRecords as JSON data with the
* properties that App Engine expects.
*/
public class JsonFormatter extends Formatter {
@Override
public String format(LogRecord record) {
Instant timestamp = Instant.ofEpochMilli(record.getMillis());
StringWriter out = new StringWriter();
// Write using a simple JsonWriter rather than the more sophisticated Gson as we generally
// will not need to serialize complex objects that require introspection and reflection.
try (JsonWriter writer = new JsonWriter(out)) {
writer.setSerializeNulls(false);
writer.setHtmlSafe(false);
writer.beginObject();
writer.name("timestamp")
.beginObject()
.name("seconds").value(timestamp.getEpochSecond())
.name("nanos").value(timestamp.getNano())
.endObject();
writer.name("severity").value(severity(record.getLevel()));
writer.name("thread").value(Thread.currentThread().getName());
writer.name("message").value(formatMessage(record));
// If there is a LogContext associated with this thread then add its properties.
LogContext logContext = LogContext.current();
if (logContext != null) {
logContext.forEach((name, value) -> {
try {
writer.name(name);
if (value == null) {
writer.nullValue();
} else if (value instanceof Boolean) {
writer.value((boolean) value);
} else if (value instanceof Number) {
writer.value((Number) value);
} else {
writer.value(value.toString());
}
} catch (IOException e) {
// Should not happen as StringWriter does not throw IOException
throw new AssertionError(e);
}
});
}
writer.endObject();
} catch (IOException e) {
// Should not happen as StringWriter does not throw IOException
throw new AssertionError(e);
}
out.append(System.lineSeparator());
return out.toString();
}
@Override
public synchronized String formatMessage(LogRecord record) {
StringBuilder sb = new StringBuilder();
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(": ");
sb.append(super.formatMessage(record));
Throwable thrown = record.getThrown();
if (thrown != null) {
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw);) {
sb.append("\n");
thrown.printStackTrace(pw);
}
sb.append(sw.getBuffer());
}
return sb.toString();
}
private static String severity(Level level) {
int intLevel = level.intValue();
if (intLevel >= Level.SEVERE.intValue()) {
return "ERROR";
} else if (intLevel >= Level.WARNING.intValue()) {
return "WARNING";
} else if (intLevel >= Level.INFO.intValue()) {
return "INFO";
} else {
// There's no trace, so we'll map everything below this to debug.
return "DEBUG";
}
}
}