blob: 268ec6af43271c86e83c297976747474ba59473b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.logging.log4j.core.layout;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.layout.internal.ExcludeChecker;
import org.apache.logging.log4j.core.layout.internal.IncludeChecker;
import org.apache.logging.log4j.core.layout.internal.ListChecker;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.net.Severity;
import org.apache.logging.log4j.core.util.JsonUtils;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.core.util.NetUtils;
import org.apache.logging.log4j.core.util.Patterns;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.plugins.Node;
import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.plugins.PluginElement;
import org.apache.logging.log4j.plugins.PluginFactory;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.apache.logging.log4j.util.Strings;
import org.apache.logging.log4j.util.TriConsumer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
/**
* Lays out events in the Graylog Extended Log Format (GELF) 1.1.
* <p>
* This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
* log event data is larger than 1024 bytes (the {@code compressionThreshold}).
* This layout does not implement chunking.
* </p>
*
* @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
*/
@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public final class GelfLayout extends AbstractStringLayout {
public enum CompressionType {
GZIP {
@Override
public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
return new GZIPOutputStream(os);
}
},
ZLIB {
@Override
public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
return new DeflaterOutputStream(os);
}
},
OFF {
@Override
public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
return null;
}
};
public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
}
private static final char C = ',';
private static final int COMPRESSION_THRESHOLD = 1024;
private static final char Q = '\"';
private static final String QC = "\",";
private static final String QU = "\"_";
private final KeyValuePair[] additionalFields;
private final int compressionThreshold;
private final CompressionType compressionType;
private final String host;
private final boolean includeStacktrace;
private final boolean includeThreadContext;
private final boolean includeNullDelimiter;
private final PatternLayout layout;
private final FieldWriter fieldWriter;
public static class Builder<B extends Builder<B>> extends AbstractStringLayout.Builder<B>
implements org.apache.logging.log4j.plugins.util.Builder<GelfLayout> {
@PluginBuilderAttribute
private String host;
@PluginElement("AdditionalField")
private KeyValuePair[] additionalFields;
@PluginBuilderAttribute
private CompressionType compressionType = CompressionType.GZIP;
@PluginBuilderAttribute
private int compressionThreshold = COMPRESSION_THRESHOLD;
@PluginBuilderAttribute
private boolean includeStacktrace = true;
@PluginBuilderAttribute
private boolean includeThreadContext = true;
@PluginBuilderAttribute
private boolean includeNullDelimiter = false;
@PluginBuilderAttribute
private String threadContextIncludes = null;
@PluginBuilderAttribute
private String threadContextExcludes = null;
@PluginBuilderAttribute
private String messagePattern = null;
public Builder() {
super();
setCharset(StandardCharsets.UTF_8);
}
@Override
public GelfLayout build() {
ListChecker checker = null;
if (threadContextExcludes != null) {
final String[] array = threadContextExcludes.split(Patterns.COMMA_SEPARATOR);
if (array.length > 0) {
List<String> excludes = new ArrayList<>(array.length);
for (final String str : array) {
excludes.add(str.trim());
}
checker = new ExcludeChecker(excludes);
}
}
if (threadContextIncludes != null) {
final String[] array = threadContextIncludes.split(Patterns.COMMA_SEPARATOR);
if (array.length > 0) {
List<String> includes = new ArrayList<>(array.length);
for (final String str : array) {
includes.add(str.trim());
}
checker = new IncludeChecker(includes);
}
}
if (checker == null) {
checker = ListChecker.NOOP_CHECKER;
}
PatternLayout patternLayout = null;
if (messagePattern != null) {
patternLayout = PatternLayout.newBuilder().setPattern(messagePattern)
.setAlwaysWriteExceptions(includeStacktrace)
.setConfiguration(getConfiguration())
.build();
}
return new GelfLayout(getConfiguration(), host, additionalFields, compressionType, compressionThreshold,
includeStacktrace, includeThreadContext, includeNullDelimiter, checker, patternLayout);
}
public String getHost() {
return host;
}
public CompressionType getCompressionType() {
return compressionType;
}
public int getCompressionThreshold() {
return compressionThreshold;
}
public boolean isIncludeStacktrace() {
return includeStacktrace;
}
public boolean isIncludeThreadContext() {
return includeThreadContext;
}
public boolean isIncludeNullDelimiter() { return includeNullDelimiter; }
public KeyValuePair[] getAdditionalFields() {
return additionalFields;
}
/**
* The value of the <code>host</code> property (optional, defaults to local host name).
*
* @return this builder
*/
public B setHost(final String host) {
this.host = host;
return asBuilder();
}
/**
* Compression to use (optional, defaults to GZIP).
*
* @return this builder
*/
public B setCompressionType(final CompressionType compressionType) {
this.compressionType = compressionType;
return asBuilder();
}
/**
* Compress if data is larger than this number of bytes (optional, defaults to 1024).
*
* @return this builder
*/
public B setCompressionThreshold(final int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
return asBuilder();
}
/**
* Whether to include full stacktrace of logged Throwables (optional, default to true).
* If set to false, only the class name and message of the Throwable will be included.
*
* @return this builder
*/
public B setIncludeStacktrace(final boolean includeStacktrace) {
this.includeStacktrace = includeStacktrace;
return asBuilder();
}
/**
* Whether to include thread context as additional fields (optional, default to true).
*
* @return this builder
*/
public B setIncludeThreadContext(final boolean includeThreadContext) {
this.includeThreadContext = includeThreadContext;
return asBuilder();
}
/**
* Whether to include NULL byte as delimiter after each event (optional, default to false).
* Useful for Graylog GELF TCP input.
*
* @return this builder
*/
public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
this.includeNullDelimiter = includeNullDelimiter;
return asBuilder();
}
/**
* Additional fields to set on each log event.
*
* @return this builder
*/
public B setAdditionalFields(final KeyValuePair[] additionalFields) {
this.additionalFields = additionalFields;
return asBuilder();
}
/**
* The pattern to use to format the message.
* @param pattern the pattern string.
* @return this builder
*/
public B setMessagePattern(final String pattern) {
this.messagePattern = pattern;
return asBuilder();
}
/**
* A comma separated list of thread context keys to include;
* @param mdcIncludes the list of keys.
* @return this builder
*/
public B setMdcIncludes(final String mdcIncludes) {
this.threadContextIncludes = mdcIncludes;
return asBuilder();
}
/**
* A comma separated list of thread context keys to include;
* @param mdcExcludes the list of keys.
* @return this builder
*/
public B setMdcExcludes(final String mdcExcludes) {
this.threadContextExcludes = mdcExcludes;
return asBuilder();
}
}
private GelfLayout(final Configuration config, final String host, final KeyValuePair[] additionalFields,
final CompressionType compressionType, final int compressionThreshold, final boolean includeStacktrace,
final boolean includeThreadContext, final boolean includeNullDelimiter, final ListChecker listChecker,
final PatternLayout patternLayout) {
super(config, StandardCharsets.UTF_8, null, null);
this.host = host != null ? host : NetUtils.getLocalHostname();
this.additionalFields = additionalFields != null ? additionalFields : new KeyValuePair[0];
if (config == null) {
for (final KeyValuePair additionalField : this.additionalFields) {
if (valueNeedsLookup(additionalField.getValue())) {
throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables");
}
}
}
this.compressionType = compressionType;
this.compressionThreshold = compressionThreshold;
this.includeStacktrace = includeStacktrace;
this.includeThreadContext = includeThreadContext;
this.includeNullDelimiter = includeNullDelimiter;
if (includeNullDelimiter && compressionType != CompressionType.OFF) {
throw new IllegalArgumentException("null delimiter cannot be used with compression");
}
this.fieldWriter = new FieldWriter(listChecker);
this.layout = patternLayout;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("host=").append(host);
sb.append(", compressionType=").append(compressionType.toString());
sb.append(", compressionThreshold=").append(compressionThreshold);
sb.append(", includeStackTrace=").append(includeStacktrace);
sb.append(", includeThreadContext=").append(includeThreadContext);
sb.append(", includeNullDelimiter=").append(includeNullDelimiter);
String threadVars = fieldWriter.getChecker().toString();
if (threadVars.length() > 0) {
sb.append(", ").append(threadVars);
}
if (layout != null) {
sb.append(", PatternLayout{").append(layout.toString()).append("}");
}
return sb.toString();
}
@PluginFactory
public static <B extends Builder<B>> B newBuilder() {
return new Builder<B>().asBuilder();
}
@Override
public Map<String, String> getContentFormat() {
return Collections.emptyMap();
}
@Override
public String getContentType() {
return "application/json; charset=" + this.getCharset();
}
@Override
public byte[] toByteArray(final LogEvent event) {
final StringBuilder text = toText(event, getStringBuilder(), false);
final byte[] bytes = getBytes(text.toString());
return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
}
@Override
public void encode(final LogEvent event, final ByteBufferDestination destination) {
if (compressionType != CompressionType.OFF) {
super.encode(event, destination);
return;
}
final StringBuilder text = toText(event, getStringBuilder(), true);
final Encoder<StringBuilder> helper = getStringBuilderEncoder();
helper.encode(text, destination);
}
private byte[] compress(final byte[] bytes) {
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
if (stream == null) {
return bytes;
}
stream.write(bytes);
stream.finish();
}
return baos.toByteArray();
} catch (final IOException e) {
StatusLogger.getLogger().error(e);
return bytes;
}
}
@Override
public String toSerializable(final LogEvent event) {
final StringBuilder text = toText(event, getStringBuilder(), false);
return text.toString();
}
private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
builder.append('{');
builder.append("\"version\":\"1.1\",");
builder.append("\"host\":\"");
JsonUtils.quoteAsString(toNullSafeString(host), builder);
builder.append(QC);
builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
if (event.getThreadName() != null) {
builder.append("\"_thread\":\"");
JsonUtils.quoteAsString(event.getThreadName(), builder);
builder.append(QC);
}
if (event.getLoggerName() != null) {
builder.append("\"_logger\":\"");
JsonUtils.quoteAsString(event.getLoggerName(), builder);
builder.append(QC);
}
if (additionalFields.length > 0) {
final StrSubstitutor strSubstitutor = getConfiguration().getStrSubstitutor();
for (final KeyValuePair additionalField : additionalFields) {
builder.append(QU);
JsonUtils.quoteAsString(additionalField.getKey(), builder);
builder.append("\":\"");
final String value = valueNeedsLookup(additionalField.getValue())
? strSubstitutor.replace(event, additionalField.getValue())
: additionalField.getValue();
JsonUtils.quoteAsString(toNullSafeString(value), builder);
builder.append(QC);
}
}
if (includeThreadContext) {
event.getContextData().forEach(fieldWriter, builder);
}
if (event.getThrown() != null || layout != null) {
builder.append("\"full_message\":\"");
if (layout != null) {
final StringBuilder messageBuffer = getMessageStringBuilder();
layout.serialize(event, messageBuffer);
JsonUtils.quoteAsString(messageBuffer, builder);
} else {
if (includeStacktrace) {
JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
} else {
JsonUtils.quoteAsString(event.getThrown().toString(), builder);
}
}
builder.append(QC);
}
builder.append("\"short_message\":\"");
final Message message = event.getMessage();
if (message instanceof CharSequence) {
JsonUtils.quoteAsString(((CharSequence) message), builder);
} else if (gcFree && message instanceof StringBuilderFormattable) {
final StringBuilder messageBuffer = getMessageStringBuilder();
try {
((StringBuilderFormattable) message).formatTo(messageBuffer);
JsonUtils.quoteAsString(messageBuffer, builder);
} finally {
trimToMaxSize(messageBuffer);
}
} else {
JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
}
builder.append(Q);
builder.append('}');
if (includeNullDelimiter) {
builder.append('\0');
}
return builder;
}
private static boolean valueNeedsLookup(final String value) {
return value != null && value.contains("${");
}
private static class FieldWriter implements TriConsumer<String, Object, StringBuilder> {
private final ListChecker checker;
FieldWriter(ListChecker checker) {
this.checker = checker;
}
@Override
public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
if (checker.check(key)) {
stringBuilder.append(QU);
JsonUtils.quoteAsString(key, stringBuilder);
stringBuilder.append("\":\"");
JsonUtils.quoteAsString(toNullSafeString(String.valueOf(value)), stringBuilder);
stringBuilder.append(QC);
}
}
public ListChecker getChecker() {
return checker;
}
};
private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
private static StringBuilder getMessageStringBuilder() {
StringBuilder result = messageStringBuilder.get();
if (result == null) {
result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
messageStringBuilder.set(result);
}
result.setLength(0);
return result;
}
private static CharSequence toNullSafeString(final CharSequence s) {
return s == null ? Strings.EMPTY : s;
}
/**
* Non-private to make it accessible from unit test.
*/
static CharSequence formatTimestamp(final long timeMillis) {
if (timeMillis < 1000) {
return "0";
}
final StringBuilder builder = getTimestampStringBuilder();
builder.append(timeMillis);
builder.insert(builder.length() - 3, '.');
return builder;
}
private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
private static StringBuilder getTimestampStringBuilder() {
StringBuilder result = timestampStringBuilder.get();
if (result == null) {
result = new StringBuilder(20);
timestampStringBuilder.set(result);
}
result.setLength(0);
return result;
}
/**
* http://en.wikipedia.org/wiki/Syslog#Severity_levels
*/
private int formatLevel(final Level level) {
return Severity.getSeverity(level).getCode();
}
/**
* Non-private to make it accessible from unit test.
*/
static CharSequence formatThrowable(final Throwable throwable) {
// stack traces are big enough to provide a reasonably large initial capacity here
final StringWriter sw = new StringWriter(2048);
final PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
pw.flush();
return sw.getBuffer();
}
}