View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.PrintWriter;
23  import java.io.StringWriter;
24  import java.math.BigDecimal;
25  import java.nio.charset.StandardCharsets;
26  import java.util.Collections;
27  import java.util.Map;
28  import java.util.zip.DeflaterOutputStream;
29  import java.util.zip.GZIPOutputStream;
30  
31  import org.apache.logging.log4j.Level;
32  import org.apache.logging.log4j.core.Layout;
33  import org.apache.logging.log4j.core.LogEvent;
34  import org.apache.logging.log4j.core.config.Node;
35  import org.apache.logging.log4j.core.config.plugins.Plugin;
36  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
37  import org.apache.logging.log4j.core.config.plugins.PluginElement;
38  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
39  import org.apache.logging.log4j.core.net.Severity;
40  import org.apache.logging.log4j.core.util.KeyValuePair;
41  import org.apache.logging.log4j.status.StatusLogger;
42  import org.apache.logging.log4j.util.Strings;
43  
44  import com.fasterxml.jackson.core.io.JsonStringEncoder;
45  
46  /**
47   * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
48   * <p>
49   * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
50   * log event data is larger than 1024 bytes (the {@code compressionThreshold}).
51   * This layout does not implement chunking.
52   * </p>
53   * <p>
54   * Configure as follows to send to a Graylog2 server:
55   * </p>
56   *
57   * <pre>
58   * &lt;Appenders&gt;
59   *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
60   *            &lt;GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"&gt;
61   *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
62   *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
63   *            &lt;/GelfLayout&gt;
64   *        &lt;/Socket&gt;
65   * &lt;/Appenders&gt;
66   * </pre>
67   *
68   * @see <a href="http://graylog2.org/gelf">GELF home page</a>
69   * @see <a href="http://graylog2.org/resources/gelf/specification">GELF
70   *      specification</a>
71   */
72  @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
73  public final class GelfLayout extends AbstractStringLayout {
74  
75      public static enum CompressionType {
76  
77          GZIP {
78              @Override
79              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
80                  return new GZIPOutputStream(os);
81              }
82          },
83          ZLIB {
84              @Override
85              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
86                  return new DeflaterOutputStream(os);
87              }
88          },
89          OFF {
90              @Override
91              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
92                  return null;
93              }
94          };
95  
96          public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
97      }
98  
99      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 }