001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.layout;
018
019import org.apache.logging.log4j.Level;
020import org.apache.logging.log4j.core.Layout;
021import org.apache.logging.log4j.core.LogEvent;
022import org.apache.logging.log4j.core.config.Node;
023import org.apache.logging.log4j.core.config.plugins.Plugin;
024import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
025import org.apache.logging.log4j.core.config.plugins.PluginElement;
026import org.apache.logging.log4j.core.config.plugins.PluginFactory;
027import org.apache.logging.log4j.core.net.Severity;
028import org.apache.logging.log4j.core.util.JsonUtils;
029import org.apache.logging.log4j.core.util.KeyValuePair;
030import org.apache.logging.log4j.message.Message;
031import org.apache.logging.log4j.status.StatusLogger;
032import org.apache.logging.log4j.util.StringBuilderFormattable;
033import org.apache.logging.log4j.util.Strings;
034
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.OutputStream;
038import java.io.PrintWriter;
039import java.io.StringWriter;
040import java.nio.charset.StandardCharsets;
041import java.util.Collections;
042import java.util.Map;
043import java.util.zip.DeflaterOutputStream;
044import java.util.zip.GZIPOutputStream;
045
046/**
047 * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
048 * <p>
049 * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
050 * log event data is larger than 1024 bytes (the {@code compressionThreshold}).
051 * This layout does not implement chunking.
052 * </p>
053 * <p>
054 * Configure as follows to send to a Graylog2 server:
055 * </p>
056 *
057 * <pre>
058 * &lt;Appenders&gt;
059 *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
060 *            &lt;GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"&gt;
061 *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
062 *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
063 *            &lt;/GelfLayout&gt;
064 *        &lt;/Socket&gt;
065 * &lt;/Appenders&gt;
066 * </pre>
067 *
068 * @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
069 */
070@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
071public final class GelfLayout extends AbstractStringLayout {
072
073    public enum CompressionType {
074
075        GZIP {
076            @Override
077            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
078                return new GZIPOutputStream(os);
079            }
080        },
081        ZLIB {
082            @Override
083            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
084                return new DeflaterOutputStream(os);
085            }
086        },
087        OFF {
088            @Override
089            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
090                return null;
091            }
092        };
093
094        public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
095    }
096
097    private static final char C = ',';
098    private static final int COMPRESSION_THRESHOLD = 1024;
099    private static final char Q = '\"';
100    private static final String QC = "\",";
101    private static final String QU = "\"_";
102
103    private final KeyValuePair[] additionalFields;
104    private final int compressionThreshold;
105    private final CompressionType compressionType;
106    private final String host;
107    private final boolean includeStacktrace;
108
109    public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
110                      final int compressionThreshold, boolean includeStacktrace) {
111        super(StandardCharsets.UTF_8);
112        this.host = host;
113        this.additionalFields = additionalFields;
114        this.compressionType = compressionType;
115        this.compressionThreshold = compressionThreshold;
116        this.includeStacktrace = includeStacktrace;
117    }
118    
119    @PluginFactory
120    public static GelfLayout createLayout(
121            //@formatter:off
122            @PluginAttribute("host") final String host,
123            @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
124            @PluginAttribute(value = "compressionType",
125                defaultString = "GZIP") final CompressionType compressionType,
126            @PluginAttribute(value = "compressionThreshold",
127                defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
128            @PluginAttribute(value = "includeStacktrace",
129                defaultBoolean = true) final boolean includeStacktrace) {
130            // @formatter:on
131        return new GelfLayout(host, additionalFields, compressionType, compressionThreshold, includeStacktrace);
132    }
133
134    @Override
135    public Map<String, String> getContentFormat() {
136        return Collections.emptyMap();
137    }
138
139    @Override
140    public String getContentType() {
141        return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
142    }
143
144    @Override
145    public byte[] toByteArray(final LogEvent event) {
146        StringBuilder text = toText(event, getStringBuilder(), false);
147        final byte[] bytes = getBytes(text.toString());
148        return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
149    }
150
151    @Override
152    public void encode(final LogEvent event, final ByteBufferDestination destination) {
153        if (compressionType != CompressionType.OFF) {
154            super.encode(event, destination);
155            return;
156        }
157        final StringBuilder text = toText(event, getStringBuilder(), true);
158        final Encoder<StringBuilder> helper = getStringBuilderEncoder();
159        helper.encode(text, destination);
160    }
161
162    private byte[] compress(final byte[] bytes) {
163        try {
164            final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
165            try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
166                if (stream == null) {
167                    return bytes;
168                }
169                stream.write(bytes);
170                stream.finish();
171            }
172            return baos.toByteArray();
173        } catch (final IOException e) {
174            StatusLogger.getLogger().error(e);
175            return bytes;
176        }
177    }
178
179    @Override
180    public String toSerializable(final LogEvent event) {
181        final StringBuilder text = toText(event, getStringBuilder(), false);
182        return text.toString();
183    }
184
185    private StringBuilder toText(LogEvent event, StringBuilder builder, boolean gcFree) {
186        builder.append('{');
187        builder.append("\"version\":\"1.1\",");
188        builder.append("\"host\":\"");
189        JsonUtils.quoteAsString(toNullSafeString(host), builder);
190        builder.append(QC);
191        builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
192        builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
193        if (event.getThreadName() != null) {
194            builder.append("\"_thread\":\"");
195            JsonUtils.quoteAsString(event.getThreadName(), builder);
196            builder.append(QC);
197        }
198        if (event.getLoggerName() != null) {
199            builder.append("\"_logger\":\"");
200            JsonUtils.quoteAsString(event.getLoggerName(), builder);
201            builder.append(QC);
202        }
203
204        for (final KeyValuePair additionalField : additionalFields) {
205            builder.append(QU);
206            JsonUtils.quoteAsString(additionalField.getKey(), builder);
207            builder.append("\":\"");
208            JsonUtils.quoteAsString(toNullSafeString(additionalField.getValue()), builder);
209            builder.append(QC);
210        }
211        for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
212            builder.append(QU);
213            JsonUtils.quoteAsString(entry.getKey(), builder);
214            builder.append("\":\"");
215            JsonUtils.quoteAsString(toNullSafeString(entry.getValue()), builder);
216            builder.append(QC);
217        }
218        if (event.getThrown() != null) {
219            builder.append("\"full_message\":\"");
220            if (includeStacktrace) {
221                JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
222            } else {
223                JsonUtils.quoteAsString(event.getThrown().toString(), builder);
224            }
225            builder.append(QC);
226        }
227
228        builder.append("\"short_message\":\"");
229        Message message = event.getMessage();
230        if (message instanceof CharSequence) {
231            JsonUtils.quoteAsString(((CharSequence)message), builder);
232        } else if (gcFree && message instanceof StringBuilderFormattable) {
233            StringBuilder messageBuffer = getMessageStringBuilder();
234            ((StringBuilderFormattable)message).formatTo(messageBuffer);
235            JsonUtils.quoteAsString(messageBuffer, builder);
236        } else {
237            JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
238        }
239        builder.append(Q);
240        builder.append('}');
241        return builder;
242    }
243
244    private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
245
246    private static StringBuilder getMessageStringBuilder() {
247        StringBuilder result = messageStringBuilder.get();
248        if (result == null) {
249            result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
250            messageStringBuilder.set(result);
251        }
252        result.setLength(0);
253        return result;
254    }
255
256    private CharSequence toNullSafeString(final CharSequence s) {
257        return s == null ? Strings.EMPTY : s;
258    }
259
260    /**
261     * Non-private to make it accessible from unit test.
262     */
263    static CharSequence formatTimestamp(final long timeMillis) {
264        if (timeMillis < 1000) {
265            return "0";
266        }
267        StringBuilder builder = getTimestampStringBuilder();
268        builder.append(timeMillis);
269        builder.insert(builder.length() - 3, '.');
270        return builder;
271    }
272
273    private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
274
275    private static StringBuilder getTimestampStringBuilder() {
276        StringBuilder result = timestampStringBuilder.get();
277        if (result == null) {
278            result = new StringBuilder(20);
279            timestampStringBuilder.set(result);
280        }
281        result.setLength(0);
282        return result;
283    }
284
285    /**
286     * http://en.wikipedia.org/wiki/Syslog#Severity_levels
287     */
288    private int formatLevel(final Level level) {
289        return Severity.getSeverity(level).getCode();
290    }
291
292    /**
293     * Non-private to make it accessible from unit test.
294     */
295    static CharSequence formatThrowable(final Throwable throwable) {
296        // stack traces are big enough to provide a reasonably large initial capacity here
297        final StringWriter sw = new StringWriter(2048);
298        final PrintWriter pw = new PrintWriter(sw);
299        throwable.printStackTrace(pw);
300        pw.flush();
301        return sw.getBuffer();
302    }
303}