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