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    
455            final int millis = cal.get(Calendar.MILLISECOND);
456            if (millis != 0) {
457                buffer.append('.');
458                pad(millis, THREE_DIGITS, buffer);
459            }
460    
461            int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
462            if (tzmin == 0) {
463                buffer.append('Z');
464            } else {
465                if (tzmin < 0) {
466                    tzmin = -tzmin;
467                    buffer.append('-');
468                } else {
469                    buffer.append('+');
470                }
471                final int tzhour = tzmin / MINUTES_PER_HOUR;
472                tzmin -= tzhour * MINUTES_PER_HOUR;
473                pad(tzhour, TWO_DIGITS, buffer);
474                buffer.append(':');
475                pad(tzmin, TWO_DIGITS, buffer);
476            }
477            synchronized (this) {
478                if (last == lastTimestamp) {
479                    lastTimestamp = now;
480                    timestamppStr = buffer.toString();
481                }
482            }
483            return buffer.toString();
484        }
485    
486        private void pad(final int val, int max, final StringBuilder buf) {
487            while (max > 1) {
488                if (val < max) {
489                    buf.append('0');
490                }
491                max = max / TWO_DIGITS;
492            }
493            buf.append(Integer.toString(val));
494        }
495    
496        private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data,
497                                             final StringBuilder sb, final ListChecker checker) {
498            if ((id == null && defaultId == null) || data.discard()) {
499                return;
500            }
501    
502            sb.append('[');
503            sb.append(id);
504            if (!mdcSdId.toString().equals(id)) {
505                appendMap(prefix, data.getFields(), sb, noopChecker);
506            } else {
507                appendMap(prefix, data.getFields(), sb, checker);
508            }
509            sb.append(']');
510        }
511    
512        private String getId(final StructuredDataId id) {
513            final StringBuilder sb = new StringBuilder();
514            if (id == null || id.getName() == null) {
515                sb.append(defaultId);
516            } else {
517                sb.append(id.getName());
518            }
519            int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber;
520            if (ein < 0) {
521                ein = enterpriseNumber;
522            }
523            if (ein >= 0) {
524                sb.append('@').append(ein);
525            }
526            return sb.toString();
527        }
528    
529        private void checkRequired(final Map<String, String> map) {
530            for (final String key : mdcRequired) {
531                final String value = map.get(key);
532                if (value == null) {
533                    throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
534                }
535            }
536        }
537    
538        private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
539                               final ListChecker checker) {
540            final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
541            for (final Map.Entry<String, String> entry : sorted.entrySet()) {
542                if (checker.check(entry.getKey()) && entry.getValue() != null) {
543                    sb.append(' ');
544                    if (prefix != null) {
545                        sb.append(prefix);
546                    }
547                    sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"")
548                        .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"');
549                }
550            }
551        }
552    
553        private String escapeSDParams(final String value) {
554            return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
555        }
556    
557        /**
558         * Interface used to check keys in a Map.
559         */
560        private interface ListChecker {
561            boolean check(String key);
562        }
563    
564        /**
565         * Includes only the listed keys.
566         */
567        private class IncludeChecker implements ListChecker {
568            @Override
569            public boolean check(final String key) {
570                return mdcIncludes.contains(key);
571            }
572        }
573    
574        /**
575         * Excludes the listed keys.
576         */
577        private class ExcludeChecker implements ListChecker {
578            @Override
579            public boolean check(final String key) {
580                return !mdcExcludes.contains(key);
581            }
582        }
583    
584        /**
585         * Does nothing.
586         */
587        private class NoopChecker implements ListChecker {
588            @Override
589            public boolean check(final String key) {
590                return true;
591            }
592        }
593    
594        @Override
595        public String toString() {
596            final StringBuilder sb = new StringBuilder();
597            sb.append("facility=").append(facility.name());
598            sb.append(" appName=").append(appName);
599            sb.append(" defaultId=").append(defaultId);
600            sb.append(" enterpriseNumber=").append(enterpriseNumber);
601            sb.append(" newLine=").append(includeNewLine);
602            sb.append(" includeMDC=").append(includeMdc);
603            sb.append(" messageId=").append(messageId);
604            return sb.toString();
605        }
606    
607        /**
608         * Create the RFC 5424 Layout.
609         *
610         * @param facility         The Facility is used to try to classify the message.
611         * @param id               The default structured data id to use when formatting according to RFC 5424.
612         * @param enterpriseNumber The IANA enterprise number.
613         * @param includeMDC       Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
614         *                         record. Defaults to "true:.
615         * @param mdcId            The id to use for the MDC Structured Data Element.
616         * @param mdcPrefix        The prefix to add to MDC key names.
617         * @param eventPrefix      The prefix to add to event key names.
618         * @param newLine          If true, a newline will be appended to the end of the syslog record. The default is false.
619         * @param escapeNL         String that should be used to replace newlines within the message text.
620         * @param appName          The value to use as the APP-NAME in the RFC 5424 syslog record.
621         * @param msgId            The default value to be used in the MSGID field of RFC 5424 syslog records.
622         * @param excludes         A comma separated list of MDC keys that should be excluded from the LogEvent.
623         * @param includes         A comma separated list of MDC keys that should be included in the FlumeEvent.
624         * @param required         A comma separated list of MDC keys that must be present in the MDC.
625         * @param exceptionPattern The pattern for formatting exceptions.
626         * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425.
627         * @param loggerFields     Container for the KeyValuePairs containing the patterns
628         * @param config           The Configuration. Some Converters require access to the Interpolator.
629         * @return An Rfc5424Layout.
630         */
631        @PluginFactory
632        public static Rfc5424Layout createLayout(
633                @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility,
634                @PluginAttribute("id") final String id,
635                @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber,
636                @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC,
637                @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId,
638                @PluginAttribute("mdcPrefix") final String mdcPrefix,
639                @PluginAttribute("eventPrefix") final String eventPrefix,
640                @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine,
641                @PluginAttribute("newLineEscape") final String escapeNL,
642                @PluginAttribute("appName") final String appName,
643                @PluginAttribute("messageId") final String msgId,
644                @PluginAttribute("mdcExcludes") final String excludes,
645                @PluginAttribute("mdcIncludes") String includes,
646                @PluginAttribute("mdcRequired") final String required,
647                @PluginAttribute("exceptionPattern") final String exceptionPattern,
648                @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat, // RFC 5425
649                @PluginElement("LoggerFields") final LoggerFields[] loggerFields,
650                @PluginConfiguration final Configuration config) {
651            final Charset charset = Charsets.UTF_8;
652            if (includes != null && excludes != null) {
653                LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
654                includes = null;
655            }
656    
657            return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix,
658                    eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern,
659                    useTlsMessageFormat, loggerFields);
660        }
661    
662        private class FieldFormatter {
663    
664            private final Map<String, List<PatternFormatter>> delegateMap;
665            private final boolean discardIfEmpty;
666    
667            public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) {
668                this.discardIfEmpty = discardIfEmpty;
669                this.delegateMap = fieldMap;
670            }
671    
672            public StructuredDataElement format(final LogEvent event) {
673                final Map<String, String> map = new HashMap<String, String>();
674    
675                for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) {
676                    final StringBuilder buffer = new StringBuilder();
677                    for (final PatternFormatter formatter : entry.getValue()) {
678                        formatter.format(event, buffer);
679                    }
680                    map.put(entry.getKey(), buffer.toString());
681                }
682                return new StructuredDataElement(map, discardIfEmpty);
683            }
684        }
685    
686        private class StructuredDataElement {
687    
688            private final Map<String, String> fields;
689            private final boolean discardIfEmpty;
690    
691            public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) {
692                this.discardIfEmpty = discardIfEmpty;
693                this.fields = fields;
694            }
695    
696            boolean discard() {
697                if (discardIfEmpty == false) {
698                    return false;
699                }
700                boolean foundNotEmptyValue = false;
701                for (final Map.Entry<String, String> entry : fields.entrySet()) {
702                    if (Strings.isNotEmpty(entry.getValue())) {
703                        foundNotEmptyValue = true;
704                        break;
705                    }
706                }
707                return !foundNotEmptyValue;
708            }
709    
710            void union(final Map<String, String> fields) {
711                this.fields.putAll(fields);
712            }
713    
714            Map<String, String> getFields() {
715                return this.fields;
716            }
717        }
718    }