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.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
53
54
55
56
57
58
59
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
165
166
167
168 public B setHost(final String host) {
169 this.host = host;
170 return asBuilder();
171 }
172
173
174
175
176
177
178 public B setCompressionType(final CompressionType compressionType) {
179 this.compressionType = compressionType;
180 return asBuilder();
181 }
182
183
184
185
186
187
188 public B setCompressionThreshold(final int compressionThreshold) {
189 this.compressionThreshold = compressionThreshold;
190 return asBuilder();
191 }
192
193
194
195
196
197
198
199 public B setIncludeStacktrace(final boolean includeStacktrace) {
200 this.includeStacktrace = includeStacktrace;
201 return asBuilder();
202 }
203
204
205
206
207
208
209 public B setIncludeThreadContext(final boolean includeThreadContext) {
210 this.includeThreadContext = includeThreadContext;
211 return asBuilder();
212 }
213
214
215
216
217
218
219
220 public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
221 this.includeNullDelimiter = includeNullDelimiter;
222 return asBuilder();
223 }
224
225
226
227
228
229
230 public B setAdditionalFields(final KeyValuePair[] additionalFields) {
231 this.additionalFields = additionalFields;
232 return asBuilder();
233 }
234 }
235
236
237
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
269
270 @Deprecated
271 public static GelfLayout createLayout(
272
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
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
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
466
467 private int formatLevel(final Level level) {
468 return Severity.getSeverity(level).getCode();
469 }
470
471
472
473
474 static CharSequence formatThrowable(final Throwable throwable) {
475
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 }