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.nio.charset.Charset;
20 import java.util.ArrayList;
21 import java.util.Calendar;
22 import java.util.GregorianCalendar;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.SortedMap;
27 import java.util.TreeMap;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30
31 import org.apache.logging.log4j.Level;
32 import org.apache.logging.log4j.LoggingException;
33 import org.apache.logging.log4j.core.Layout;
34 import org.apache.logging.log4j.core.LogEvent;
35 import org.apache.logging.log4j.core.appender.TlsSyslogFrame;
36 import org.apache.logging.log4j.core.config.Configuration;
37 import org.apache.logging.log4j.core.config.Node;
38 import org.apache.logging.log4j.core.config.plugins.Plugin;
39 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
40 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
41 import org.apache.logging.log4j.core.config.plugins.PluginElement;
42 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
43 import org.apache.logging.log4j.core.net.Facility;
44 import org.apache.logging.log4j.core.net.Priority;
45 import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
46 import org.apache.logging.log4j.core.pattern.PatternConverter;
47 import org.apache.logging.log4j.core.pattern.PatternFormatter;
48 import org.apache.logging.log4j.core.pattern.PatternParser;
49 import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
50 import org.apache.logging.log4j.core.util.Charsets;
51 import org.apache.logging.log4j.core.util.NetUtils;
52 import org.apache.logging.log4j.core.util.Patterns;
53 import org.apache.logging.log4j.message.Message;
54 import org.apache.logging.log4j.message.StructuredDataId;
55 import org.apache.logging.log4j.message.StructuredDataMessage;
56 import org.apache.logging.log4j.util.Strings;
57
58
59
60
61
62
63
64 @Plugin(name = "Rfc5424Layout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
65 public final class Rfc5424Layout extends AbstractStringLayout {
66
67 private static final long serialVersionUID = 1L;
68
69 private static final String LF = "\n";
70
71
72
73
74 public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
75
76
77
78 public static final String DEFAULT_ID = "Audit";
79
80
81
82 public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
83
84
85
86 public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
87
88 public static final String DEFAULT_MDCID = "mdc";
89 private static final int TWO_DIGITS = 10;
90 private static final int THREE_DIGITS = 100;
91 private static final int MILLIS_PER_MINUTE = 60000;
92 private static final int MINUTES_PER_HOUR = 60;
93
94 private static final String COMPONENT_KEY = "RFC5424-Converter";
95
96 private final Facility facility;
97 private final String defaultId;
98 private final int enterpriseNumber;
99 private final boolean includeMdc;
100 private final String mdcId;
101 private final StructuredDataId mdcSdId;
102 private final String localHostName;
103 private final String appName;
104 private final String messageId;
105 private final String configName;
106 private final String mdcPrefix;
107 private final String eventPrefix;
108 private final List<String> mdcExcludes;
109 private final List<String> mdcIncludes;
110 private final List<String> mdcRequired;
111 private final ListChecker checker;
112 private final ListChecker noopChecker = new NoopChecker();
113 private final boolean includeNewLine;
114 private final String escapeNewLine;
115 private final boolean useTlsMessageFormat;
116
117 private long lastTimestamp = -1;
118 private String timestamppStr;
119
120 private final List<PatternFormatter> exceptionFormatters;
121 private final Map<String, FieldFormatter> fieldFormatters;
122
123 private Rfc5424Layout(final Configuration config, final Facility facility, final String id, final int ein,
124 final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId,
125 final String mdcPrefix, final String eventPrefix,
126 final String appName, final String messageId, final String excludes, final String includes,
127 final String required, final Charset charset, final String exceptionPattern,
128 final boolean useTLSMessageFormat, final LoggerFields[] loggerFields) {
129 super(charset);
130 final PatternParser exceptionParser = createPatternParser(config, ThrowablePatternConverter.class);
131 exceptionFormatters = exceptionPattern == null ? null : exceptionParser.parse(exceptionPattern, false, false);
132 this.facility = facility;
133 this.defaultId = id == null ? DEFAULT_ID : id;
134 this.enterpriseNumber = ein;
135 this.includeMdc = includeMDC;
136 this.includeNewLine = includeNL;
137 this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL);
138 this.mdcId = mdcId;
139 this.mdcSdId = new StructuredDataId(mdcId, enterpriseNumber, null, null);
140 this.mdcPrefix = mdcPrefix;
141 this.eventPrefix = eventPrefix;
142 this.appName = appName;
143 this.messageId = messageId;
144 this.useTlsMessageFormat = useTLSMessageFormat;
145 this.localHostName = NetUtils.getLocalHostname();
146 ListChecker c = null;
147 if (excludes != null) {
148 final String[] array = excludes.split(Patterns.COMMA_SEPARATOR);
149 if (array.length > 0) {
150 c = new ExcludeChecker();
151 mdcExcludes = new ArrayList<String>(array.length);
152 for (final String str : array) {
153 mdcExcludes.add(str.trim());
154 }
155 } else {
156 mdcExcludes = null;
157 }
158 } else {
159 mdcExcludes = null;
160 }
161 if (includes != null) {
162 final String[] array = includes.split(Patterns.COMMA_SEPARATOR);
163 if (array.length > 0) {
164 c = new IncludeChecker();
165 mdcIncludes = new ArrayList<String>(array.length);
166 for (final String str : array) {
167 mdcIncludes.add(str.trim());
168 }
169 } else {
170 mdcIncludes = null;
171 }
172 } else {
173 mdcIncludes = null;
174 }
175 if (required != null) {
176 final String[] array = required.split(Patterns.COMMA_SEPARATOR);
177 if (array.length > 0) {
178 mdcRequired = new ArrayList<String>(array.length);
179 for (final String str : array) {
180 mdcRequired.add(str.trim());
181 }
182 } else {
183 mdcRequired = null;
184 }
185
186 } else {
187 mdcRequired = null;
188 }
189 this.checker = c != null ? c : noopChecker;
190 final String name = config == null ? null : config.getName();
191 configName = name != null && name.length() > 0 ? name : null;
192 this.fieldFormatters = createFieldFormatters(loggerFields, config);
193 }
194
195 private Map<String, FieldFormatter> createFieldFormatters(final LoggerFields[] loggerFields,
196 final Configuration config) {
197 final Map<String, FieldFormatter> sdIdMap = new HashMap<String, FieldFormatter>();
198
199 if (loggerFields != null) {
200 for (final LoggerFields lField : loggerFields) {
201 final StructuredDataId key = lField.getSdId() == null ? mdcSdId : lField.getSdId();
202 final Map<String, List<PatternFormatter>> sdParams = new HashMap<String, List<PatternFormatter>>();
203 final Map<String, String> fields = lField.getMap();
204 if (!fields.isEmpty()) {
205 final PatternParser fieldParser = createPatternParser(config, null);
206
207 for (final Map.Entry<String, String> entry : fields.entrySet()) {
208 final List<PatternFormatter> formatters = fieldParser.parse(entry.getValue(), false, false);
209 sdParams.put(entry.getKey(), formatters);
210 }
211 final FieldFormatter fieldFormatter = new FieldFormatter(sdParams,
212 lField.getDiscardIfAllFieldsAreEmpty());
213 sdIdMap.put(key.toString(), fieldFormatter);
214 }
215 }
216 }
217 return sdIdMap.size() > 0 ? sdIdMap : null;
218 }
219
220
221
222
223
224
225
226
227 private static PatternParser createPatternParser(final Configuration config,
228 final Class<? extends PatternConverter> filterClass) {
229 if (config == null) {
230 return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class, filterClass);
231 }
232 PatternParser parser = config.getComponent(COMPONENT_KEY);
233 if (parser == null) {
234 parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class);
235 config.addComponent(COMPONENT_KEY, parser);
236 parser = (PatternParser) config.getComponent(COMPONENT_KEY);
237 }
238 return parser;
239 }
240
241
242
243
244
245
246
247
248
249
250 @Override
251 public Map<String, String> getContentFormat() {
252 final Map<String, String> result = new HashMap<String, String>();
253 result.put("structured", "true");
254 result.put("formatType", "RFC5424");
255 return result;
256 }
257
258
259
260
261
262
263
264 @Override
265 public String toSerializable(final LogEvent event) {
266 final StringBuilder buf = new StringBuilder();
267 appendPriority(buf, event.getLevel());
268 appendTimestamp(buf, event.getTimeMillis());
269 appendSpace(buf);
270 appendHostName(buf);
271 appendSpace(buf);
272 appendAppName(buf);
273 appendSpace(buf);
274 appendProcessId(buf);
275 appendSpace(buf);
276 appendMessageId(buf, event.getMessage());
277 appendSpace(buf);
278 appendStructuredElements(buf, event);
279 appendMessage(buf, event);
280 if (useTlsMessageFormat) {
281 return new TlsSyslogFrame(buf.toString()).toString();
282 }
283 return buf.toString();
284 }
285
286 private void appendPriority(final StringBuilder buffer, final Level logLevel) {
287 buffer.append('<');
288 buffer.append(Priority.getPriority(facility, logLevel));
289 buffer.append(">1 ");
290 }
291
292 private void appendTimestamp(final StringBuilder buffer, final long milliseconds) {
293 buffer.append(computeTimeStampString(milliseconds));
294 }
295
296 private void appendSpace(final StringBuilder buffer) {
297 buffer.append(' ');
298 }
299
300 private void appendHostName(final StringBuilder buffer) {
301 buffer.append(localHostName);
302 }
303
304 private void appendAppName(final StringBuilder buffer) {
305 if (appName != null) {
306 buffer.append(appName);
307 } else if (configName != null) {
308 buffer.append(configName);
309 } else {
310 buffer.append('-');
311 }
312 }
313
314 private void appendProcessId(final StringBuilder buffer) {
315 buffer.append(getProcId());
316 }
317
318 private void appendMessageId(final StringBuilder buffer, final Message message) {
319 final boolean isStructured = message instanceof StructuredDataMessage;
320 final String type = isStructured ? ((StructuredDataMessage) message).getType() : null;
321 if (type != null) {
322 buffer.append(type);
323 } else if (messageId != null) {
324 buffer.append(messageId);
325 } else {
326 buffer.append('-');
327 }
328 }
329
330 private void appendMessage(final StringBuilder buffer, final LogEvent event) {
331 final Message message = event.getMessage();
332
333 final String text = (message instanceof StructuredDataMessage) ? message.getFormat() : message.getFormattedMessage();
334
335 if (text != null && text.length() > 0) {
336 buffer.append(' ').append(escapeNewlines(text, escapeNewLine));
337 }
338
339 if (exceptionFormatters != null && event.getThrown() != null) {
340 final StringBuilder exception = new StringBuilder(LF);
341 for (final PatternFormatter formatter : exceptionFormatters) {
342 formatter.format(event, exception);
343 }
344 buffer.append(escapeNewlines(exception.toString(), escapeNewLine));
345 }
346 if (includeNewLine) {
347 buffer.append(LF);
348 }
349 }
350
351 private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) {
352 final Message message = event.getMessage();
353 final boolean isStructured = message instanceof StructuredDataMessage;
354
355 if (!isStructured && (fieldFormatters!= null && fieldFormatters.isEmpty()) && !includeMdc) {
356 buffer.append('-');
357 return;
358 }
359
360 final Map<String, StructuredDataElement> sdElements = new HashMap<String, StructuredDataElement>();
361 final Map<String, String> contextMap = event.getContextMap();
362
363 if (mdcRequired != null) {
364 checkRequired(contextMap);
365 }
366
367 if (fieldFormatters != null) {
368 for (final Map.Entry<String, FieldFormatter> sdElement: fieldFormatters.entrySet()) {
369 final String sdId = sdElement.getKey();
370 final StructuredDataElement elem = sdElement.getValue().format(event);
371 sdElements.put(sdId, elem);
372 }
373 }
374
375 if (includeMdc && contextMap.size() > 0) {
376 if (sdElements.containsKey(mdcSdId.toString())) {
377 final StructuredDataElement union = sdElements.get(mdcSdId.toString());
378 union.union(contextMap);
379 sdElements.put(mdcSdId.toString(), union);
380 } else {
381 final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false);
382 sdElements.put(mdcSdId.toString(), formattedContextMap);
383 }
384 }
385
386 if (isStructured) {
387 final StructuredDataMessage data = (StructuredDataMessage) message;
388 final Map<String, String> map = data.getData();
389 final StructuredDataId id = data.getId();
390 final String sdId = getId(id);
391
392 if (sdElements.containsKey(sdId)) {
393 final StructuredDataElement union = sdElements.get(id.toString());
394 union.union(map);
395 sdElements.put(sdId, union);
396 } else {
397 final StructuredDataElement formattedData = new StructuredDataElement(map, false);
398 sdElements.put(sdId, formattedData);
399 }
400 }
401
402 if (sdElements.isEmpty()) {
403 buffer.append('-');
404 return;
405 }
406
407 for (final Map.Entry<String, StructuredDataElement> entry: sdElements.entrySet()) {
408 formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, checker);
409 }
410 }
411
412 private String escapeNewlines(final String text, final String escapeNewLine) {
413 if (null == escapeNewLine) {
414 return text;
415 }
416 return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine);
417 }
418
419 protected String getProcId() {
420 return "-";
421 }
422
423 protected List<String> getMdcExcludes() {
424 return mdcExcludes;
425 }
426
427 protected List<String> getMdcIncludes() {
428 return mdcIncludes;
429 }
430
431 private String computeTimeStampString(final long now) {
432 long last;
433 synchronized (this) {
434 last = lastTimestamp;
435 if (now == lastTimestamp) {
436 return timestamppStr;
437 }
438 }
439
440 final StringBuilder buffer = new StringBuilder();
441 final Calendar cal = new GregorianCalendar();
442 cal.setTimeInMillis(now);
443 buffer.append(Integer.toString(cal.get(Calendar.YEAR)));
444 buffer.append('-');
445 pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer);
446 buffer.append('-');
447 pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer);
448 buffer.append('T');
449 pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer);
450 buffer.append(':');
451 pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer);
452 buffer.append(':');
453 pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer);
454
455 final int millis = cal.get(Calendar.MILLISECOND);
456 if (millis != 0) {
457 buffer.append('.');
458 pad(millis, THREE_DIGITS, buffer);
459 }
460
461 int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
462 if (tzmin == 0) {
463 buffer.append('Z');
464 } else {
465 if (tzmin < 0) {
466 tzmin = -tzmin;
467 buffer.append('-');
468 } else {
469 buffer.append('+');
470 }
471 final int tzhour = tzmin / MINUTES_PER_HOUR;
472 tzmin -= tzhour * MINUTES_PER_HOUR;
473 pad(tzhour, TWO_DIGITS, buffer);
474 buffer.append(':');
475 pad(tzmin, TWO_DIGITS, buffer);
476 }
477 synchronized (this) {
478 if (last == lastTimestamp) {
479 lastTimestamp = now;
480 timestamppStr = buffer.toString();
481 }
482 }
483 return buffer.toString();
484 }
485
486 private void pad(final int val, int max, final StringBuilder buf) {
487 while (max > 1) {
488 if (val < max) {
489 buf.append('0');
490 }
491 max = max / TWO_DIGITS;
492 }
493 buf.append(Integer.toString(val));
494 }
495
496 private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
497 final StringBuilder sb, final ListChecker checker) {
498 if ((id == null && defaultId == null) || data.discard()) {
499 return;
500 }
501
502 sb.append('[');
503 sb.append(id);
504 if (!mdcSdId.toString().equals(id)) {
505 appendMap(prefix, data.getFields(), sb, noopChecker);
506 } else {
507 appendMap(prefix, data.getFields(), sb, checker);
508 }
509 sb.append(']');
510 }
511
512 private String getId(final StructuredDataId id) {
513 final StringBuilder sb = new StringBuilder();
514 if (id == null || id.getName() == null) {
515 sb.append(defaultId);
516 } else {
517 sb.append(id.getName());
518 }
519 int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
520 if (ein < 0) {
521 ein = enterpriseNumber;
522 }
523 if (ein >= 0) {
524 sb.append('@').append(ein);
525 }
526 return sb.toString();
527 }
528
529 private void checkRequired(final Map<String, String> map) {
530 for (final String key : mdcRequired) {
531 final String value = map.get(key);
532 if (value == null) {
533 throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
534 }
535 }
536 }
537
538 private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
539 final ListChecker checker) {
540 final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
541 for (final Map.Entry<String, String> entry : sorted.entrySet()) {
542 if (checker.check(entry.getKey()) && entry.getValue() != null) {
543 sb.append(' ');
544 if (prefix != null) {
545 sb.append(prefix);
546 }
547 sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"")
548 .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"');
549 }
550 }
551 }
552
553 private String escapeSDParams(final String value) {
554 return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
555 }
556
557
558
559
560 private interface ListChecker {
561 boolean check(String key);
562 }
563
564
565
566
567 private class IncludeChecker implements ListChecker {
568 @Override
569 public boolean check(final String key) {
570 return mdcIncludes.contains(key);
571 }
572 }
573
574
575
576
577 private class ExcludeChecker implements ListChecker {
578 @Override
579 public boolean check(final String key) {
580 return !mdcExcludes.contains(key);
581 }
582 }
583
584
585
586
587 private class NoopChecker implements ListChecker {
588 @Override
589 public boolean check(final String key) {
590 return true;
591 }
592 }
593
594 @Override
595 public String toString() {
596 final StringBuilder sb = new StringBuilder();
597 sb.append("facility=").append(facility.name());
598 sb.append(" appName=").append(appName);
599 sb.append(" defaultId=").append(defaultId);
600 sb.append(" enterpriseNumber=").append(enterpriseNumber);
601 sb.append(" newLine=").append(includeNewLine);
602 sb.append(" includeMDC=").append(includeMdc);
603 sb.append(" messageId=").append(messageId);
604 return sb.toString();
605 }
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631 @PluginFactory
632 public static Rfc5424Layout createLayout(
633 @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
634 @PluginAttribute("id") final String id,
635 @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber,
636 @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
637 @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
638 @PluginAttribute("mdcPrefix") final String mdcPrefix,
639 @PluginAttribute("eventPrefix") final String eventPrefix,
640 @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
641 @PluginAttribute("newLineEscape") final String escapeNL,
642 @PluginAttribute("appName") final String appName,
643 @PluginAttribute("messageId") final String msgId,
644 @PluginAttribute("mdcExcludes") final String excludes,
645 @PluginAttribute("mdcIncludes") String includes,
646 @PluginAttribute("mdcRequired") final String required,
647 @PluginAttribute("exceptionPattern") final String exceptionPattern,
648 @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat,
649 @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
650 @PluginConfiguration final Configuration config) {
651 final Charset charset = Charsets.UTF_8;
652 if (includes != null && excludes != null) {
653 LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
654 includes = null;
655 }
656
657 return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix,
658 eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern,
659 useTlsMessageFormat, loggerFields);
660 }
661
662 private class FieldFormatter {
663
664 private final Map<String, List<PatternFormatter>> delegateMap;
665 private final boolean discardIfEmpty;
666
667 public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
668 this.discardIfEmpty = discardIfEmpty;
669 this.delegateMap = fieldMap;
670 }
671
672 public StructuredDataElement format(final LogEvent event) {
673 final Map<String, String> map = new HashMap<String, String>();
674
675 for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
676 final StringBuilder buffer = new StringBuilder();
677 for (final PatternFormatter formatter : entry.getValue()) {
678 formatter.format(event, buffer);
679 }
680 map.put(entry.getKey(), buffer.toString());
681 }
682 return new StructuredDataElement(map, discardIfEmpty);
683 }
684 }
685
686 private class StructuredDataElement {
687
688 private final Map<String, String> fields;
689 private final boolean discardIfEmpty;
690
691 public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
692 this.discardIfEmpty = discardIfEmpty;
693 this.fields = fields;
694 }
695
696 boolean discard() {
697 if (discardIfEmpty == false) {
698 return false;
699 }
700 boolean foundNotEmptyValue = false;
701 for (final Map.Entry<String, String> entry : fields.entrySet()) {
702 if (Strings.isNotEmpty(entry.getValue())) {
703 foundNotEmptyValue = true;
704 break;
705 }
706 }
707 return !foundNotEmptyValue;
708 }
709
710 void union(final Map<String, String> fields) {
711 this.fields.putAll(fields);
712 }
713
714 Map<String, String> getFields() {
715 return this.fields;
716 }
717 }
718 }