View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import java.util.HashMap;
20  import org.apache.logging.log4j.LoggingException;
21  import org.apache.logging.log4j.core.LogEvent;
22  import org.apache.logging.log4j.core.config.Configuration;
23  import org.apache.logging.log4j.core.config.plugins.Plugin;
24  import org.apache.logging.log4j.core.config.plugins.PluginAttr;
25  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
26  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
27  import org.apache.logging.log4j.core.helpers.Charsets;
28  import org.apache.logging.log4j.core.helpers.NetUtils;
29  import org.apache.logging.log4j.core.net.Facility;
30  import org.apache.logging.log4j.core.net.Priority;
31  import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
32  import org.apache.logging.log4j.core.pattern.PatternFormatter;
33  import org.apache.logging.log4j.core.pattern.PatternParser;
34  import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
35  import org.apache.logging.log4j.message.Message;
36  import org.apache.logging.log4j.message.StructuredDataId;
37  import org.apache.logging.log4j.message.StructuredDataMessage;
38  
39  import java.nio.charset.Charset;
40  import java.util.ArrayList;
41  import java.util.Calendar;
42  import java.util.GregorianCalendar;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.SortedMap;
46  import java.util.TreeMap;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  
51  /**
52   * Formats a log event in accordance with RFC 5424.
53   *
54   * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>
55   */
56  @Plugin(name = "RFC5424Layout", category = "Core", elementType = "layout", printObject = true)
57  public final class RFC5424Layout extends AbstractStringLayout {
58  
59      /**
60       * Not a very good default - it is the Apache Software Foundation's enterprise number.
61       */
62      public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
63      /**
64       * The default event id.
65       */
66      public static final String DEFAULT_ID = "Audit";
67      /**
68       * Match newlines in a platform-independent manner.
69       */
70      public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
71      /**
72       * Match characters which require escaping
73       */
74      public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
75  
76      private static final String DEFAULT_MDCID = "mdc";
77      private static final int TWO_DIGITS = 10;
78      private static final int THREE_DIGITS = 100;
79      private static final int MILLIS_PER_MINUTE = 60000;
80      private static final int MINUTES_PER_HOUR = 60;
81  
82      private static final String COMPONENT_KEY = "RFC5424-Converter";
83  
84      private final Facility facility;
85      private final String defaultId;
86      private final Integer enterpriseNumber;
87      private final boolean includeMDC;
88      private final String mdcId;
89      private final String localHostName;
90      private final String appName;
91      private final String messageId;
92      private final String configName;
93      private final String mdcPrefix;
94      private final String eventPrefix;
95      private final List<String> mdcExcludes;
96      private final List<String> mdcIncludes;
97      private final List<String> mdcRequired;
98      private final ListChecker checker;
99      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 }