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 long serialVersionUID = 1L;
090
091    private static final String LF = "\n";
092    private static final int TWO_DIGITS = 10;
093    private static final int THREE_DIGITS = 100;
094    private static final int MILLIS_PER_MINUTE = 60000;
095    private static final int MINUTES_PER_HOUR = 60;
096    private static final String COMPONENT_KEY = "RFC5424-Converter";
097
098    private final Facility facility;
099    private final String defaultId;
100    private final int enterpriseNumber;
101    private final boolean includeMdc;
102    private final String mdcId;
103    private final StructuredDataId mdcSdId;
104    private final String localHostName;
105    private final String appName;
106    private final String messageId;
107    private final String configName;
108    private final String mdcPrefix;
109    private final String eventPrefix;
110    private final List<String> mdcExcludes;
111    private final List<String> mdcIncludes;
112    private final List<String> mdcRequired;
113    private final ListChecker checker;
114    private final ListChecker noopChecker = new NoopChecker();
115    private final boolean includeNewLine;
116    private final String escapeNewLine;
117    private final boolean useTlsMessageFormat;
118
119    private long lastTimestamp = -1;
120    private String timestamppStr;
121
122    private final List<PatternFormatter> exceptionFormatters;
123    private final Map<String, FieldFormatter> fieldFormatters;
124
125    private Rfc5424Layout(final Configuration config, final Facility facility, final String id, final int ein,
126            final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId,
127            final String mdcPrefix, final String eventPrefix, final String appName, final String messageId,
128            final String excludes, final String includes, final String required, final Charset charset,
129            final String exceptionPattern, 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<>(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<>(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<>(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 = Strings.isNotEmpty(name) ? 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<>();
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<>();
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 = 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<>();
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 = getStringBuilder();
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
335                .getFormattedMessage();
336
337        if (text != null && text.length() > 0) {
338            buffer.append(' ').append(escapeNewlines(text, escapeNewLine));
339        }
340
341        if (exceptionFormatters != null && event.getThrown() != null) {
342            final StringBuilder exception = new StringBuilder(LF);
343            for (final PatternFormatter formatter : exceptionFormatters) {
344                formatter.format(event, exception);
345            }
346            buffer.append(escapeNewlines(exception.toString(), escapeNewLine));
347        }
348        if (includeNewLine) {
349            buffer.append(LF);
350        }
351    }
352
353    private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) {
354        final Message message = event.getMessage();
355        final boolean isStructured = message instanceof StructuredDataMessage;
356
357        if (!isStructured && (fieldFormatters != null && fieldFormatters.isEmpty()) && !includeMdc) {
358            buffer.append('-');
359            return;
360        }
361
362        final Map<String, StructuredDataElement> sdElements = new HashMap<>();
363        final Map<String, String> contextMap = event.getContextMap();
364
365        if (mdcRequired != null) {
366            checkRequired(contextMap);
367        }
368
369        if (fieldFormatters != null) {
370            for (final Map.Entry<String, FieldFormatter> sdElement : fieldFormatters.entrySet()) {
371                final String sdId = sdElement.getKey();
372                final StructuredDataElement elem = sdElement.getValue().format(event);
373                sdElements.put(sdId, elem);
374            }
375        }
376
377        if (includeMdc && contextMap.size() > 0) {
378            final String mdcSdIdStr = mdcSdId.toString();
379            final StructuredDataElement union = sdElements.get(mdcSdIdStr);
380            if (union != null) {
381                union.union(contextMap);
382                sdElements.put(mdcSdIdStr, union);
383            } else {
384                final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false);
385                sdElements.put(mdcSdIdStr, formattedContextMap);
386            }
387        }
388
389        if (isStructured) {
390            final StructuredDataMessage data = (StructuredDataMessage) message;
391            final Map<String, String> map = data.getData();
392            final StructuredDataId id = data.getId();
393            final String sdId = getId(id);
394
395            if (sdElements.containsKey(sdId)) {
396                final StructuredDataElement union = sdElements.get(id.toString());
397                union.union(map);
398                sdElements.put(sdId, union);
399            } else {
400                final StructuredDataElement formattedData = new StructuredDataElement(map, false);
401                sdElements.put(sdId, formattedData);
402            }
403        }
404
405        if (sdElements.isEmpty()) {
406            buffer.append('-');
407            return;
408        }
409
410        for (final Map.Entry<String, StructuredDataElement> entry : sdElements.entrySet()) {
411            formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, checker);
412        }
413    }
414
415    private String escapeNewlines(final String text, final String escapeNewLine) {
416        if (null == escapeNewLine) {
417            return text;
418        }
419        return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine);
420    }
421
422    protected String getProcId() {
423        return "-";
424    }
425
426    protected List<String> getMdcExcludes() {
427        return mdcExcludes;
428    }
429
430    protected List<String> getMdcIncludes() {
431        return mdcIncludes;
432    }
433
434    private String computeTimeStampString(final long now) {
435        long last;
436        synchronized (this) {
437            last = lastTimestamp;
438            if (now == lastTimestamp) {
439                return timestamppStr;
440            }
441        }
442
443        final StringBuilder buffer = new StringBuilder();
444        final Calendar cal = new GregorianCalendar();
445        cal.setTimeInMillis(now);
446        buffer.append(Integer.toString(cal.get(Calendar.YEAR)));
447        buffer.append('-');
448        pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer);
449        buffer.append('-');
450        pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer);
451        buffer.append('T');
452        pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer);
453        buffer.append(':');
454        pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer);
455        buffer.append(':');
456        pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer);
457        buffer.append('.');
458        pad(cal.get(Calendar.MILLISECOND), THREE_DIGITS, buffer);
459
460        int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
461        if (tzmin == 0) {
462            buffer.append('Z');
463        } else {
464            if (tzmin < 0) {
465                tzmin = -tzmin;
466                buffer.append('-');
467            } else {
468                buffer.append('+');
469            }
470            final int tzhour = tzmin / MINUTES_PER_HOUR;
471            tzmin -= tzhour * MINUTES_PER_HOUR;
472            pad(tzhour, TWO_DIGITS, buffer);
473            buffer.append(':');
474            pad(tzmin, TWO_DIGITS, buffer);
475        }
476        synchronized (this) {
477            if (last == lastTimestamp) {
478                lastTimestamp = now;
479                timestamppStr = buffer.toString();
480            }
481        }
482        return buffer.toString();
483    }
484
485    private void pad(final int val, int max, final StringBuilder buf) {
486        while (max > 1) {
487            if (val < max) {
488                buf.append('0');
489            }
490            max = max / TWO_DIGITS;
491        }
492        buf.append(Integer.toString(val));
493    }
494
495    private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
496            final StringBuilder sb, final ListChecker checker) {
497        if ((id == null && defaultId == null) || data.discard()) {
498            return;
499        }
500
501        sb.append('[');
502        sb.append(id);
503        if (!mdcSdId.toString().equals(id)) {
504            appendMap(prefix, data.getFields(), sb, noopChecker);
505        } else {
506            appendMap(prefix, data.getFields(), sb, checker);
507        }
508        sb.append(']');
509    }
510
511    private String getId(final StructuredDataId id) {
512        final StringBuilder sb = new StringBuilder();
513        if (id == null || id.getName() == null) {
514            sb.append(defaultId);
515        } else {
516            sb.append(id.getName());
517        }
518        int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
519        if (ein < 0) {
520            ein = enterpriseNumber;
521        }
522        if (ein >= 0) {
523            sb.append('@').append(ein);
524        }
525        return sb.toString();
526    }
527
528    private void checkRequired(final Map<String, String> map) {
529        for (final String key : mdcRequired) {
530            final String value = map.get(key);
531            if (value == null) {
532                throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
533            }
534        }
535    }
536
537    private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
538            final ListChecker checker) {
539        final SortedMap<String, String> sorted = new TreeMap<>(map);
540        for (final Map.Entry<String, String> entry : sorted.entrySet()) {
541            if (checker.check(entry.getKey()) && entry.getValue() != null) {
542                sb.append(' ');
543                if (prefix != null) {
544                    sb.append(prefix);
545                }
546                final String safeKey = escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine);
547                final String safeValue = escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine);
548                StringBuilders.appendKeyDqValue(sb, safeKey, safeValue);
549            }
550        }
551    }
552
553    private String escapeSDParams(final String value) {
554        return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
555    }
556
557    /**
558     * Interface used to check keys in a Map.
559     */
560    private interface ListChecker {
561        boolean check(String key);
562    }
563
564    /**
565     * Includes only the listed keys.
566     */
567    private class IncludeChecker implements ListChecker {
568        @Override
569        public boolean check(final String key) {
570            return mdcIncludes.contains(key);
571        }
572    }
573
574    /**
575     * Excludes the listed keys.
576     */
577    private class ExcludeChecker implements ListChecker {
578        @Override
579        public boolean check(final String key) {
580            return !mdcExcludes.contains(key);
581        }
582    }
583
584    /**
585     * Does nothing.
586     */
587    private class NoopChecker implements ListChecker {
588        @Override
589        public boolean check(final String key) {
590            return true;
591        }
592    }
593
594    @Override
595    public String toString() {
596        final StringBuilder sb = new StringBuilder();
597        sb.append("facility=").append(facility.name());
598        sb.append(" appName=").append(appName);
599        sb.append(" defaultId=").append(defaultId);
600        sb.append(" enterpriseNumber=").append(enterpriseNumber);
601        sb.append(" newLine=").append(includeNewLine);
602        sb.append(" includeMDC=").append(includeMdc);
603        sb.append(" messageId=").append(messageId);
604        return sb.toString();
605    }
606
607    /**
608     * Create the RFC 5424 Layout.
609     *
610     * @param facility The Facility is used to try to classify the message.
611     * @param id The default structured data id to use when formatting according to RFC 5424.
612     * @param enterpriseNumber The IANA enterprise number.
613     * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
614     *            record. Defaults to "true:.
615     * @param mdcId The id to use for the MDC Structured Data Element.
616     * @param mdcPrefix The prefix to add to MDC key names.
617     * @param eventPrefix The prefix to add to event key names.
618     * @param newLine If true, a newline will be appended to the end of the syslog record. The default is false.
619     * @param escapeNL String that should be used to replace newlines within the message text.
620     * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record.
621     * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records.
622     * @param excludes A comma separated list of MDC keys that should be excluded from the LogEvent.
623     * @param includes A comma separated list of MDC keys that should be included in the FlumeEvent.
624     * @param required A comma separated list of MDC keys that must be present in the MDC.
625     * @param exceptionPattern The pattern for formatting exceptions.
626     * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
627     * @param loggerFields Container for the KeyValuePairs containing the patterns
628     * @param config The Configuration. Some Converters require access to the Interpolator.
629     * @return An Rfc5424Layout.
630     */
631    @PluginFactory
632    public static Rfc5424Layout createLayout(
633            // @formatter:off
634            @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
635            @PluginAttribute("id") final String id,
636            @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER)
637            final int enterpriseNumber,
638            @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
639            @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
640            @PluginAttribute("mdcPrefix") final String mdcPrefix,
641            @PluginAttribute("eventPrefix") final String eventPrefix,
642            @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
643            @PluginAttribute("newLineEscape") final String escapeNL,
644            @PluginAttribute("appName") final String appName,
645            @PluginAttribute("messageId") final String msgId,
646            @PluginAttribute("mdcExcludes") final String excludes,
647            @PluginAttribute("mdcIncludes") String includes,
648            @PluginAttribute("mdcRequired") final String required,
649            @PluginAttribute("exceptionPattern") final String exceptionPattern,
650            // RFC 5425
651            @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat,
652            @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
653            @PluginConfiguration final Configuration config) {
654        // @formatter:on
655        if (includes != null && excludes != null) {
656            LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
657            includes = null;
658        }
659
660        return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId,
661                mdcPrefix, eventPrefix, appName, msgId, excludes, includes, required, StandardCharsets.UTF_8,
662                exceptionPattern, 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<>();
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}