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 
455         final int millis = cal.get(Calendar.MILLISECOND);
456         if (millis != 0) {
457             buffer.append('.');
458             pad(millis, THREE_DIGITS, buffer);
459         }
460 
461         int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
462         if (tzmin == 0) {
463             buffer.append('Z');
464         } else {
465             if (tzmin < 0) {
466                 tzmin = -tzmin;
467                 buffer.append('-');
468             } else {
469                 buffer.append('+');
470             }
471             final int tzhour = tzmin / MINUTES_PER_HOUR;
472             tzmin -= tzhour * MINUTES_PER_HOUR;
473             pad(tzhour, TWO_DIGITS, buffer);
474             buffer.append(':');
475             pad(tzmin, TWO_DIGITS, buffer);
476         }
477         synchronized (this) {
478             if (last == lastTimestamp) {
479                 lastTimestamp = now;
480                 timestamppStr = buffer.toString();
481             }
482         }
483         return buffer.toString();
484     }
485 
486     private void pad(final int val, int max, final StringBuilder buf) {
487         while (max > 1) {
488             if (val < max) {
489                 buf.append('0');
490             }
491             max = max / TWO_DIGITS;
492         }
493         buf.append(Integer.toString(val));
494     }
495 
496     private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
497                                          final StringBuilder sb, final ListChecker checker) {
498         if ((id == null && defaultId == null) || data.discard()) {
499             return;
500         }
501 
502         sb.append('[');
503         sb.append(id);
504         if (!mdcSdId.toString().equals(id)) {
505             appendMap(prefix, data.getFields(), sb, noopChecker);
506         } else {
507             appendMap(prefix, data.getFields(), sb, checker);
508         }
509         sb.append(']');
510     }
511 
512     private String getId(final StructuredDataId id) {
513         final StringBuilder sb = new StringBuilder();
514         if (id == null || id.getName() == null) {
515             sb.append(defaultId);
516         } else {
517             sb.append(id.getName());
518         }
519         int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
520         if (ein < 0) {
521             ein = enterpriseNumber;
522         }
523         if (ein >= 0) {
524             sb.append('@').append(ein);
525         }
526         return sb.toString();
527     }
528 
529     private void checkRequired(final Map<String, String> map) {
530         for (final String key : mdcRequired) {
531             final String value = map.get(key);
532             if (value == null) {
533                 throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
534             }
535         }
536     }
537 
538     private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
539                            final ListChecker checker) {
540         final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
541         for (final Map.Entry<String, String> entry : sorted.entrySet()) {
542             if (checker.check(entry.getKey()) && entry.getValue() != null) {
543                 sb.append(' ');
544                 if (prefix != null) {
545                     sb.append(prefix);
546                 }
547                 sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"")
548                     .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"');
549             }
550         }
551     }
552 
553     private String escapeSDParams(final String value) {
554         return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
555     }
556 
557     /**
558      * Interface used to check keys in a Map.
559      */
560     private interface ListChecker {
561         boolean check(String key);
562     }
563 
564     /**
565      * Includes only the listed keys.
566      */
567     private class IncludeChecker implements ListChecker {
568         @Override
569         public boolean check(final String key) {
570             return mdcIncludes.contains(key);
571         }
572     }
573 
574     /**
575      * Excludes the listed keys.
576      */
577     private class ExcludeChecker implements ListChecker {
578         @Override
579         public boolean check(final String key) {
580             return !mdcExcludes.contains(key);
581         }
582     }
583 
584     /**
585      * Does nothing.
586      */
587     private class NoopChecker implements ListChecker {
588         @Override
589         public boolean check(final String key) {
590             return true;
591         }
592     }
593 
594     @Override
595     public String toString() {
596         final StringBuilder sb = new StringBuilder();
597         sb.append("facility=").append(facility.name());
598         sb.append(" appName=").append(appName);
599         sb.append(" defaultId=").append(defaultId);
600         sb.append(" enterpriseNumber=").append(enterpriseNumber);
601         sb.append(" newLine=").append(includeNewLine);
602         sb.append(" includeMDC=").append(includeMdc);
603         sb.append(" messageId=").append(messageId);
604         return sb.toString();
605     }
606 
607     /**
608      * Create the RFC 5424 Layout.
609      *
610      * @param facility         The Facility is used to try to classify the message.
611      * @param id               The default structured data id to use when formatting according to RFC 5424.
612      * @param enterpriseNumber The IANA enterprise number.
613      * @param includeMDC       Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
614      *                         record. Defaults to "true:.
615      * @param mdcId            The id to use for the MDC Structured Data Element.
616      * @param mdcPrefix        The prefix to add to MDC key names.
617      * @param eventPrefix      The prefix to add to event key names.
618      * @param newLine          If true, a newline will be appended to the end of the syslog record. The default is false.
619      * @param escapeNL         String that should be used to replace newlines within the message text.
620      * @param appName          The value to use as the APP-NAME in the RFC 5424 syslog record.
621      * @param msgId            The default value to be used in the MSGID field of RFC 5424 syslog records.
622      * @param excludes         A comma separated list of MDC keys that should be excluded from the LogEvent.
623      * @param includes         A comma separated list of MDC keys that should be included in the FlumeEvent.
624      * @param required         A comma separated list of MDC keys that must be present in the MDC.
625      * @param exceptionPattern The pattern for formatting exceptions.
626      * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
627      * @param loggerFields     Container for the KeyValuePairs containing the patterns
628      * @param config           The Configuration. Some Converters require access to the Interpolator.
629      * @return An Rfc5424Layout.
630      */
631     @PluginFactory
632     public static Rfc5424Layout createLayout(
633             @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
634             @PluginAttribute("id") final String id,
635             @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber,
636             @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
637             @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
638             @PluginAttribute("mdcPrefix") final String mdcPrefix,
639             @PluginAttribute("eventPrefix") final String eventPrefix,
640             @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
641             @PluginAttribute("newLineEscape") final String escapeNL,
642             @PluginAttribute("appName") final String appName,
643             @PluginAttribute("messageId") final String msgId,
644             @PluginAttribute("mdcExcludes") final String excludes,
645             @PluginAttribute("mdcIncludes") String includes,
646             @PluginAttribute("mdcRequired") final String required,
647             @PluginAttribute("exceptionPattern") final String exceptionPattern,
648             @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat, // RFC 5425
649             @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
650             @PluginConfiguration final Configuration config) {
651         final Charset charset = Charsets.UTF_8;
652         if (includes != null && excludes != null) {
653             LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
654             includes = null;
655         }
656 
657         return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix,
658                 eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern,
659                 useTlsMessageFormat, loggerFields);
660     }
661 
662     private class FieldFormatter {
663 
664         private final Map<String, List<PatternFormatter>> delegateMap;
665         private final boolean discardIfEmpty;
666 
667         public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
668             this.discardIfEmpty = discardIfEmpty;
669             this.delegateMap = fieldMap;
670         }
671 
672         public StructuredDataElement format(final LogEvent event) {
673             final Map<String, String> map = new HashMap<String, String>();
674 
675             for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
676                 final StringBuilder buffer = new StringBuilder();
677                 for (final PatternFormatter formatter : entry.getValue()) {
678                     formatter.format(event, buffer);
679                 }
680                 map.put(entry.getKey(), buffer.toString());
681             }
682             return new StructuredDataElement(map, discardIfEmpty);
683         }
684     }
685 
686     private class StructuredDataElement {
687 
688         private final Map<String, String> fields;
689         private final boolean discardIfEmpty;
690 
691         public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
692             this.discardIfEmpty = discardIfEmpty;
693             this.fields = fields;
694         }
695 
696         boolean discard() {
697             if (discardIfEmpty == false) {
698                 return false;
699             }
700             boolean foundNotEmptyValue = false;
701             for (final Map.Entry<String, String> entry : fields.entrySet()) {
702                 if (Strings.isNotEmpty(entry.getValue())) {
703                     foundNotEmptyValue = true;
704                     break;
705                 }
706             }
707             return !foundNotEmptyValue;
708         }
709 
710         void union(final Map<String, String> fields) {
711             this.fields.putAll(fields);
712         }
713 
714         Map<String, String> getFields() {
715             return this.fields;
716         }
717     }
718 }