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