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