    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     */
    017    package org.apache.logging.log4j.core.layout;
    018
    019    import java.io.ByteArrayOutputStream;
    020    import java.io.IOException;
    021    import java.io.OutputStream;
    022    import java.io.PrintWriter;
    023    import java.io.StringWriter;
    024    import java.math.BigDecimal;
    025    import java.util.Collections;
    026    import java.util.Map;
    027    import java.util.zip.DeflaterOutputStream;
    028    import java.util.zip.GZIPOutputStream;
    029
    030    import org.apache.logging.log4j.Level;
    031    import org.apache.logging.log4j.core.Layout;
    032    import org.apache.logging.log4j.core.LogEvent;
    033    import org.apache.logging.log4j.core.config.Node;
    034    import org.apache.logging.log4j.core.config.plugins.Plugin;
    035    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    036    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    037    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    038    import org.apache.logging.log4j.core.net.Severity;
    039    import org.apache.logging.log4j.core.util.Constants;
    040    import org.apache.logging.log4j.core.util.KeyValuePair;
    041    import org.apache.logging.log4j.status.StatusLogger;
    042
    043    import 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)
    070    public 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    }