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