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 buffer.append('.');
455 pad(cal.get(Calendar.MILLISECOND), THREE_DIGITS, buffer);
456
457 int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
458 if (tzmin == 0) {
459 buffer.append('Z');
460 } else {
461 if (tzmin < 0) {
462 tzmin = -tzmin;
463 buffer.append('-');
464 } else {
465 buffer.append('+');
466 }
467 final int tzhour = tzmin / MINUTES_PER_HOUR;
468 tzmin -= tzhour * MINUTES_PER_HOUR;
469 pad(tzhour, TWO_DIGITS, buffer);
470 buffer.append(':');
471 pad(tzmin, TWO_DIGITS, buffer);
472 }
473 synchronized (this) {
474 if (last == lastTimestamp) {
475 lastTimestamp = now;
476 timestamppStr = buffer.toString();
477 }
478 }
479 return buffer.toString();
480 }
481
482 private void pad(final int val, int max, final StringBuilder buf) {
483 while (max > 1) {
484 if (val < max) {
485 buf.append('0');
486 }
487 max = max / TWO_DIGITS;
488 }
489 buf.append(Integer.toString(val));
490 }
491
492 private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
493 final StringBuilder sb, final ListChecker checker) {
494 if ((id == null && defaultId == null) || data.discard()) {
495 return;
496 }
497
498 sb.append('[');
499 sb.append(id);
500 if (!mdcSdId.toString().equals(id)) {
501 appendMap(prefix, data.getFields(), sb, noopChecker);
502 } else {
503 appendMap(prefix, data.getFields(), sb, checker);
504 }
505 sb.append(']');
506 }
507
508 private String getId(final StructuredDataId id) {
509 final StringBuilder sb = new StringBuilder();
510 if (id == null || id.getName() == null) {
511 sb.append(defaultId);
512 } else {
513 sb.append(id.getName());
514 }
515 int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
516 if (ein < 0) {
517 ein = enterpriseNumber;
518 }
519 if (ein >= 0) {
520 sb.append('@').append(ein);
521 }
522 return sb.toString();
523 }
524
525 private void checkRequired(final Map<String, String> map) {
526 for (final String key : mdcRequired) {
527 final String value = map.get(key);
528 if (value == null) {
529 throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
530 }
531 }
532 }
533
534 private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
535 final ListChecker checker) {
536 final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
537 for (final Map.Entry<String, String> entry : sorted.entrySet()) {
538 if (checker.check(entry.getKey()) && entry.getValue() != null) {
539 sb.append(' ');
540 if (prefix != null) {
541 sb.append(prefix);
542 }
543 sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"")
544 .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"');
545 }
546 }
547 }
548
549 private String escapeSDParams(final String value) {
550 return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
551 }
552
553
554
555
556 private interface ListChecker {
557 boolean check(String key);
558 }
559
560
561
562
563 private class IncludeChecker implements ListChecker {
564 @Override
565 public boolean check(final String key) {
566 return mdcIncludes.contains(key);
567 }
568 }
569
570
571
572
573 private class ExcludeChecker implements ListChecker {
574 @Override
575 public boolean check(final String key) {
576 return !mdcExcludes.contains(key);
577 }
578 }
579
580
581
582
583 private class NoopChecker implements ListChecker {
584 @Override
585 public boolean check(final String key) {
586 return true;
587 }
588 }
589
590 @Override
591 public String toString() {
592 final StringBuilder sb = new StringBuilder();
593 sb.append("facility=").append(facility.name());
594 sb.append(" appName=").append(appName);
595 sb.append(" defaultId=").append(defaultId);
596 sb.append(" enterpriseNumber=").append(enterpriseNumber);
597 sb.append(" newLine=").append(includeNewLine);
598 sb.append(" includeMDC=").append(includeMdc);
599 sb.append(" messageId=").append(messageId);
600 return sb.toString();
601 }
602
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 @PluginFactory
628 public static Rfc5424Layout createLayout(
629 @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
630 @PluginAttribute("id") final String id,
631 @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber,
632 @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
633 @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
634 @PluginAttribute("mdcPrefix") final String mdcPrefix,
635 @PluginAttribute("eventPrefix") final String eventPrefix,
636 @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
637 @PluginAttribute("newLineEscape") final String escapeNL,
638 @PluginAttribute("appName") final String appName,
639 @PluginAttribute("messageId") final String msgId,
640 @PluginAttribute("mdcExcludes") final String excludes,
641 @PluginAttribute("mdcIncludes") String includes,
642 @PluginAttribute("mdcRequired") final String required,
643 @PluginAttribute("exceptionPattern") final String exceptionPattern,
644 @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat,
645 @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
646 @PluginConfiguration final Configuration config) {
647 final Charset charset = Charsets.UTF_8;
648 if (includes != null && excludes != null) {
649 LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
650 includes = null;
651 }
652
653 return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix,
654 eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern,
655 useTlsMessageFormat, loggerFields);
656 }
657
658 private class FieldFormatter {
659
660 private final Map<String, List<PatternFormatter>> delegateMap;
661 private final boolean discardIfEmpty;
662
663 public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
664 this.discardIfEmpty = discardIfEmpty;
665 this.delegateMap = fieldMap;
666 }
667
668 public StructuredDataElement format(final LogEvent event) {
669 final Map<String, String> map = new HashMap<String, String>();
670
671 for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
672 final StringBuilder buffer = new StringBuilder();
673 for (final PatternFormatter formatter : entry.getValue()) {
674 formatter.format(event, buffer);
675 }
676 map.put(entry.getKey(), buffer.toString());
677 }
678 return new StructuredDataElement(map, discardIfEmpty);
679 }
680 }
681
682 private class StructuredDataElement {
683
684 private final Map<String, String> fields;
685 private final boolean discardIfEmpty;
686
687 public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
688 this.discardIfEmpty = discardIfEmpty;
689 this.fields = fields;
690 }
691
692 boolean discard() {
693 if (discardIfEmpty == false) {
694 return false;
695 }
696 boolean foundNotEmptyValue = false;
697 for (final Map.Entry<String, String> entry : fields.entrySet()) {
698 if (Strings.isNotEmpty(entry.getValue())) {
699 foundNotEmptyValue = true;
700 break;
701 }
702 }
703 return !foundNotEmptyValue;
704 }
705
706 void union(final Map<String, String> fields) {
707 this.fields.putAll(fields);
708 }
709
710 Map<String, String> getFields() {
711 return this.fields;
712 }
713 }
714 }