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