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