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.nio.charset.StandardCharsets;
25  import java.util.Collections;
26  import java.util.Map;
27  import java.util.zip.DeflaterOutputStream;
28  import java.util.zip.GZIPOutputStream;
29  
30  import org.apache.logging.log4j.Level;
31  import org.apache.logging.log4j.core.Layout;
32  import org.apache.logging.log4j.core.LogEvent;
33  import org.apache.logging.log4j.core.config.Configuration;
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.PluginBuilderAttribute;
38  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
39  import org.apache.logging.log4j.core.config.plugins.PluginElement;
40  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
41  import org.apache.logging.log4j.core.net.Severity;
42  import org.apache.logging.log4j.core.util.JsonUtils;
43  import org.apache.logging.log4j.core.util.KeyValuePair;
44  import org.apache.logging.log4j.core.util.NetUtils;
45  import org.apache.logging.log4j.message.Message;
46  import org.apache.logging.log4j.status.StatusLogger;
47  import org.apache.logging.log4j.util.StringBuilderFormattable;
48  import org.apache.logging.log4j.util.Strings;
49  import org.apache.logging.log4j.util.TriConsumer;
50  
51  /**
52   * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
53   * <p>
54   * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
55   * log event data is larger than 1024 bytes (the {@code compressionThreshold}).
56   * This layout does not implement chunking.
57   * </p>
58   *
59   * @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
60   */
61  @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
62  public final class GelfLayout extends AbstractStringLayout {
63  
64      public enum CompressionType {
65  
66          GZIP {
67              @Override
68              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
69                  return new GZIPOutputStream(os);
70              }
71          },
72          ZLIB {
73              @Override
74              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
75                  return new DeflaterOutputStream(os);
76              }
77          },
78          OFF {
79              @Override
80              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
81                  return null;
82              }
83          };
84  
85          public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
86      }
87  
88      private static final char C = ',';
89      private static final int COMPRESSION_THRESHOLD = 1024;
90      private static final char Q = '\"';
91      private static final String QC = "\",";
92      private static final String QU = "\"_";
93  
94      private final KeyValuePair[] additionalFields;
95      private final int compressionThreshold;
96      private final CompressionType compressionType;
97      private final String host;
98      private final boolean includeStacktrace;
99      private final boolean includeThreadContext;
100     private final boolean includeNullDelimiter;
101 
102     public static class Builder<B extends Builder<B>> extends AbstractStringLayout.Builder<B>
103         implements org.apache.logging.log4j.core.util.Builder<GelfLayout> {
104 
105         @PluginBuilderAttribute
106         private String host;
107 
108         @PluginElement("AdditionalField")
109         private KeyValuePair[] additionalFields;
110 
111         @PluginBuilderAttribute
112         private CompressionType compressionType = CompressionType.GZIP;
113 
114         @PluginBuilderAttribute
115         private int compressionThreshold = COMPRESSION_THRESHOLD;
116 
117         @PluginBuilderAttribute
118         private boolean includeStacktrace = true;
119 
120         @PluginBuilderAttribute
121         private boolean includeThreadContext = true;
122 
123         @PluginBuilderAttribute
124         private boolean includeNullDelimiter = false;
125 
126         public Builder() {
127             super();
128             setCharset(StandardCharsets.UTF_8);
129         }
130 
131         @Override
132         public GelfLayout build() {
133             return new GelfLayout(getConfiguration(), host, additionalFields, compressionType, compressionThreshold,
134                 includeStacktrace, includeThreadContext, includeNullDelimiter);
135         }
136 
137         public String getHost() {
138             return host;
139         }
140 
141         public CompressionType getCompressionType() {
142             return compressionType;
143         }
144 
145         public int getCompressionThreshold() {
146             return compressionThreshold;
147         }
148 
149         public boolean isIncludeStacktrace() {
150             return includeStacktrace;
151         }
152 
153         public boolean isIncludeThreadContext() {
154             return includeThreadContext;
155         }
156 
157         public boolean isIncludeNullDelimiter() { return includeNullDelimiter; }
158 
159         public KeyValuePair[] getAdditionalFields() {
160             return additionalFields;
161         }
162 
163         /**
164          * The value of the <code>host</code> property (optional, defaults to local host name).
165          *
166          * @return this builder
167          */
168         public B setHost(final String host) {
169             this.host = host;
170             return asBuilder();
171         }
172 
173         /**
174          * Compression to use (optional, defaults to GZIP).
175          *
176          * @return this builder
177          */
178         public B setCompressionType(final CompressionType compressionType) {
179             this.compressionType = compressionType;
180             return asBuilder();
181         }
182 
183         /**
184          * Compress if data is larger than this number of bytes (optional, defaults to 1024).
185          *
186          * @return this builder
187          */
188         public B setCompressionThreshold(final int compressionThreshold) {
189             this.compressionThreshold = compressionThreshold;
190             return asBuilder();
191         }
192 
193         /**
194          * Whether to include full stacktrace of logged Throwables (optional, default to true).
195          * If set to false, only the class name and message of the Throwable will be included.
196          *
197          * @return this builder
198          */
199         public B setIncludeStacktrace(final boolean includeStacktrace) {
200             this.includeStacktrace = includeStacktrace;
201             return asBuilder();
202         }
203 
204         /**
205          * Whether to include thread context as additional fields (optional, default to true).
206          *
207          * @return this builder
208          */
209         public B setIncludeThreadContext(final boolean includeThreadContext) {
210             this.includeThreadContext = includeThreadContext;
211             return asBuilder();
212         }
213 
214         /**
215          * Whether to include NULL byte as delimiter after each event (optional, default to false).
216          * Useful for Graylog GELF TCP input.
217          *
218          * @return this builder
219          */
220         public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
221             this.includeNullDelimiter = includeNullDelimiter;
222             return asBuilder();
223         }
224 
225         /**
226          * Additional fields to set on each log event.
227          *
228          * @return this builder
229          */
230         public B setAdditionalFields(final KeyValuePair[] additionalFields) {
231             this.additionalFields = additionalFields;
232             return asBuilder();
233         }
234     }
235 
236     /**
237      * @deprecated Use {@link #newBuilder()} instead
238      */
239     @Deprecated
240     public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
241                       final int compressionThreshold, final boolean includeStacktrace) {
242         this(null, host, additionalFields, compressionType, compressionThreshold, includeStacktrace, true, false);
243     }
244 
245     private GelfLayout(final Configuration config, final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
246                final int compressionThreshold, final boolean includeStacktrace, final boolean includeThreadContext, final boolean includeNullDelimiter) {
247         super(config, StandardCharsets.UTF_8, null, null);
248         this.host = host != null ? host : NetUtils.getLocalHostname();
249         this.additionalFields = additionalFields != null ? additionalFields : new KeyValuePair[0];
250         if (config == null) {
251             for (final KeyValuePair additionalField : this.additionalFields) {
252                 if (valueNeedsLookup(additionalField.getValue())) {
253                     throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables");
254                 }
255             }
256         }
257         this.compressionType = compressionType;
258         this.compressionThreshold = compressionThreshold;
259         this.includeStacktrace = includeStacktrace;
260         this.includeThreadContext = includeThreadContext;
261         this.includeNullDelimiter = includeNullDelimiter;
262         if (includeNullDelimiter && compressionType != CompressionType.OFF) {
263             throw new IllegalArgumentException("null delimiter cannot be used with compression");
264         }
265     }
266 
267     /**
268      * @deprecated Use {@link #newBuilder()} instead
269      */
270     @Deprecated
271     public static GelfLayout createLayout(
272             //@formatter:off
273             @PluginAttribute("host") final String host,
274             @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
275             @PluginAttribute(value = "compressionType",
276                 defaultString = "GZIP") final CompressionType compressionType,
277             @PluginAttribute(value = "compressionThreshold",
278                 defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
279             @PluginAttribute(value = "includeStacktrace",
280                 defaultBoolean = true) final boolean includeStacktrace) {
281             // @formatter:on
282         return new GelfLayout(null, host, additionalFields, compressionType, compressionThreshold, includeStacktrace, true, false);
283     }
284 
285     @PluginBuilderFactory
286     public static <B extends Builder<B>> B newBuilder() {
287         return new Builder<B>().asBuilder();
288     }
289 
290     @Override
291     public Map<String, String> getContentFormat() {
292         return Collections.emptyMap();
293     }
294 
295     @Override
296     public String getContentType() {
297         return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
298     }
299 
300     @Override
301     public byte[] toByteArray(final LogEvent event) {
302         final StringBuilder text = toText(event, getStringBuilder(), false);
303         final byte[] bytes = getBytes(text.toString());
304         return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
305     }
306 
307     @Override
308     public void encode(final LogEvent event, final ByteBufferDestination destination) {
309         if (compressionType != CompressionType.OFF) {
310             super.encode(event, destination);
311             return;
312         }
313         final StringBuilder text = toText(event, getStringBuilder(), true);
314         final Encoder<StringBuilder> helper = getStringBuilderEncoder();
315         helper.encode(text, destination);
316     }
317 
318     private byte[] compress(final byte[] bytes) {
319         try {
320             final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
321             try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
322                 if (stream == null) {
323                     return bytes;
324                 }
325                 stream.write(bytes);
326                 stream.finish();
327             }
328             return baos.toByteArray();
329         } catch (final IOException e) {
330             StatusLogger.getLogger().error(e);
331             return bytes;
332         }
333     }
334 
335     @Override
336     public String toSerializable(final LogEvent event) {
337         final StringBuilder text = toText(event, getStringBuilder(), false);
338         return text.toString();
339     }
340 
341     private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
342         builder.append('{');
343         builder.append("\"version\":\"1.1\",");
344         builder.append("\"host\":\"");
345         JsonUtils.quoteAsString(toNullSafeString(host), builder);
346         builder.append(QC);
347         builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
348         builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
349         if (event.getThreadName() != null) {
350             builder.append("\"_thread\":\"");
351             JsonUtils.quoteAsString(event.getThreadName(), builder);
352             builder.append(QC);
353         }
354         if (event.getLoggerName() != null) {
355             builder.append("\"_logger\":\"");
356             JsonUtils.quoteAsString(event.getLoggerName(), builder);
357             builder.append(QC);
358         }
359         if (additionalFields.length > 0) {
360             final StrSubstitutor strSubstitutor = getConfiguration().getStrSubstitutor();
361             for (final KeyValuePair additionalField : additionalFields) {
362                 builder.append(QU);
363                 JsonUtils.quoteAsString(additionalField.getKey(), builder);
364                 builder.append("\":\"");
365                 final String value = valueNeedsLookup(additionalField.getValue())
366                     ? strSubstitutor.replace(event, additionalField.getValue())
367                     : additionalField.getValue();
368                 JsonUtils.quoteAsString(toNullSafeString(value), builder);
369                 builder.append(QC);
370             }
371         }
372         if (includeThreadContext) {
373             event.getContextData().forEach(WRITE_KEY_VALUES_INTO, builder);
374         }
375         if (event.getThrown() != null) {
376             builder.append("\"full_message\":\"");
377             if (includeStacktrace) {
378                 JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
379             } else {
380                 JsonUtils.quoteAsString(event.getThrown().toString(), builder);
381             }
382             builder.append(QC);
383         }
384 
385         builder.append("\"short_message\":\"");
386         final Message message = event.getMessage();
387         if (message instanceof CharSequence) {
388             JsonUtils.quoteAsString(((CharSequence)message), builder);
389         } else if (gcFree && message instanceof StringBuilderFormattable) {
390             final StringBuilder messageBuffer = getMessageStringBuilder();
391             try {
392                 ((StringBuilderFormattable) message).formatTo(messageBuffer);
393                 JsonUtils.quoteAsString(messageBuffer, builder);
394             } finally {
395                 trimToMaxSize(messageBuffer);
396             }
397         } else {
398             JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
399         }
400         builder.append(Q);
401         builder.append('}');
402         if (includeNullDelimiter) {
403             builder.append('\0');
404         }
405         return builder;
406     }
407 
408     private static boolean valueNeedsLookup(final String value) {
409         return value != null && value.contains("${");
410     }
411 
412     private static final TriConsumer<String, Object, StringBuilder> WRITE_KEY_VALUES_INTO = new TriConsumer<String, Object, StringBuilder>() {
413         @Override
414         public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
415             stringBuilder.append(QU);
416             JsonUtils.quoteAsString(key, stringBuilder);
417             stringBuilder.append("\":\"");
418             JsonUtils.quoteAsString(toNullSafeString(String.valueOf(value)), stringBuilder);
419             stringBuilder.append(QC);
420         }
421     };
422 
423     private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
424 
425     private static StringBuilder getMessageStringBuilder() {
426         StringBuilder result = messageStringBuilder.get();
427         if (result == null) {
428             result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
429             messageStringBuilder.set(result);
430         }
431         result.setLength(0);
432         return result;
433     }
434 
435     private static CharSequence toNullSafeString(final CharSequence s) {
436         return s == null ? Strings.EMPTY : s;
437     }
438 
439     /**
440      * Non-private to make it accessible from unit test.
441      */
442     static CharSequence formatTimestamp(final long timeMillis) {
443         if (timeMillis < 1000) {
444             return "0";
445         }
446         final StringBuilder builder = getTimestampStringBuilder();
447         builder.append(timeMillis);
448         builder.insert(builder.length() - 3, '.');
449         return builder;
450     }
451 
452     private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
453 
454     private static StringBuilder getTimestampStringBuilder() {
455         StringBuilder result = timestampStringBuilder.get();
456         if (result == null) {
457             result = new StringBuilder(20);
458             timestampStringBuilder.set(result);
459         }
460         result.setLength(0);
461         return result;
462     }
463 
464     /**
465      * http://en.wikipedia.org/wiki/Syslog#Severity_levels
466      */
467     private int formatLevel(final Level level) {
468         return Severity.getSeverity(level).getCode();
469     }
470 
471     /**
472      * Non-private to make it accessible from unit test.
473      */
474     static CharSequence formatThrowable(final Throwable throwable) {
475         // stack traces are big enough to provide a reasonably large initial capacity here
476         final StringWriter sw = new StringWriter(2048);
477         final PrintWriter pw = new PrintWriter(sw);
478         throwable.printStackTrace(pw);
479         pw.flush();
480         return sw.getBuffer();
481     }
482 }