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.nio.charset.StandardCharsets;
021import java.util.ArrayList;
022import java.util.Calendar;
023import java.util.GregorianCalendar;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.SortedMap;
028import java.util.TreeMap;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import org.apache.logging.log4j.Level;
033import org.apache.logging.log4j.LoggingException;
034import org.apache.logging.log4j.core.Layout;
035import org.apache.logging.log4j.core.LogEvent;
036import org.apache.logging.log4j.core.appender.TlsSyslogFrame;
037import org.apache.logging.log4j.core.config.Configuration;
038import org.apache.logging.log4j.core.config.Node;
039import org.apache.logging.log4j.core.config.plugins.Plugin;
040import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
041import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
042import org.apache.logging.log4j.core.config.plugins.PluginElement;
043import org.apache.logging.log4j.core.config.plugins.PluginFactory;
044import org.apache.logging.log4j.core.net.Facility;
045import org.apache.logging.log4j.core.net.Priority;
046import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
047import org.apache.logging.log4j.core.pattern.PatternConverter;
048import org.apache.logging.log4j.core.pattern.PatternFormatter;
049import org.apache.logging.log4j.core.pattern.PatternParser;
050import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
051import org.apache.logging.log4j.core.util.NetUtils;
052import org.apache.logging.log4j.core.util.Patterns;
053import org.apache.logging.log4j.message.Message;
054import org.apache.logging.log4j.message.StructuredDataId;
055import org.apache.logging.log4j.message.StructuredDataMessage;
056import org.apache.logging.log4j.util.StringBuilders;
057import org.apache.logging.log4j.util.Strings;
058
059/**
060 * Formats a log event in accordance with RFC 5424.
061 *
062 * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
063 */
064@Plugin(name = "Rfc5424Layout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
065public final class Rfc5424Layout extends AbstractStringLayout {
066
067    /**
068     * Not a very good default - it is the Apache Software Foundation's enterprise number.
069     */
070    public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
071    /**
072     * The default event id.
073     */
074    public static final String DEFAULT_ID = "Audit";
075    /**
076     * Match newlines in a platform-independent manner.
077     */
078    public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
079    /**
080     * Match characters which require escaping.
081     */
082    public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
083
084    /**
085     * Default MDC ID: {@value} .
086     */
087    public static final String DEFAULT_MDCID = "mdc";
088
089    private static final String LF = "\n";
090    private static final int TWO_DIGITS = 10;
091    private static final int THREE_DIGITS = 100;
092    private static final int MILLIS_PER_MINUTE = 60000;
093    private static final int MINUTES_PER_HOUR = 60;
094    private static final String COMPONENT_KEY = "RFC5424-Converter";
095
096    private final Facility facility;
097    private final String defaultId;
098    private final int enterpriseNumber;
099    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 listChecker;
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    private final String procId;
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, final String appName, final String messageId,
127            final String excludes, final String includes, final String required, final Charset charset,
128            final String exceptionPattern, 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<>(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<>(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<>(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.listChecker = c != null ? c : noopChecker;
190        final String name = config == null ? null : config.getName();
191        configName = Strings.isNotEmpty(name) ? name : null;
192        this.fieldFormatters = createFieldFormatters(loggerFields, config);
193        // TODO Java 9: ProcessHandle.current().getPid();
194        this.procId = "-";
195    }
196
197    private Map<String, FieldFormatter> createFieldFormatters(final LoggerFields[] loggerFields,
198            final Configuration config) {
199        final Map<String, FieldFormatter> sdIdMap = new HashMap<>();
200
201        if (loggerFields != null) {
202            for (final LoggerFields lField : loggerFields) {
203                final StructuredDataId key = lField.getSdId() == null ? mdcSdId : lField.getSdId();
204                final Map<String, List<PatternFormatter>> sdParams = new HashMap<>();
205                final Map<String, String> fields = lField.getMap();
206                if (!fields.isEmpty()) {
207                    final PatternParser fieldParser = createPatternParser(config, null);
208
209                    for (final Map.Entry<String, String> entry : fields.entrySet()) {
210                        final List<PatternFormatter> formatters = fieldParser.parse(entry.getValue(), false, false);
211                        sdParams.put(entry.getKey(), formatters);
212                    }
213                    final FieldFormatter fieldFormatter = new FieldFormatter(sdParams,
214                            lField.getDiscardIfAllFieldsAreEmpty());
215                    sdIdMap.put(key.toString(), fieldFormatter);
216                }
217            }
218        }
219        return sdIdMap.size() > 0 ? sdIdMap : null;
220    }
221
222    /**
223     * Create a PatternParser.
224     *
225     * @param config The Configuration.
226     * @param filterClass Filter the returned plugins after calling the plugin manager.
227     * @return The PatternParser.
228     */
229    private static PatternParser createPatternParser(final Configuration config,
230            final Class<? extends PatternConverter> filterClass) {
231        if (config == null) {
232            return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class, filterClass);
233        }
234        PatternParser parser = config.getComponent(COMPONENT_KEY);
235        if (parser == null) {
236            parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class);
237            config.addComponent(COMPONENT_KEY, parser);
238            parser = config.getComponent(COMPONENT_KEY);
239        }
240        return parser;
241    }
242
243    /**
244     * Gets this Rfc5424Layout's content format. Specified by:
245     * <ul>
246     * <li>Key: "structured" Value: "true"</li>
247     * <li>Key: "format" Value: "RFC5424"</li>
248     * </ul>
249     * 
250     * @return Map of content format keys supporting Rfc5424Layout
251     */
252    @Override
253    public Map<String, String> getContentFormat() {
254        final Map<String, String> result = new HashMap<>();
255        result.put("structured", "true");
256        result.put("formatType", "RFC5424");
257        return result;
258    }
259
260    /**
261     * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification.
262     *
263     * @param event The LogEvent.
264     * @return The RFC 5424 String representation of the LogEvent.
265     */
266    @Override
267    public String toSerializable(final LogEvent event) {
268        final StringBuilder buf = getStringBuilder();
269        appendPriority(buf, event.getLevel());
270        appendTimestamp(buf, event.getTimeMillis());
271        appendSpace(buf);
272        appendHostName(buf);
273        appendSpace(buf);
274        appendAppName(buf);
275        appendSpace(buf);
276        appendProcessId(buf);
277        appendSpace(buf);
278        appendMessageId(buf, event.getMessage());
279        appendSpace(buf);
280        appendStructuredElements(buf, event);
281        appendMessage(buf, event);
282        if (useTlsMessageFormat) {
283            return new TlsSyslogFrame(buf.toString()).toString();
284        }
285        return buf.toString();
286    }
287
288    private void appendPriority(final StringBuilder buffer, final Level logLevel) {
289        buffer.append('<');
290        buffer.append(Priority.getPriority(facility, logLevel));
291        buffer.append(">1 ");
292    }
293
294    private void appendTimestamp(final StringBuilder buffer, final long milliseconds) {
295        buffer.append(computeTimeStampString(milliseconds));
296    }
297
298    private void appendSpace(final StringBuilder buffer) {
299        buffer.append(' ');
300    }
301
302    private void appendHostName(final StringBuilder buffer) {
303        buffer.append(localHostName);
304    }
305
306    private void appendAppName(final StringBuilder buffer) {
307        if (appName != null) {
308            buffer.append(appName);
309        } else if (configName != null) {
310            buffer.append(configName);
311        } else {
312            buffer.append('-');
313        }
314    }
315
316    private void appendProcessId(final StringBuilder buffer) {
317        buffer.append(getProcId());
318    }
319
320    private void appendMessageId(final StringBuilder buffer, final Message message) {
321        final boolean isStructured = message instanceof StructuredDataMessage;
322        final String type = isStructured ? ((StructuredDataMessage) message).getType() : null;
323        if (type != null) {
324            buffer.append(type);
325        } else if (messageId != null) {
326            buffer.append(messageId);
327        } else {
328            buffer.append('-');
329        }
330    }
331
332    private void appendMessage(final StringBuilder buffer, final LogEvent event) {
333        final Message message = event.getMessage();
334        // This layout formats StructuredDataMessages instead of delegating to the Message itself.
335        final String text = (message instanceof StructuredDataMessage) ? message.getFormat() : message
336                .getFormattedMessage();
337
338        if (text != null && text.length() > 0) {
339            buffer.append(' ').append(escapeNewlines(text, escapeNewLine));
340        }
341
342        if (exceptionFormatters != null && event.getThrown() != null) {
343            final StringBuilder exception = new StringBuilder(LF);
344            for (final PatternFormatter formatter : exceptionFormatters) {
345                formatter.format(event, exception);
346            }
347            buffer.append(escapeNewlines(exception.toString(), escapeNewLine));
348        }
349        if (includeNewLine) {
350            buffer.append(LF);
351        }
352    }
353
354    private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) {
355        final Message message = event.getMessage();
356        final boolean isStructured = message instanceof StructuredDataMessage;
357
358        if (!isStructured && (fieldFormatters != null && fieldFormatters.isEmpty()) && !includeMdc) {
359            buffer.append('-');
360            return;
361        }
362
363        final Map<String, StructuredDataElement> sdElements = new HashMap<>();
364        final Map<String, String> contextMap = event.getContextMap();
365
366        if (mdcRequired != null) {
367            checkRequired(contextMap);
368        }
369
370        if (fieldFormatters != null) {
371            for (final Map.Entry<String, FieldFormatter> sdElement : fieldFormatters.entrySet()) {
372                final String sdId = sdElement.getKey();
373                final StructuredDataElement elem = sdElement.getValue().format(event);
374                sdElements.put(sdId, elem);
375            }
376        }
377
378        if (includeMdc && contextMap.size() > 0) {
379            final String mdcSdIdStr = mdcSdId.toString();
380            final StructuredDataElement union = sdElements.get(mdcSdIdStr);
381            if (union != null) {
382                union.union(contextMap);
383                sdElements.put(mdcSdIdStr, union);
384            } else {
385                final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false);
386                sdElements.put(mdcSdIdStr, formattedContextMap);
387            }
388        }
389
390        if (isStructured) {
391            final StructuredDataMessage data = (StructuredDataMessage) message;
392            final Map<String, String> map = data.getData();
393            final StructuredDataId id = data.getId();
394            final String sdId = getId(id);
395
396            if (sdElements.containsKey(sdId)) {
397                final StructuredDataElement union = sdElements.get(id.toString());
398                union.union(map);
399                sdElements.put(sdId, union);
400            } else {
401                final StructuredDataElement formattedData = new StructuredDataElement(map, false);
402                sdElements.put(sdId, formattedData);
403            }
404        }
405
406        if (sdElements.isEmpty()) {
407            buffer.append('-');
408            return;
409        }
410
411        for (final Map.Entry<String, StructuredDataElement> entry : sdElements.entrySet()) {
412            formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, listChecker);
413        }
414    }
415
416    private String escapeNewlines(final String text, final String replacement) {
417        if (null == replacement) {
418            return text;
419        }
420        return NEWLINE_PATTERN.matcher(text).replaceAll(replacement);
421    }
422
423    protected String getProcId() {
424        return procId;
425    }
426
427    protected List<String> getMdcExcludes() {
428        return mdcExcludes;
429    }
430
431    protected List<String> getMdcIncludes() {
432        return mdcIncludes;
433    }
434
435    private String computeTimeStampString(final long now) {
436        long last;
437        synchronized (this) {
438            last = lastTimestamp;
439            if (now == lastTimestamp) {
440                return timestamppStr;
441            }
442        }
443
444        final StringBuilder buffer = new StringBuilder();
445        final Calendar cal = new GregorianCalendar();
446        cal.setTimeInMillis(now);
447        buffer.append(Integer.toString(cal.get(Calendar.YEAR)));
448        buffer.append('-');
449        pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer);
450        buffer.append('-');
451        pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer);
452        buffer.append('T');
453        pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer);
454        buffer.append(':');
455        pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer);
456        buffer.append(':');
457        pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer);
458        buffer.append('.');
459        pad(cal.get(Calendar.MILLISECOND), THREE_DIGITS, buffer);
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<>(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                final String safeKey = escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine);
548                final String safeValue = escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine);
549                StringBuilders.appendKeyDqValue(sb, safeKey, safeValue);
550            }
551        }
552    }
553
554    private String escapeSDParams(final String value) {
555        return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
556    }
557
558    /**
559     * Interface used to check keys in a Map.
560     */
561    private interface ListChecker {
562        boolean check(String key);
563    }
564
565    /**
566     * Includes only the listed keys.
567     */
568    private class IncludeChecker implements ListChecker {
569        @Override
570        public boolean check(final String key) {
571            return mdcIncludes.contains(key);
572        }
573    }
574
575    /**
576     * Excludes the listed keys.
577     */
578    private class ExcludeChecker implements ListChecker {
579        @Override
580        public boolean check(final String key) {
581            return !mdcExcludes.contains(key);
582        }
583    }
584
585    /**
586     * Does nothing.
587     */
588    private class NoopChecker implements ListChecker {
589        @Override
590        public boolean check(final String key) {
591            return true;
592        }
593    }
594
595    @Override
596    public String toString() {
597        final StringBuilder sb = new StringBuilder();
598        sb.append("facility=").append(facility.name());
599        sb.append(" appName=").append(appName);
600        sb.append(" defaultId=").append(defaultId);
601        sb.append(" enterpriseNumber=").append(enterpriseNumber);
602        sb.append(" newLine=").append(includeNewLine);
603        sb.append(" includeMDC=").append(includeMdc);
604        sb.append(" messageId=").append(messageId);
605        return sb.toString();
606    }
607
608    /**
609     * Create the RFC 5424 Layout.
610     *
611     * @param facility The Facility is used to try to classify the message.
612     * @param id The default structured data id to use when formatting according to RFC 5424.
613     * @param enterpriseNumber The IANA enterprise number.
614     * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
615     *            record. Defaults to "true:.
616     * @param mdcId The id to use for the MDC Structured Data Element.
617     * @param mdcPrefix The prefix to add to MDC key names.
618     * @param eventPrefix The prefix to add to event key names.
619     * @param newLine If true, a newline will be appended to the end of the syslog record. The default is false.
620     * @param escapeNL String that should be used to replace newlines within the message text.
621     * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record.
622     * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records.
623     * @param excludes A comma separated list of MDC keys that should be excluded from the LogEvent.
624     * @param includes A comma separated list of MDC keys that should be included in the FlumeEvent.
625     * @param required A comma separated list of MDC keys that must be present in the MDC.
626     * @param exceptionPattern The pattern for formatting exceptions.
627     * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
628     * @param loggerFields Container for the KeyValuePairs containing the patterns
629     * @param config The Configuration. Some Converters require access to the Interpolator.
630     * @return An Rfc5424Layout.
631     */
632    @PluginFactory
633    public static Rfc5424Layout createLayout(
634            // @formatter:off
635            @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
636            @PluginAttribute("id") final String id,
637            @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER)
638            final int enterpriseNumber,
639            @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
640            @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
641            @PluginAttribute("mdcPrefix") final String mdcPrefix,
642            @PluginAttribute("eventPrefix") final String eventPrefix,
643            @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
644            @PluginAttribute("newLineEscape") final String escapeNL,
645            @PluginAttribute("appName") final String appName,
646            @PluginAttribute("messageId") final String msgId,
647            @PluginAttribute("mdcExcludes") final String excludes,
648            @PluginAttribute("mdcIncludes") String includes,
649            @PluginAttribute("mdcRequired") final String required,
650            @PluginAttribute("exceptionPattern") final String exceptionPattern,
651            // RFC 5425
652            @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat,
653            @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
654            @PluginConfiguration final Configuration config) {
655        // @formatter:on
656        if (includes != null && excludes != null) {
657            LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
658            includes = null;
659        }
660
661        return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId,
662                mdcPrefix, eventPrefix, appName, msgId, excludes, includes, required, StandardCharsets.UTF_8,
663                exceptionPattern, useTlsMessageFormat, loggerFields);
664    }
665
666    private class FieldFormatter {
667
668        private final Map<String, List<PatternFormatter>> delegateMap;
669        private final boolean discardIfEmpty;
670
671        public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
672            this.discardIfEmpty = discardIfEmpty;
673            this.delegateMap = fieldMap;
674        }
675
676        public StructuredDataElement format(final LogEvent event) {
677            final Map<String, String> map = new HashMap<>();
678
679            for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
680                final StringBuilder buffer = new StringBuilder();
681                for (final PatternFormatter formatter : entry.getValue()) {
682                    formatter.format(event, buffer);
683                }
684                map.put(entry.getKey(), buffer.toString());
685            }
686            return new StructuredDataElement(map, discardIfEmpty);
687        }
688    }
689
690    private class StructuredDataElement {
691
692        private final Map<String, String> fields;
693        private final boolean discardIfEmpty;
694
695        public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
696            this.discardIfEmpty = discardIfEmpty;
697            this.fields = fields;
698        }
699
700        boolean discard() {
701            if (discardIfEmpty == false) {
702                return false;
703            }
704            boolean foundNotEmptyValue = false;
705            for (final Map.Entry<String, String> entry : fields.entrySet()) {
706                if (Strings.isNotEmpty(entry.getValue())) {
707                    foundNotEmptyValue = true;
708                    break;
709                }
710            }
711            return !foundNotEmptyValue;
712        }
713
714        void union(final Map<String, String> addFields) {
715            this.fields.putAll(addFields);
716        }
717
718        Map<String, String> getFields() {
719            return this.fields;
720        }
721    }
722}