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 java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.io.PrintWriter;
023import java.io.StringWriter;
024import java.math.BigDecimal;
025import java.nio.charset.StandardCharsets;
026import java.util.Collections;
027import java.util.Map;
028import java.util.zip.DeflaterOutputStream;
029import java.util.zip.GZIPOutputStream;
030
031import org.apache.logging.log4j.Level;
032import org.apache.logging.log4j.core.Layout;
033import org.apache.logging.log4j.core.LogEvent;
034import org.apache.logging.log4j.core.config.Node;
035import org.apache.logging.log4j.core.config.plugins.Plugin;
036import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
037import org.apache.logging.log4j.core.config.plugins.PluginElement;
038import org.apache.logging.log4j.core.config.plugins.PluginFactory;
039import org.apache.logging.log4j.core.net.Severity;
040import org.apache.logging.log4j.core.util.KeyValuePair;
041import org.apache.logging.log4j.status.StatusLogger;
042import org.apache.logging.log4j.util.Strings;
043
044import com.fasterxml.jackson.core.io.JsonStringEncoder;
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://graylog2.org/gelf">GELF home page</a>
069 * @see <a href="http://graylog2.org/resources/gelf/specification">GELF
070 *      specification</a>
071 */
072@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
073public final class GelfLayout extends AbstractStringLayout {
074
075    public static enum CompressionType {
076
077        GZIP {
078            @Override
079            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
080                return new GZIPOutputStream(os);
081            }
082        },
083        ZLIB {
084            @Override
085            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
086                return new DeflaterOutputStream(os);
087            }
088        },
089        OFF {
090            @Override
091            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
092                return null;
093            }
094        };
095
096        public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
097
098    }
099
100    private static final char C = ',';
101    private static final int COMPRESSION_THRESHOLD = 1024;
102    private static final char Q = '\"';
103    private static final String QC = "\",";
104    private static final String QU = "\"_";
105    private static final long serialVersionUID = 1L;
106    private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
107
108    @PluginFactory
109    public static GelfLayout createLayout(
110            //@formatter:off
111            @PluginAttribute("host") final String host,
112            @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
113            @PluginAttribute(value = "compressionType",
114                defaultString = "GZIP") final CompressionType compressionType,
115            @PluginAttribute(value = "compressionThreshold",
116                defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
117            // @formatter:on
118        return new GelfLayout(host, additionalFields, compressionType, compressionThreshold);
119    }
120
121    /**
122     * http://en.wikipedia.org/wiki/Syslog#Severity_levels
123     */
124    static int formatLevel(final Level level) {
125        return Severity.getSeverity(level).getCode();
126    }
127
128    static String formatThrowable(final Throwable throwable) {
129        // stack traces are big enough to provide a reasonably large initial capacity here
130        final StringWriter sw = new StringWriter(2048);
131        final PrintWriter pw = new PrintWriter(sw);
132        throwable.printStackTrace(pw);
133        pw.flush();
134        return sw.toString();
135    }
136
137    static String formatTimestamp(final long timeMillis) {
138        return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
139    }
140
141    private final KeyValuePair[] additionalFields;
142
143    private final int compressionThreshold;
144
145    private final CompressionType compressionType;
146
147    private final String host;
148
149    public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
150            final int compressionThreshold) {
151        super(StandardCharsets.UTF_8);
152        this.host = host;
153        this.additionalFields = additionalFields;
154        this.compressionType = compressionType;
155        this.compressionThreshold = compressionThreshold;
156    }
157
158    private byte[] compress(final byte[] bytes) {
159        try {
160            final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
161            try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
162                if (stream == null) {
163                    return bytes;
164                }
165                stream.write(bytes);
166                stream.finish();
167            }
168            return baos.toByteArray();
169        } catch (final IOException e) {
170            StatusLogger.getLogger().error(e);
171            return bytes;
172        }
173    }
174
175    @Override
176    public Map<String, String> getContentFormat() {
177        return Collections.emptyMap();
178    }
179
180    @Override
181    public String getContentType() {
182        return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
183    }
184
185    @Override
186    public byte[] toByteArray(final LogEvent event) {
187        final byte[] bytes = getBytes(toSerializable(event));
188        return bytes.length > compressionThreshold ? compress(bytes) : bytes;
189    }
190
191    @Override
192    public String toSerializable(final LogEvent event) {
193        final StringBuilder builder = new StringBuilder(256);
194        final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance();
195        builder.append('{');
196        builder.append("\"version\":\"1.1\",");
197        builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(host))).append(QC);
198        builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
199        builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
200        if (event.getThreadName() != null) {
201            builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC);
202        }
203        if (event.getLoggerName() != null) {
204            builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC);
205        }
206
207        for (final KeyValuePair additionalField : additionalFields) {
208            builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"")
209                    .append(jsonEncoder.quoteAsString(toNullSafeString(additionalField.getValue()))).append(QC);
210        }
211        for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
212            builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"")
213                    .append(jsonEncoder.quoteAsString(toNullSafeString(entry.getValue()))).append(QC);
214        }
215        if (event.getThrown() != null) {
216            builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown())))
217                    .append(QC);
218        }
219
220        builder.append("\"short_message\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(event.getMessage().getFormattedMessage())))
221                .append(Q);
222        builder.append('}');
223        return builder.toString();
224    }
225
226    private String toNullSafeString(final String s) {
227        return s == null ? Strings.EMPTY : s;
228    }
229}