001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.layout;
018
019import java.nio.charset.Charset;
020import java.util.ArrayList;
021import java.util.Calendar;
022import java.util.GregorianCalendar;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.SortedMap;
027import java.util.TreeMap;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.logging.log4j.Level;
032import org.apache.logging.log4j.LoggingException;
033import org.apache.logging.log4j.core.Layout;
034import org.apache.logging.log4j.core.LogEvent;
035import org.apache.logging.log4j.core.appender.TlsSyslogFrame;
036import org.apache.logging.log4j.core.config.Configuration;
037import org.apache.logging.log4j.core.config.Node;
038import org.apache.logging.log4j.core.config.plugins.Plugin;
039import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
040import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
041import org.apache.logging.log4j.core.config.plugins.PluginElement;
042import org.apache.logging.log4j.core.config.plugins.PluginFactory;
043import org.apache.logging.log4j.core.net.Facility;
044import org.apache.logging.log4j.core.net.Priority;
045import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
046import org.apache.logging.log4j.core.pattern.PatternConverter;
047import org.apache.logging.log4j.core.pattern.PatternFormatter;
048import org.apache.logging.log4j.core.pattern.PatternParser;
049import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
050import org.apache.logging.log4j.core.util.Constants;
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/**
061 * Formats a log event in accordance with RFC 5424.
062 *
063 * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
064 */
065@Plugin(name = "Rfc5424Layout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
066public final class Rfc5424Layout extends AbstractStringLayout {
067
068    private static final long serialVersionUID = 1L;
069
070    private static final String LF = "\n";
071
072    /**
073     * Not a very good default - it is the Apache Software Foundation's enterprise number.
074     */
075    public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
076    /**
077     * The default event id.
078     */
079    public static final String DEFAULT_ID = "Audit";
080    /**
081     * Match newlines in a platform-independent manner.
082     */
083    public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
084    /**
085     * Match characters which require escaping
086     */
087    public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
088
089    public static final String DEFAULT_MDCID = "mdc";
090    private static final int TWO_DIGITS = 10;
091    private static final int THREE_DIGITS = 100;
092    private static final int MILLIS_PER_MINUTE = 60000;
093    private static final int MINUTES_PER_HOUR = 60;
094
095    private static final String COMPONENT_KEY = "RFC5424-Converter";
096
097    private final Facility facility;
098    private final String defaultId;
099    private final int enterpriseNumber;
100    private final boolean includeMdc;
101    private final String mdcId;
102    private final StructuredDataId mdcSdId;
103    private final String localHostName;
104    private final String appName;
105    private final String messageId;
106    private final String configName;
107    private final String mdcPrefix;
108    private final String eventPrefix;
109    private final List<String> mdcExcludes;
110    private final List<String> mdcIncludes;
111    private final List<String> mdcRequired;
112    private final ListChecker checker;
113    private final ListChecker noopChecker = new NoopChecker();
114    private final boolean includeNewLine;
115    private final String escapeNewLine;
116    private final boolean useTlsMessageFormat;
117
118    private long lastTimestamp = -1;
119    private String timestamppStr;
120
121    private final List<PatternFormatter> exceptionFormatters;
122    private final Map<String,  FieldFormatter> fieldFormatters;
123
124    private Rfc5424Layout(final Configuration config, final Facility facility, final String id, final int ein,
125                          final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId,
126                          final String mdcPrefix, final String eventPrefix,
127                          final String appName, final String messageId, final String excludes, final String includes,
128                          final String required, final Charset charset, final String exceptionPattern,
129                          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<String>(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<String>(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<String>(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 = name != null && name.length() > 0 ? 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<String, FieldFormatter>();
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<String, List<PatternFormatter>>();
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 = (PatternParser) 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<String, String>();
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 = new StringBuilder();
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.getFormattedMessage();
335
336        if (text != null && text.length() > 0) {
337            buffer.append(' ').append(escapeNewlines(text, escapeNewLine));
338        }
339
340        if (exceptionFormatters != null && event.getThrown() != null) {
341            final StringBuilder exception = new StringBuilder(LF);
342            for (final PatternFormatter formatter : exceptionFormatters) {
343                formatter.format(event, exception);
344            }
345            buffer.append(escapeNewlines(exception.toString(), escapeNewLine));
346        }
347        if (includeNewLine) {
348            buffer.append(LF);
349        }
350    }
351
352    private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) {
353        final Message message = event.getMessage();
354        final boolean isStructured = message instanceof StructuredDataMessage;
355
356        if (!isStructured && (fieldFormatters!= null && fieldFormatters.isEmpty()) && !includeMdc) {
357            buffer.append('-');
358            return;
359        }
360
361        final Map<String, StructuredDataElement> sdElements = new HashMap<String, StructuredDataElement>();
362        final Map<String, String> contextMap = event.getContextMap();
363
364        if (mdcRequired != null) {
365            checkRequired(contextMap);
366        }
367
368        if (fieldFormatters != null) {
369            for (final Map.Entry<String, FieldFormatter> sdElement: fieldFormatters.entrySet()) {
370                final String sdId = sdElement.getKey();
371                final StructuredDataElement elem = sdElement.getValue().format(event);
372                sdElements.put(sdId, elem);
373            }
374        }
375
376        if (includeMdc && contextMap.size() > 0) {
377            if (sdElements.containsKey(mdcSdId.toString())) {
378                final StructuredDataElement union = sdElements.get(mdcSdId.toString());
379                union.union(contextMap);
380                sdElements.put(mdcSdId.toString(), union);
381            } else {
382                final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false);
383                sdElements.put(mdcSdId.toString(), formattedContextMap);
384            }
385        }
386
387        if (isStructured) {
388            final StructuredDataMessage data = (StructuredDataMessage) message;
389            final Map<String, String> map = data.getData();
390            final StructuredDataId id = data.getId();
391            final String sdId = getId(id);
392
393            if (sdElements.containsKey(sdId)) {
394                final StructuredDataElement union = sdElements.get(id.toString());
395                union.union(map);
396                sdElements.put(sdId, union);
397            } else {
398                final StructuredDataElement formattedData = new StructuredDataElement(map, false);
399                sdElements.put(sdId, formattedData);
400            }
401        }
402
403        if (sdElements.isEmpty()) {
404            buffer.append('-');
405            return;
406        }
407
408        for (final Map.Entry<String, StructuredDataElement> entry: sdElements.entrySet()) {
409            formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, checker);
410        }
411    }
412
413    private String escapeNewlines(final String text, final String escapeNewLine) {
414        if (null == escapeNewLine) {
415            return text;
416        }
417        return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine);
418    }
419
420    protected String getProcId() {
421        return "-";
422    }
423
424    protected List<String> getMdcExcludes() {
425        return mdcExcludes;
426    }
427
428    protected List<String> getMdcIncludes() {
429        return mdcIncludes;
430    }
431
432    private String computeTimeStampString(final long now) {
433        long last;
434        synchronized (this) {
435            last = lastTimestamp;
436            if (now == lastTimestamp) {
437                return timestamppStr;
438            }
439        }
440
441        final StringBuilder buffer = new StringBuilder();
442        final Calendar cal = new GregorianCalendar();
443        cal.setTimeInMillis(now);
444        buffer.append(Integer.toString(cal.get(Calendar.YEAR)));
445        buffer.append('-');
446        pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer);
447        buffer.append('-');
448        pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer);
449        buffer.append('T');
450        pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer);
451        buffer.append(':');
452        pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer);
453        buffer.append(':');
454        pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer);
455        buffer.append('.');
456        pad(cal.get(Calendar.MILLISECOND), THREE_DIGITS, buffer);
457
458        int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
459        if (tzmin == 0) {
460            buffer.append('Z');
461        } else {
462            if (tzmin < 0) {
463                tzmin = -tzmin;
464                buffer.append('-');
465            } else {
466                buffer.append('+');
467            }
468            final int tzhour = tzmin / MINUTES_PER_HOUR;
469            tzmin -= tzhour * MINUTES_PER_HOUR;
470            pad(tzhour, TWO_DIGITS, buffer);
471            buffer.append(':');
472            pad(tzmin, TWO_DIGITS, buffer);
473        }
474        synchronized (this) {
475            if (last == lastTimestamp) {
476                lastTimestamp = now;
477                timestamppStr = buffer.toString();
478            }
479        }
480        return buffer.toString();
481    }
482
483    private void pad(final int val, int max, final StringBuilder buf) {
484        while (max > 1) {
485            if (val < max) {
486                buf.append('0');
487            }
488            max = max / TWO_DIGITS;
489        }
490        buf.append(Integer.toString(val));
491    }
492
493    private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
494                                         final StringBuilder sb, final ListChecker checker) {
495        if ((id == null && defaultId == null) || data.discard()) {
496            return;
497        }
498
499        sb.append('[');
500        sb.append(id);
501        if (!mdcSdId.toString().equals(id)) {
502            appendMap(prefix, data.getFields(), sb, noopChecker);
503        } else {
504            appendMap(prefix, data.getFields(), sb, checker);
505        }
506        sb.append(']');
507    }
508
509    private String getId(final StructuredDataId id) {
510        final StringBuilder sb = new StringBuilder();
511        if (id == null || id.getName() == null) {
512            sb.append(defaultId);
513        } else {
514            sb.append(id.getName());
515        }
516        int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
517        if (ein < 0) {
518            ein = enterpriseNumber;
519        }
520        if (ein >= 0) {
521            sb.append('@').append(ein);
522        }
523        return sb.toString();
524    }
525
526    private void checkRequired(final Map<String, String> map) {
527        for (final String key : mdcRequired) {
528            final String value = map.get(key);
529            if (value == null) {
530                throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
531            }
532        }
533    }
534
535    private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
536            final ListChecker checker) {
537        final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
538        for (final Map.Entry<String, String> entry : sorted.entrySet()) {
539            if (checker.check(entry.getKey()) && entry.getValue() != null) {
540                sb.append(' ');
541                if (prefix != null) {
542                    sb.append(prefix);
543                }
544                final String safeKey = escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine);
545                final String safeValue = escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine);
546                StringBuilders.appendKeyDqValue(sb, safeKey, safeValue);
547            }
548        }
549    }
550
551    private String escapeSDParams(final String value) {
552        return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
553    }
554
555    /**
556     * Interface used to check keys in a Map.
557     */
558    private interface ListChecker {
559        boolean check(String key);
560    }
561
562    /**
563     * Includes only the listed keys.
564     */
565    private class IncludeChecker implements ListChecker {
566        @Override
567        public boolean check(final String key) {
568            return mdcIncludes.contains(key);
569        }
570    }
571
572    /**
573     * Excludes the listed keys.
574     */
575    private class ExcludeChecker implements ListChecker {
576        @Override
577        public boolean check(final String key) {
578            return !mdcExcludes.contains(key);
579        }
580    }
581
582    /**
583     * Does nothing.
584     */
585    private class NoopChecker implements ListChecker {
586        @Override
587        public boolean check(final String key) {
588            return true;
589        }
590    }
591
592    @Override
593    public String toString() {
594        final StringBuilder sb = new StringBuilder();
595        sb.append("facility=").append(facility.name());
596        sb.append(" appName=").append(appName);
597        sb.append(" defaultId=").append(defaultId);
598        sb.append(" enterpriseNumber=").append(enterpriseNumber);
599        sb.append(" newLine=").append(includeNewLine);
600        sb.append(" includeMDC=").append(includeMdc);
601        sb.append(" messageId=").append(messageId);
602        return sb.toString();
603    }
604
605    /**
606     * Create the RFC 5424 Layout.
607     *
608     * @param facility         The Facility is used to try to classify the message.
609     * @param id               The default structured data id to use when formatting according to RFC 5424.
610     * @param enterpriseNumber The IANA enterprise number.
611     * @param includeMDC       Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
612     *                         record. Defaults to "true:.
613     * @param mdcId            The id to use for the MDC Structured Data Element.
614     * @param mdcPrefix        The prefix to add to MDC key names.
615     * @param eventPrefix      The prefix to add to event key names.
616     * @param newLine          If true, a newline will be appended to the end of the syslog record. The default is false.
617     * @param escapeNL         String that should be used to replace newlines within the message text.
618     * @param appName          The value to use as the APP-NAME in the RFC 5424 syslog record.
619     * @param msgId            The default value to be used in the MSGID field of RFC 5424 syslog records.
620     * @param excludes         A comma separated list of MDC keys that should be excluded from the LogEvent.
621     * @param includes         A comma separated list of MDC keys that should be included in the FlumeEvent.
622     * @param required         A comma separated list of MDC keys that must be present in the MDC.
623     * @param exceptionPattern The pattern for formatting exceptions.
624     * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
625     * @param loggerFields     Container for the KeyValuePairs containing the patterns
626     * @param config           The Configuration. Some Converters require access to the Interpolator.
627     * @return An Rfc5424Layout.
628     */
629    @PluginFactory
630    public static Rfc5424Layout createLayout(
631            @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
632            @PluginAttribute("id") final String id,
633            @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber,
634            @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
635            @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
636            @PluginAttribute("mdcPrefix") final String mdcPrefix,
637            @PluginAttribute("eventPrefix") final String eventPrefix,
638            @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
639            @PluginAttribute("newLineEscape") final String escapeNL,
640            @PluginAttribute("appName") final String appName,
641            @PluginAttribute("messageId") final String msgId,
642            @PluginAttribute("mdcExcludes") final String excludes,
643            @PluginAttribute("mdcIncludes") String includes,
644            @PluginAttribute("mdcRequired") final String required,
645            @PluginAttribute("exceptionPattern") final String exceptionPattern,
646            @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat, // RFC 5425
647            @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
648            @PluginConfiguration final Configuration config) {
649        final Charset charset = Constants.UTF_8;
650        if (includes != null && excludes != null) {
651            LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
652            includes = null;
653        }
654
655        return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix,
656                eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern,
657                useTlsMessageFormat, loggerFields);
658    }
659
660    private class FieldFormatter {
661
662        private final Map<String, List<PatternFormatter>> delegateMap;
663        private final boolean discardIfEmpty;
664
665        public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
666            this.discardIfEmpty = discardIfEmpty;
667            this.delegateMap = fieldMap;
668        }
669
670        public StructuredDataElement format(final LogEvent event) {
671            final Map<String, String> map = new HashMap<String, String>();
672
673            for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
674                final StringBuilder buffer = new StringBuilder();
675                for (final PatternFormatter formatter : entry.getValue()) {
676                    formatter.format(event, buffer);
677                }
678                map.put(entry.getKey(), buffer.toString());
679            }
680            return new StructuredDataElement(map, discardIfEmpty);
681        }
682    }
683
684    private class StructuredDataElement {
685
686        private final Map<String, String> fields;
687        private final boolean discardIfEmpty;
688
689        public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
690            this.discardIfEmpty = discardIfEmpty;
691            this.fields = fields;
692        }
693
694        boolean discard() {
695            if (discardIfEmpty == false) {
696                return false;
697            }
698            boolean foundNotEmptyValue = false;
699            for (final Map.Entry<String, String> entry : fields.entrySet()) {
700                if (Strings.isNotEmpty(entry.getValue())) {
701                    foundNotEmptyValue = true;
702                    break;
703                }
704            }
705            return !foundNotEmptyValue;
706        }
707
708        void union(final Map<String, String> fields) {
709            this.fields.putAll(fields);
710        }
711
712        Map<String, String> getFields() {
713            return this.fields;
714        }
715    }
716}