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.util.HashMap;
020    import org.apache.logging.log4j.LoggingException;
021    import org.apache.logging.log4j.core.LogEvent;
022    import org.apache.logging.log4j.core.config.Configuration;
023    import org.apache.logging.log4j.core.config.plugins.Plugin;
024    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
025    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
026    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
027    import org.apache.logging.log4j.core.helpers.Charsets;
028    import org.apache.logging.log4j.core.helpers.NetUtils;
029    import org.apache.logging.log4j.core.net.Facility;
030    import org.apache.logging.log4j.core.net.Priority;
031    import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
032    import org.apache.logging.log4j.core.pattern.PatternFormatter;
033    import org.apache.logging.log4j.core.pattern.PatternParser;
034    import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
035    import org.apache.logging.log4j.message.Message;
036    import org.apache.logging.log4j.message.StructuredDataId;
037    import org.apache.logging.log4j.message.StructuredDataMessage;
038    
039    import java.nio.charset.Charset;
040    import java.util.ArrayList;
041    import java.util.Calendar;
042    import java.util.GregorianCalendar;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.SortedMap;
046    import java.util.TreeMap;
047    import java.util.regex.Matcher;
048    import java.util.regex.Pattern;
049    
050    
051    /**
052     * Formats a log event in accordance with RFC 5424.
053     *
054     * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
055     */
056    @Plugin(name = "RFC5424Layout", category = "Core", elementType = "layout", printObject = true)
057    public final class RFC5424Layout extends AbstractStringLayout {
058    
059        /**
060         * Not a very good default - it is the Apache Software Foundation's enterprise number.
061         */
062        public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
063        /**
064         * The default event id.
065         */
066        public static final String DEFAULT_ID = "Audit";
067        /**
068         * Match newlines in a platform-independent manner.
069         */
070        public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
071        /**
072         * Match characters which require escaping
073         */
074        public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
075    
076        private static final String DEFAULT_MDCID = "mdc";
077        private static final int TWO_DIGITS = 10;
078        private static final int THREE_DIGITS = 100;
079        private static final int MILLIS_PER_MINUTE = 60000;
080        private static final int MINUTES_PER_HOUR = 60;
081    
082        private static final String COMPONENT_KEY = "RFC5424-Converter";
083    
084        private final Facility facility;
085        private final String defaultId;
086        private final Integer enterpriseNumber;
087        private final boolean includeMDC;
088        private final String mdcId;
089        private final String localHostName;
090        private final String appName;
091        private final String messageId;
092        private final String configName;
093        private final String mdcPrefix;
094        private final String eventPrefix;
095        private final List<String> mdcExcludes;
096        private final List<String> mdcIncludes;
097        private final List<String> mdcRequired;
098        private final ListChecker checker;
099        private final ListChecker noopChecker = new NoopChecker();
100        private final boolean includeNewLine;
101        private final String escapeNewLine;
102    
103        private long lastTimestamp = -1;
104        private String timestamppStr;
105    
106        private final List<PatternFormatter> formatters;
107    
108        private RFC5424Layout(final Configuration config, final Facility facility, final String id, final int ein,
109                              final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId,
110                              final String mdcPrefix, final String eventPrefix,
111                              final String appName, final String messageId, final String excludes, final String includes,
112                              final String required, final Charset charset, final String exceptionPattern) {
113            super(charset);
114            final PatternParser parser = createPatternParser(config);
115            formatters = exceptionPattern == null ? null : parser.parse(exceptionPattern, false);
116            this.facility = facility;
117            this.defaultId = id == null ? DEFAULT_ID : id;
118            this.enterpriseNumber = ein;
119            this.includeMDC = includeMDC;
120            this.includeNewLine = includeNL;
121            this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL);
122            this.mdcId = mdcId;
123            this.mdcPrefix = mdcPrefix;
124            this.eventPrefix = eventPrefix;
125            this.appName = appName;
126            this.messageId = messageId;
127            this.localHostName = NetUtils.getLocalHostname();
128            ListChecker c = null;
129            if (excludes != null) {
130                final String[] array = excludes.split(",");
131                if (array.length > 0) {
132                    c = new ExcludeChecker();
133                    mdcExcludes = new ArrayList<String>(array.length);
134                    for (final String str : array) {
135                        mdcExcludes.add(str.trim());
136                    }
137                } else {
138                    mdcExcludes = null;
139                }
140            } else {
141                mdcExcludes = null;
142            }
143            if (includes != null) {
144                final String[] array = includes.split(",");
145                if (array.length > 0) {
146                    c = new IncludeChecker();
147                    mdcIncludes = new ArrayList<String>(array.length);
148                    for (final String str : array) {
149                        mdcIncludes.add(str.trim());
150                    }
151                } else {
152                    mdcIncludes = null;
153                }
154            } else {
155                mdcIncludes = null;
156            }
157            if (required != null) {
158                final String[] array = required.split(",");
159                if (array.length > 0) {
160                    mdcRequired = new ArrayList<String>(array.length);
161                    for (final String str : array) {
162                        mdcRequired.add(str.trim());
163                    }
164                } else {
165                    mdcRequired = null;
166                }
167    
168            } else {
169                mdcRequired = null;
170            }
171            this.checker = c != null ? c : noopChecker;
172            final String name = config == null ? null : config.getName();
173            configName = name != null && name.length() > 0 ? name : null;
174        }
175    
176        /**
177         * Create a PatternParser.
178         * @param config The Configuration.
179         * @return The PatternParser.
180         */
181        public static PatternParser createPatternParser(final Configuration config) {
182            if (config == null) {
183                return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class,
184                    ThrowablePatternConverter.class);
185            }
186            PatternParser parser = (PatternParser) config.getComponent(COMPONENT_KEY);
187            if (parser == null) {
188                parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class);
189                config.addComponent(COMPONENT_KEY, parser);
190                parser = (PatternParser) config.getComponent(COMPONENT_KEY);
191            }
192            return parser;
193        }
194    
195        /**
196         * RFC5424Layout's content format is specified by:<p/>
197         * Key: "structured" Value: "true"<p/>
198         * Key: "format" Value: "RFC5424"<p/>
199         * @return Map of content format keys supporting RFC5424Layout
200         */
201        @Override
202        public Map<String, String> getContentFormat()
203        {
204            Map<String, String> result = new HashMap<String, String>();
205            result.put("structured", "true");
206            result.put("formatType", "RFC5424");
207            return result;
208        }
209    
210        /**
211         * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification.
212         *
213         * @param event The LogEvent.
214         * @return The RFC 5424 String representation of the LogEvent.
215         */
216        @Override
217        public String toSerializable(final LogEvent event) {
218            final Message msg = event.getMessage();
219            final boolean isStructured = msg instanceof StructuredDataMessage;
220            final StringBuilder buf = new StringBuilder();
221    
222            buf.append("<");
223            buf.append(Priority.getPriority(facility, event.getLevel()));
224            buf.append(">1 ");
225            buf.append(computeTimeStampString(event.getMillis()));
226            buf.append(' ');
227            buf.append(localHostName);
228            buf.append(' ');
229            if (appName != null) {
230                buf.append(appName);
231            } else if (configName != null) {
232                buf.append(configName);
233            } else {
234                buf.append("-");
235            }
236            buf.append(" ");
237            buf.append(getProcId());
238            buf.append(" ");
239            final String type = isStructured ? ((StructuredDataMessage) msg).getType() : null;
240            if (type != null) {
241                buf.append(type);
242            } else if (messageId != null) {
243                buf.append(messageId);
244            } else {
245                buf.append("-");
246            }
247            buf.append(" ");
248            if (isStructured || includeMDC) {
249                StructuredDataId id = null;
250                String text;
251                if (isStructured) {
252                    final StructuredDataMessage data = (StructuredDataMessage) msg;
253                    final Map<String, String> map = data.getData();
254                    id = data.getId();
255                    formatStructuredElement(id, eventPrefix, map, buf, noopChecker);
256                    text = data.getFormat();
257                } else {
258                    text = msg.getFormattedMessage();
259                }
260                if (includeMDC) {
261                    Map<String, String> map = event.getContextMap();
262                    if (mdcRequired != null) {
263                        checkRequired(map);
264                    }
265                    final int ein = id == null || id.getEnterpriseNumber() < 0 ?
266                        enterpriseNumber : id.getEnterpriseNumber();
267                    final StructuredDataId mdcSDID = new StructuredDataId(mdcId, ein, null, null);
268                    formatStructuredElement(mdcSDID, mdcPrefix, map, buf, checker);
269                }
270                if (text != null && text.length() > 0) {
271                    buf.append(" ").append(escapeNewlines(text, escapeNewLine));
272                }
273            } else {
274                buf.append("- ");
275                buf.append(escapeNewlines(msg.getFormattedMessage(), escapeNewLine));
276            }
277            if (formatters != null && event.getThrown() != null) {
278                final StringBuilder exception = new StringBuilder("\n");
279                for (final PatternFormatter formatter : formatters) {
280                    formatter.format(event, exception);
281                }
282                buf.append(escapeNewlines(exception.toString(), escapeNewLine));
283            }
284            if (includeNewLine) {
285                buf.append("\n");
286            }
287            return buf.toString();
288        }
289    
290        private String escapeNewlines(final String text, final String escapeNewLine)
291        {
292            if (null == escapeNewLine) {
293                return text;
294            }
295            return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine);
296        }
297    
298        protected String getProcId() {
299            return "-";
300        }
301    
302        protected List<String> getMdcExcludes() {
303            return mdcExcludes;
304        }
305    
306        protected List<String> getMdcIncludes() {
307            return mdcIncludes;
308        }
309    
310        private String computeTimeStampString(final long now) {
311            long last;
312            synchronized (this) {
313                last = lastTimestamp;
314                if (now == lastTimestamp) {
315                    return timestamppStr;
316                }
317            }
318    
319            final StringBuilder buf = new StringBuilder();
320            final Calendar cal = new GregorianCalendar();
321            cal.setTimeInMillis(now);
322            buf.append(Integer.toString(cal.get(Calendar.YEAR)));
323            buf.append("-");
324            pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buf);
325            buf.append("-");
326            pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buf);
327            buf.append("T");
328            pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buf);
329            buf.append(":");
330            pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buf);
331            buf.append(":");
332            pad(cal.get(Calendar.SECOND), TWO_DIGITS, buf);
333    
334            final int millis = cal.get(Calendar.MILLISECOND);
335            if (millis != 0) {
336                buf.append('.');
337                pad(millis, THREE_DIGITS, buf);
338            }
339    
340            int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
341            if (tzmin == 0) {
342                buf.append("Z");
343            } else {
344                if (tzmin < 0) {
345                    tzmin = -tzmin;
346                    buf.append("-");
347                } else {
348                    buf.append("+");
349                }
350                final int tzhour = tzmin / MINUTES_PER_HOUR;
351                tzmin -= tzhour * MINUTES_PER_HOUR;
352                pad(tzhour, TWO_DIGITS, buf);
353                buf.append(":");
354                pad(tzmin, TWO_DIGITS, buf);
355            }
356            synchronized (this) {
357                if (last == lastTimestamp) {
358                    lastTimestamp = now;
359                    timestamppStr = buf.toString();
360                }
361            }
362            return buf.toString();
363        }
364    
365        private void pad(final int val, int max, final StringBuilder buf) {
366            while (max > 1) {
367                if (val < max) {
368                    buf.append("0");
369                }
370                max = max / TWO_DIGITS;
371            }
372            buf.append(Integer.toString(val));
373        }
374    
375        private void formatStructuredElement(final StructuredDataId id, final String prefix, final Map<String, String> data,
376                                             final StringBuilder sb, final ListChecker checker) {
377            if (id == null && defaultId == null) {
378                return;
379            }
380            sb.append("[");
381            sb.append(getId(id));
382            appendMap(prefix, data, sb, checker);
383            sb.append("]");
384        }
385    
386        private String getId(final StructuredDataId id) {
387            final StringBuilder sb = new StringBuilder();
388            if (id.getName() == null) {
389                sb.append(defaultId);
390            } else {
391                sb.append(id.getName());
392            }
393            int ein = id.getEnterpriseNumber();
394            if (ein < 0) {
395                ein = enterpriseNumber;
396            }
397            if (ein >= 0) {
398                sb.append("@").append(ein);
399            }
400            return sb.toString();
401        }
402    
403        private void checkRequired(final Map<String, String> map) {
404            for (final String key : mdcRequired) {
405                final String value = map.get(key);
406                if (value == null) {
407                    throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
408                }
409            }
410        }
411    
412        private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb,
413                               final ListChecker checker)
414        {
415            final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
416            for (final Map.Entry<String, String> entry : sorted.entrySet()) {
417                if (checker.check(entry.getKey()) && entry.getValue() != null) {
418                    sb.append(" ");
419                    if (prefix != null) {
420                        sb.append(prefix);
421                    }
422                    sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"")
423                      .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append("\"");
424                }
425            }
426        }
427    
428        private String escapeSDParams(String value)
429        {
430            return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
431        }
432    
433        /**
434         * Interface used to check keys in a Map.
435         */
436        private interface ListChecker {
437            boolean check(String key);
438        }
439    
440        /**
441         * Includes only the listed keys.
442         */
443        private class IncludeChecker implements ListChecker {
444            @Override
445            public boolean check(final String key) {
446                return mdcIncludes.contains(key);
447            }
448        }
449    
450        /**
451         * Excludes the listed keys.
452         */
453        private class ExcludeChecker implements ListChecker {
454            @Override
455            public boolean check(final String key) {
456                return !mdcExcludes.contains(key);
457            }
458        }
459    
460        /**
461         * Does nothing.
462         */
463        private class NoopChecker implements ListChecker {
464            @Override
465            public boolean check(final String key) {
466                return true;
467            }
468        }
469    
470        @Override
471        public String toString() {
472            final StringBuilder sb = new StringBuilder();
473            sb.append("facility=").append(facility.name());
474            sb.append(" appName=").append(appName);
475            sb.append(" defaultId=").append(defaultId);
476            sb.append(" enterpriseNumber=").append(enterpriseNumber);
477            sb.append(" newLine=").append(includeNewLine);
478            sb.append(" includeMDC=").append(includeMDC);
479            sb.append(" messageId=").append(messageId);
480            return sb.toString();
481        }
482    
483        /**
484         * Create the RFC 5424 Layout.
485         * @param facility The Facility is used to try to classify the message.
486         * @param id The default structured data id to use when formatting according to RFC 5424.
487         * @param ein The IANA enterprise number.
488         * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
489         * record. Defaults to "true:.
490         * @param mdcId The id to use for the MDC Structured Data Element.
491         * @param mdcPrefix The prefix to add to MDC key names.
492         * @param eventPrefix The prefix to add to event key names.
493         * @param includeNL If true, a newline will be appended to the end of the syslog record. The default is false.
494         * @param escapeNL String that should be used to replace newlines within the message text.
495         * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record.
496         * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records.
497         * @param excludes A comma separated list of mdc keys that should be excluded from the LogEvent.
498         * @param includes A comma separated list of mdc keys that should be included in the FlumeEvent.
499         * @param required A comma separated list of mdc keys that must be present in the MDC.
500         * @param exceptionPattern The pattern for formatting exceptions.
501         * @param config The Configuration. Some Converters require access to the Interpolator.
502         * @return An RFC5424Layout.
503         */
504        @PluginFactory
505        public static RFC5424Layout createLayout(@PluginAttr("facility") final String facility,
506                                                 @PluginAttr("id") final String id,
507                                                 @PluginAttr("enterpriseNumber") final String ein,
508                                                 @PluginAttr("includeMDC") final String includeMDC,
509                                                 @PluginAttr("mdcId") String mdcId,
510                                                 @PluginAttr("mdcPrefix") String mdcPrefix,
511                                                 @PluginAttr("eventPrefix") String eventPrefix,
512                                                 @PluginAttr("newLine") final String includeNL,
513                                                 @PluginAttr("newLineEscape") final String escapeNL,
514                                                 @PluginAttr("appName") final String appName,
515                                                 @PluginAttr("messageId") final String msgId,
516                                                 @PluginAttr("mdcExcludes") final String excludes,
517                                                 @PluginAttr("mdcIncludes") String includes,
518                                                 @PluginAttr("mdcRequired") final String required,
519                                                 @PluginAttr("exceptionPattern") final String exceptionPattern,
520                                                 @PluginConfiguration final Configuration config) {
521            final Charset charset = Charsets.UTF_8;
522            if (includes != null && excludes != null) {
523                LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
524                includes = null;
525            }
526            final Facility f = Facility.toFacility(facility, Facility.LOCAL0);
527            final int enterpriseNumber = ein == null ? DEFAULT_ENTERPRISE_NUMBER : Integer.parseInt(ein);
528            final boolean isMdc = includeMDC == null ? true : Boolean.valueOf(includeMDC);
529            final boolean includeNewLine = includeNL == null ? false : Boolean.valueOf(includeNL);
530            if (mdcId == null) {
531                mdcId = DEFAULT_MDCID;
532            }
533    
534            return new RFC5424Layout(config, f, id, enterpriseNumber, isMdc, includeNewLine, escapeNL, mdcId, mdcPrefix,
535                                     eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern);
536        }
537    }