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