1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
60
61
62
63
64
65
66
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
215
216
217
218 public B setHost(final String host) {
219 this.host = host;
220 return asBuilder();
221 }
222
223
224
225
226
227
228 public B setCompressionType(final CompressionType compressionType) {
229 this.compressionType = compressionType;
230 return asBuilder();
231 }
232
233
234
235
236
237
238 public B setCompressionThreshold(final int compressionThreshold) {
239 this.compressionThreshold = compressionThreshold;
240 return asBuilder();
241 }
242
243
244
245
246
247
248
249 public B setIncludeStacktrace(final boolean includeStacktrace) {
250 this.includeStacktrace = includeStacktrace;
251 return asBuilder();
252 }
253
254
255
256
257
258
259 public B setIncludeThreadContext(final boolean includeThreadContext) {
260 this.includeThreadContext = includeThreadContext;
261 return asBuilder();
262 }
263
264
265
266
267
268
269
270 public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
271 this.includeNullDelimiter = includeNullDelimiter;
272 return asBuilder();
273 }
274
275
276
277
278
279
280 public B setAdditionalFields(final KeyValuePair[] additionalFields) {
281 this.additionalFields = additionalFields;
282 return asBuilder();
283 }
284
285
286
287
288
289
290 public B setMessagePattern(final String pattern) {
291 this.messagePattern = pattern;
292 return asBuilder();
293 }
294
295
296
297
298
299
300 public B setMdcIncludes(final String mdcIncludes) {
301 this.threadContextIncludes = mdcIncludes;
302 return asBuilder();
303 }
304
305
306
307
308
309
310 public B setMdcExcludes(final String mdcExcludes) {
311 this.threadContextExcludes = mdcExcludes;
312 return asBuilder();
313 }
314 }
315
316
317
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
373
374 @Deprecated
375 public static GelfLayout createLayout(
376
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
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
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
595
596 private int formatLevel(final Level level) {
597 return Severity.getSeverity(level).getCode();
598 }
599
600
601
602
603 static CharSequence formatThrowable(final Throwable throwable) {
604
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 }