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