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 org.apache.logging.log4j.Level;
20  import org.apache.logging.log4j.core.Layout;
21  import org.apache.logging.log4j.core.LogEvent;
22  import org.apache.logging.log4j.core.config.Node;
23  import org.apache.logging.log4j.core.config.plugins.Plugin;
24  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
25  import org.apache.logging.log4j.core.config.plugins.PluginElement;
26  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
27  import org.apache.logging.log4j.core.net.Severity;
28  import org.apache.logging.log4j.core.util.JsonUtils;
29  import org.apache.logging.log4j.core.util.KeyValuePair;
30  import org.apache.logging.log4j.message.Message;
31  import org.apache.logging.log4j.status.StatusLogger;
32  import org.apache.logging.log4j.util.StringBuilderFormattable;
33  import org.apache.logging.log4j.util.Strings;
34  
35  import java.io.ByteArrayOutputStream;
36  import java.io.IOException;
37  import java.io.OutputStream;
38  import java.io.PrintWriter;
39  import java.io.StringWriter;
40  import java.nio.charset.StandardCharsets;
41  import java.util.Collections;
42  import java.util.Map;
43  import java.util.zip.DeflaterOutputStream;
44  import java.util.zip.GZIPOutputStream;
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://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
69   */
70  @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
71  public final class GelfLayout extends AbstractStringLayout {
72  
73      public enum CompressionType {
74  
75          GZIP {
76              @Override
77              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
78                  return new GZIPOutputStream(os);
79              }
80          },
81          ZLIB {
82              @Override
83              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
84                  return new DeflaterOutputStream(os);
85              }
86          },
87          OFF {
88              @Override
89              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
90                  return null;
91              }
92          };
93  
94          public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
95      }
96  
97      private static final char C = ',';
98      private static final int COMPRESSION_THRESHOLD = 1024;
99      private static final char Q = '\"';
100     private static final String QC = "\",";
101     private static final String QU = "\"_";
102 
103     private final KeyValuePair[] additionalFields;
104     private final int compressionThreshold;
105     private final CompressionType compressionType;
106     private final String host;
107     private final boolean includeStacktrace;
108 
109     public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
110                       final int compressionThreshold, final boolean includeStacktrace) {
111         super(StandardCharsets.UTF_8);
112         this.host = host;
113         this.additionalFields = additionalFields;
114         this.compressionType = compressionType;
115         this.compressionThreshold = compressionThreshold;
116         this.includeStacktrace = includeStacktrace;
117     }
118     
119     @PluginFactory
120     public static GelfLayout createLayout(
121             //@formatter:off
122             @PluginAttribute("host") final String host,
123             @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
124             @PluginAttribute(value = "compressionType",
125                 defaultString = "GZIP") final CompressionType compressionType,
126             @PluginAttribute(value = "compressionThreshold",
127                 defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
128             @PluginAttribute(value = "includeStacktrace",
129                 defaultBoolean = true) final boolean includeStacktrace) {
130             // @formatter:on
131         return new GelfLayout(host, additionalFields, compressionType, compressionThreshold, includeStacktrace);
132     }
133 
134     @Override
135     public Map<String, String> getContentFormat() {
136         return Collections.emptyMap();
137     }
138 
139     @Override
140     public String getContentType() {
141         return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
142     }
143 
144     @Override
145     public byte[] toByteArray(final LogEvent event) {
146         final StringBuilder text = toText(event, getStringBuilder(), false);
147         final byte[] bytes = getBytes(text.toString());
148         return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
149     }
150 
151     @Override
152     public void encode(final LogEvent event, final ByteBufferDestination destination) {
153         if (compressionType != CompressionType.OFF) {
154             super.encode(event, destination);
155             return;
156         }
157         final StringBuilder text = toText(event, getStringBuilder(), true);
158         final Encoder<StringBuilder> helper = getStringBuilderEncoder();
159         helper.encode(text, destination);
160     }
161 
162     private byte[] compress(final byte[] bytes) {
163         try {
164             final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
165             try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
166                 if (stream == null) {
167                     return bytes;
168                 }
169                 stream.write(bytes);
170                 stream.finish();
171             }
172             return baos.toByteArray();
173         } catch (final IOException e) {
174             StatusLogger.getLogger().error(e);
175             return bytes;
176         }
177     }
178 
179     @Override
180     public String toSerializable(final LogEvent event) {
181         final StringBuilder text = toText(event, getStringBuilder(), false);
182         return text.toString();
183     }
184 
185     private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
186         builder.append('{');
187         builder.append("\"version\":\"1.1\",");
188         builder.append("\"host\":\"");
189         JsonUtils.quoteAsString(toNullSafeString(host), builder);
190         builder.append(QC);
191         builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
192         builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
193         if (event.getThreadName() != null) {
194             builder.append("\"_thread\":\"");
195             JsonUtils.quoteAsString(event.getThreadName(), builder);
196             builder.append(QC);
197         }
198         if (event.getLoggerName() != null) {
199             builder.append("\"_logger\":\"");
200             JsonUtils.quoteAsString(event.getLoggerName(), builder);
201             builder.append(QC);
202         }
203 
204         for (final KeyValuePair additionalField : additionalFields) {
205             builder.append(QU);
206             JsonUtils.quoteAsString(additionalField.getKey(), builder);
207             builder.append("\":\"");
208             JsonUtils.quoteAsString(toNullSafeString(additionalField.getValue()), builder);
209             builder.append(QC);
210         }
211         for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
212             builder.append(QU);
213             JsonUtils.quoteAsString(entry.getKey(), builder);
214             builder.append("\":\"");
215             JsonUtils.quoteAsString(toNullSafeString(entry.getValue()), builder);
216             builder.append(QC);
217         }
218         if (event.getThrown() != null) {
219             builder.append("\"full_message\":\"");
220             if (includeStacktrace) {
221                 JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
222             } else {
223                 JsonUtils.quoteAsString(event.getThrown().toString(), builder);
224             }
225             builder.append(QC);
226         }
227 
228         builder.append("\"short_message\":\"");
229         final Message message = event.getMessage();
230         if (message instanceof CharSequence) {
231             JsonUtils.quoteAsString(((CharSequence)message), builder);
232         } else if (gcFree && message instanceof StringBuilderFormattable) {
233             final StringBuilder messageBuffer = getMessageStringBuilder();
234             ((StringBuilderFormattable)message).formatTo(messageBuffer);
235             JsonUtils.quoteAsString(messageBuffer, builder);
236         } else {
237             JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
238         }
239         builder.append(Q);
240         builder.append('}');
241         return builder;
242     }
243 
244     private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
245 
246     private static StringBuilder getMessageStringBuilder() {
247         StringBuilder result = messageStringBuilder.get();
248         if (result == null) {
249             result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
250             messageStringBuilder.set(result);
251         }
252         result.setLength(0);
253         return result;
254     }
255 
256     private CharSequence toNullSafeString(final CharSequence s) {
257         return s == null ? Strings.EMPTY : s;
258     }
259 
260     /**
261      * Non-private to make it accessible from unit test.
262      */
263     static CharSequence formatTimestamp(final long timeMillis) {
264         if (timeMillis < 1000) {
265             return "0";
266         }
267         final StringBuilder builder = getTimestampStringBuilder();
268         builder.append(timeMillis);
269         builder.insert(builder.length() - 3, '.');
270         return builder;
271     }
272 
273     private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
274 
275     private static StringBuilder getTimestampStringBuilder() {
276         StringBuilder result = timestampStringBuilder.get();
277         if (result == null) {
278             result = new StringBuilder(20);
279             timestampStringBuilder.set(result);
280         }
281         result.setLength(0);
282         return result;
283     }
284 
285     /**
286      * http://en.wikipedia.org/wiki/Syslog#Severity_levels
287      */
288     private int formatLevel(final Level level) {
289         return Severity.getSeverity(level).getCode();
290     }
291 
292     /**
293      * Non-private to make it accessible from unit test.
294      */
295     static CharSequence formatThrowable(final Throwable throwable) {
296         // stack traces are big enough to provide a reasonably large initial capacity here
297         final StringWriter sw = new StringWriter(2048);
298         final PrintWriter pw = new PrintWriter(sw);
299         throwable.printStackTrace(pw);
300         pw.flush();
301         return sw.getBuffer();
302     }
303 }