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