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