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.Layout;
034    import org.apache.logging.log4j.core.LogEvent;
035    import org.apache.logging.log4j.core.appender.TlsSyslogFrame;
036    import org.apache.logging.log4j.core.config.Configuration;
037    import org.apache.logging.log4j.core.config.Node;
038    import org.apache.logging.log4j.core.config.plugins.Plugin;
039    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
040    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
041    import org.apache.logging.log4j.core.config.plugins.PluginElement;
042    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
043    import org.apache.logging.log4j.core.net.Facility;
044    import org.apache.logging.log4j.core.net.Priority;
045    import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
046    import org.apache.logging.log4j.core.pattern.PatternConverter;
047    import org.apache.logging.log4j.core.pattern.PatternFormatter;
048    import org.apache.logging.log4j.core.pattern.PatternParser;
049    import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
050    import org.apache.logging.log4j.core.util.Constants;
051    import org.apache.logging.log4j.core.util.NetUtils;
052    import org.apache.logging.log4j.core.util.Patterns;
053    import org.apache.logging.log4j.message.Message;
054    import org.apache.logging.log4j.message.StructuredDataId;
055    import org.apache.logging.log4j.message.StructuredDataMessage;
056    import org.apache.logging.log4j.util.StringBuilders;
057    import 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)
066    public 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    }