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    }