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