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    private static final char C = ',';
100    private static final int COMPRESSION_THRESHOLD = 1024;
101    private static final char Q = '\"';
102    private static final String QC = "\",";
103    private static final String QU = "\"_";
104    private static final long serialVersionUID = 1L;
105    private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
106
107    private final KeyValuePair[] additionalFields;
108    private final int compressionThreshold;
109    private final CompressionType compressionType;
110    private final String host;
111
112    public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
113            final int compressionThreshold) {
114        super(StandardCharsets.UTF_8);
115        this.host = host;
116        this.additionalFields = additionalFields;
117        this.compressionType = compressionType;
118        this.compressionThreshold = compressionThreshold;
119    }
120    
121    @PluginFactory
122    public static GelfLayout createLayout(
123            //@formatter:off
124            @PluginAttribute("host") final String host,
125            @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
126            @PluginAttribute(value = "compressionType",
127                defaultString = "GZIP") final CompressionType compressionType,
128            @PluginAttribute(value = "compressionThreshold",
129                defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
130            // @formatter:on
131        return new GelfLayout(host, additionalFields, compressionType, compressionThreshold);
132    }
133
134    /**
135     * http://en.wikipedia.org/wiki/Syslog#Severity_levels
136     */
137    static int formatLevel(final Level level) {
138        return Severity.getSeverity(level).getCode();
139    }
140
141    static String formatThrowable(final Throwable throwable) {
142        // stack traces are big enough to provide a reasonably large initial capacity here
143        final StringWriter sw = new StringWriter(2048);
144        final PrintWriter pw = new PrintWriter(sw);
145        throwable.printStackTrace(pw);
146        pw.flush();
147        return sw.toString();
148    }
149
150    static String formatTimestamp(final long timeMillis) {
151        return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
152    }
153
154    private byte[] compress(final byte[] bytes) {
155        try {
156            final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
157            try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
158                if (stream == null) {
159                    return bytes;
160                }
161                stream.write(bytes);
162                stream.finish();
163            }
164            return baos.toByteArray();
165        } catch (final IOException e) {
166            StatusLogger.getLogger().error(e);
167            return bytes;
168        }
169    }
170
171    @Override
172    public Map<String, String> getContentFormat() {
173        return Collections.emptyMap();
174    }
175
176    @Override
177    public String getContentType() {
178        return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
179    }
180
181    @Override
182    public byte[] toByteArray(final LogEvent event) {
183        final byte[] bytes = getBytes(toSerializable(event));
184        return bytes.length > compressionThreshold ? compress(bytes) : bytes;
185    }
186
187    @Override
188    public String toSerializable(final LogEvent event) {
189        final StringBuilder builder = getStringBuilder();
190        final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance();
191        builder.append('{');
192        builder.append("\"version\":\"1.1\",");
193        builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(host))).append(QC);
194        builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
195        builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
196        if (event.getThreadName() != null) {
197            builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC);
198        }
199        if (event.getLoggerName() != null) {
200            builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC);
201        }
202
203        for (final KeyValuePair additionalField : additionalFields) {
204            builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"")
205                    .append(jsonEncoder.quoteAsString(toNullSafeString(additionalField.getValue()))).append(QC);
206        }
207        for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
208            builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"")
209                    .append(jsonEncoder.quoteAsString(toNullSafeString(entry.getValue()))).append(QC);
210        }
211        if (event.getThrown() != null) {
212            builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown())))
213                    .append(QC);
214        }
215
216        builder.append("\"short_message\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(event.getMessage().getFormattedMessage())))
217                .append(Q);
218        builder.append('}');
219        return builder.toString();
220    }
221
222    private String toNullSafeString(final String s) {
223        return s == null ? Strings.EMPTY : s;
224    }
225}