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.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   * Formats a log event in accordance with RFC 5424.
61   *
62   * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
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       * Not a very good default - it is the Apache Software Foundation's enterprise number.
73       */
74      public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
75      /**
76       * The default event id.
77       */
78      public static final String DEFAULT_ID = "Audit";
79      /**
80       * Match newlines in a platform-independent manner.
81       */
82      public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
83      /**
84       * Match characters which require escaping
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      * Create a PatternParser.
222      *
223      * @param config The Configuration.
224      * @param filterClass Filter the returned plugins after calling the plugin manager.
225      * @return The PatternParser.
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      * Gets this Rfc5424Layout's content format. Specified by:
243      * <ul>
244      * <li>Key: "structured" Value: "true"</li>
245      * <li>Key: "format" Value: "RFC5424"</li>
246      * </ul>
247      * 
248      * @return Map of content format keys supporting Rfc5424Layout
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      * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification.
260      *
261      * @param event The LogEvent.
262      * @return The RFC 5424 String representation of the LogEvent.
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         // This layout formats StructuredDataMessages instead of delegating to the Message itself.
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      * Interface used to check keys in a Map.
555      */
556     private interface ListChecker {
557         boolean check(String key);
558     }
559 
560     /**
561      * Includes only the listed keys.
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      * Excludes the listed keys.
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      * Does nothing.
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      * Create the RFC 5424 Layout.
605      *
606      * @param facility         The Facility is used to try to classify the message.
607      * @param id               The default structured data id to use when formatting according to RFC 5424.
608      * @param enterpriseNumber The IANA enterprise number.
609      * @param includeMDC       Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
610      *                         record. Defaults to "true:.
611      * @param mdcId            The id to use for the MDC Structured Data Element.
612      * @param mdcPrefix        The prefix to add to MDC key names.
613      * @param eventPrefix      The prefix to add to event key names.
614      * @param newLine          If true, a newline will be appended to the end of the syslog record. The default is false.
615      * @param escapeNL         String that should be used to replace newlines within the message text.
616      * @param appName          The value to use as the APP-NAME in the RFC 5424 syslog record.
617      * @param msgId            The default value to be used in the MSGID field of RFC 5424 syslog records.
618      * @param excludes         A comma separated list of MDC keys that should be excluded from the LogEvent.
619      * @param includes         A comma separated list of MDC keys that should be included in the FlumeEvent.
620      * @param required         A comma separated list of MDC keys that must be present in the MDC.
621      * @param exceptionPattern The pattern for formatting exceptions.
622      * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
623      * @param loggerFields     Container for the KeyValuePairs containing the patterns
624      * @param config           The Configuration. Some Converters require access to the Interpolator.
625      * @return An Rfc5424Layout.
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, // RFC 5425
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 }