    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    }