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.util.Collections;
026import java.util.Map;
027import java.util.zip.DeflaterOutputStream;
028import java.util.zip.GZIPOutputStream;
029
030import org.apache.logging.log4j.Level;
031import org.apache.logging.log4j.core.Layout;
032import org.apache.logging.log4j.core.LogEvent;
033import org.apache.logging.log4j.core.config.Node;
034import org.apache.logging.log4j.core.config.plugins.Plugin;
035import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
036import org.apache.logging.log4j.core.config.plugins.PluginElement;
037import org.apache.logging.log4j.core.config.plugins.PluginFactory;
038import org.apache.logging.log4j.core.net.Severity;
039import org.apache.logging.log4j.core.util.Constants;
040import org.apache.logging.log4j.core.util.KeyValuePair;
041import org.apache.logging.log4j.status.StatusLogger;
042
043import com.fasterxml.jackson.core.io.JsonStringEncoder;
044
045/**
046 * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
047 * <p>
048 * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if log event data is larger than 1024 bytes
049 * (the {@code compressionThreshold}). This layout does not implement chunking.
050 * </p>
051 * <p>
052 * Configure as follows to send to a Graylog2 server:
053 * </p>
054 *
055 * <pre>
056 * &lt;Appenders&gt;
057 *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
058 *            &lt;GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"&gt;
059 *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
060 *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
061 *            &lt;/GelfLayout&gt;
062 *        &lt;/Socket&gt;
063 * &lt;/Appenders&gt;
064 * </pre>
065 *
066 * @see <a href="http://graylog2.org/gelf">GELF home page</a>
067 * @see <a href="http://graylog2.org/resources/gelf/specification">GELF specification</a>
068 */
069@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
070public final class GelfLayout extends AbstractStringLayout {
071
072    public static enum CompressionType {
073
074        GZIP {
075            @Override
076            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
077                return new GZIPOutputStream(os);
078            }
079        },
080        ZLIB {
081            @Override
082            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
083                return new DeflaterOutputStream(os);
084            }
085        },
086        OFF {
087            @Override
088            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
089                return null;
090            }
091        };
092
093        public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
094
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    private static final long serialVersionUID = 1L;
103    private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
104
105    @PluginFactory
106    public static GelfLayout createLayout(
107            //@formatter:off
108            @PluginAttribute("host") final String host,
109            @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
110            @PluginAttribute(value = "compressionType",
111                defaultString = "GZIP") final CompressionType compressionType,
112            @PluginAttribute(value = "compressionThreshold",
113                defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
114            // @formatter:on
115        return new GelfLayout(host, additionalFields, compressionType, compressionThreshold);
116    }
117
118    /**
119     * http://en.wikipedia.org/wiki/Syslog#Severity_levels
120     */
121    static int formatLevel(final Level level) {
122        return Severity.getSeverity(level).getCode();
123    }
124
125    static String formatThrowable(final Throwable throwable) {
126        // stack traces are big enough to provide a reasonably large initial capacity here
127        final StringWriter sw = new StringWriter(2048);
128        final PrintWriter pw = new PrintWriter(sw);
129        throwable.printStackTrace(pw);
130        pw.flush();
131        return sw.toString();
132    }
133
134    static String formatTimestamp(final long timeMillis) {
135        return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
136    }
137
138    private final KeyValuePair[] additionalFields;
139
140    private final int compressionThreshold;
141
142    private final CompressionType compressionType;
143
144    private final String host;
145
146    public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
147            final int compressionThreshold) {
148        super(Constants.UTF_8);
149        this.host = host;
150        this.additionalFields = additionalFields;
151        this.compressionType = compressionType;
152        this.compressionThreshold = compressionThreshold;
153    }
154
155    private byte[] compress(final byte[] bytes) {
156        try {
157            final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
158            final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos);
159            if (stream == null) {
160                return bytes;
161            }
162            stream.write(bytes);
163            stream.finish();
164            stream.close();
165            return baos.toByteArray();
166        } catch (final IOException e) {
167            StatusLogger.getLogger().error(e);
168            return bytes;
169        }
170    }
171
172    @Override
173    public Map<String, String> getContentFormat() {
174        return Collections.emptyMap();
175    }
176
177    @Override
178    public String getContentType() {
179        return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
180    }
181
182    @Override
183    public byte[] toByteArray(final LogEvent event) {
184        final byte[] bytes = getBytes(toSerializable(event));
185        return bytes.length > compressionThreshold ? compress(bytes) : bytes;
186    }
187
188    @Override
189    public String toSerializable(final LogEvent event) {
190        final StringBuilder builder = new StringBuilder(256);
191        final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance();
192        builder.append('{');
193        builder.append("\"version\":\"1.1\",");
194        builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(host)).append(QC);
195        builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
196        builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
197        if (event.getThreadName() != null) {
198            builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC);
199        }
200        if (event.getLoggerName() != null) {
201            builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC);
202        }
203
204        for (final KeyValuePair additionalField : additionalFields) {
205            builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"")
206                    .append(jsonEncoder.quoteAsString(additionalField.getValue())).append(QC);
207        }
208        for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
209            builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"")
210                    .append(jsonEncoder.quoteAsString(entry.getValue())).append(QC);
211        }
212        if (event.getThrown() != null) {
213            builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown())))
214                    .append(QC);
215        }
216
217        builder.append("\"short_message\":\"")
218                .append(jsonEncoder.quoteAsString(event.getMessage().getFormattedMessage())).append(Q);
219        builder.append('}');
220        return builder.toString();
221    }
222}