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  @Plugin(name = "RFC5424Layout", type = "Core", elementType = "layout", printObject = true)
55  public final class RFC5424Layout extends AbstractStringLayout {
56  
57      /**
58       * Not a very good default - it is the Apache Software Foundation's enterprise number.
59       */
60      public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
61      /**
62       * The default event id.
63       */
64      public static final String DEFAULT_ID = "Audit";
65      /**
66       * Match newlines in a platform-independent manner.
67       */
68      public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
69      /**
70       * Match characters which require escaping
71       */
72      public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
73  
74      private static final String DEFAULT_MDCID = "mdc";
75      private static final int TWO_DIGITS = 10;
76      private static final int THREE_DIGITS = 100;
77      private static final int MILLIS_PER_MINUTE = 60000;
78      private static final int MINUTES_PER_HOUR = 60;
79  
80      private static final String COMPONENT_KEY = "RFC5424-Converter";
81  
82      private final Facility facility;
83      private final String defaultId;
84      private final Integer enterpriseNumber;
85      private final boolean includeMDC;
86      private final String mdcId;
87      private final String localHostName;
88      private final String appName;
89      private final String messageId;
90      private final String configName;
91      private final String mdcPrefix;
92      private final String eventPrefix;
93      private final List<String> mdcExcludes;
94      private final List<String> mdcIncludes;
95      private final List<String> mdcRequired;
96      private final ListChecker checker;
97      private final ListChecker noopChecker = new NoopChecker();
98      private final boolean includeNewLine;
99      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 }