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 org.apache.logging.log4j.LoggingException;
20  import org.apache.logging.log4j.core.LogEvent;
21  import org.apache.logging.log4j.core.config.Configuration;
22  import org.apache.logging.log4j.core.config.plugins.Plugin;
23  import org.apache.logging.log4j.core.config.plugins.PluginAttr;
24  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
25  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
26  import org.apache.logging.log4j.core.helpers.Charsets;
27  import org.apache.logging.log4j.core.helpers.NetUtils;
28  import org.apache.logging.log4j.core.net.Facility;
29  import org.apache.logging.log4j.core.net.Priority;
30  import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
31  import org.apache.logging.log4j.core.pattern.PatternFormatter;
32  import org.apache.logging.log4j.core.pattern.PatternParser;
33  import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
34  import org.apache.logging.log4j.message.Message;
35  import org.apache.logging.log4j.message.StructuredDataId;
36  import org.apache.logging.log4j.message.StructuredDataMessage;
37  
38  import java.nio.charset.Charset;
39  import java.util.ArrayList;
40  import java.util.Calendar;
41  import java.util.GregorianCalendar;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.SortedMap;
45  import java.util.TreeMap;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  
50  /**
51   * Formats a log event in accordance with RFC 5424.
52   */
53  @Plugin(name = "RFC5424Layout", type = "Core", elementType = "layout", printObject = true)
54  public final class RFC5424Layout extends AbstractStringLayout {
55  
56      /**
57       * Not a very good default - it is the Apache Software Foundation's enterprise number.
58       */
59      public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
60      /**
61       * The default event id.
62       */
63      public static final String DEFAULT_ID = "Audit";
64      /**
65       * Match newlines in a platform-independent manner.
66       */
67      public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
68  
69      private static final String DEFAULT_MDCID = "mdc";
70      private static final int TWO_DIGITS = 10;
71      private static final int THREE_DIGITS = 100;
72      private static final int MILLIS_PER_MINUTE = 60000;
73      private static final int MINUTES_PER_HOUR = 60;
74  
75      private static final String COMPONENT_KEY = "RFC5424-Converter";
76  
77      private final Facility facility;
78      private final String defaultId;
79      private final Integer enterpriseNumber;
80      private final boolean includeMDC;
81      private final String mdcId;
82      private final String localHostName;
83      private final String appName;
84      private final String messageId;
85      private final String configName;
86      private final List<String> mdcExcludes;
87      private final List<String> mdcIncludes;
88      private final List<String> mdcRequired;
89      private final ListChecker checker;
90      private final ListChecker noopChecker = new NoopChecker();
91      private final boolean includeNewLine;
92      private final String escapeNewLine;
93  
94      private long lastTimestamp = -1;
95      private String timestamppStr;
96  
97      private final List<PatternFormatter> formatters;
98  
99      private RFC5424Layout(final Configuration config, final Facility facility, final String id, final int ein,
100                           final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId,
101                           final String appName, final String messageId, final String excludes, final String includes,
102                           final String required, final Charset charset, final String exceptionPattern) {
103         super(charset);
104         final PatternParser parser = createPatternParser(config);
105         formatters = exceptionPattern == null ? null : parser.parse(exceptionPattern, false);
106         this.facility = facility;
107         this.defaultId = id == null ? DEFAULT_ID : id;
108         this.enterpriseNumber = ein;
109         this.includeMDC = includeMDC;
110         this.includeNewLine = includeNL;
111         this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL);
112         this.mdcId = mdcId;
113         this.appName = appName;
114         this.messageId = messageId;
115         this.localHostName = NetUtils.getLocalHostname();
116         ListChecker c = null;
117         if (excludes != null) {
118             final String[] array = excludes.split(",");
119             if (array.length > 0) {
120                 c = new ExcludeChecker();
121                 mdcExcludes = new ArrayList<String>(array.length);
122                 for (final String str : array) {
123                     mdcExcludes.add(str.trim());
124                 }
125             } else {
126                 mdcExcludes = null;
127             }
128         } else {
129             mdcExcludes = null;
130         }
131         if (includes != null) {
132             final String[] array = includes.split(",");
133             if (array.length > 0) {
134                 c = new IncludeChecker();
135                 mdcIncludes = new ArrayList<String>(array.length);
136                 for (final String str : array) {
137                     mdcIncludes.add(str.trim());
138                 }
139             } else {
140                 mdcIncludes = null;
141             }
142         } else {
143             mdcIncludes = null;
144         }
145         if (required != null) {
146             final String[] array = required.split(",");
147             if (array.length > 0) {
148                 mdcRequired = new ArrayList<String>(array.length);
149                 for (final String str : array) {
150                     mdcRequired.add(str.trim());
151                 }
152             } else {
153                 mdcRequired = null;
154             }
155 
156         } else {
157             mdcRequired = null;
158         }
159         this.checker = c != null ? c : noopChecker;
160         final String name = config == null ? null : config.getName();
161         configName = name != null && name.length() > 0 ? name : null;
162     }
163 
164     /**
165      * Create a PatternParser.
166      * @param config The Configuration.
167      * @return The PatternParser.
168      */
169     public static PatternParser createPatternParser(final Configuration config) {
170         if (config == null) {
171             return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class,
172                 ThrowablePatternConverter.class);
173         }
174         PatternParser parser = (PatternParser) config.getComponent(COMPONENT_KEY);
175         if (parser == null) {
176             parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class);
177             config.addComponent(COMPONENT_KEY, parser);
178             parser = (PatternParser) config.getComponent(COMPONENT_KEY);
179         }
180         return parser;
181     }
182 
183     /**
184      * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification.
185      *
186      * @param event The LogEvent.
187      * @return The RFC 5424 String representation of the LogEvent.
188      */
189     public String toSerializable(final LogEvent event) {
190         final Message msg = event.getMessage();
191         final boolean isStructured = msg instanceof StructuredDataMessage;
192         final StringBuilder buf = new StringBuilder();
193 
194         buf.append("<");
195         buf.append(Priority.getPriority(facility, event.getLevel()));
196         buf.append(">1 ");
197         buf.append(computeTimeStampString(event.getMillis()));
198         buf.append(' ');
199         buf.append(localHostName);
200         buf.append(' ');
201         if (appName != null) {
202             buf.append(appName);
203         } else if (configName != null) {
204             buf.append(configName);
205         } else {
206             buf.append("-");
207         }
208         buf.append(" ");
209         buf.append(getProcId());
210         buf.append(" ");
211         final String type = isStructured ? ((StructuredDataMessage) msg).getType() : null;
212         if (type != null) {
213             buf.append(type);
214         } else if (messageId != null) {
215             buf.append(messageId);
216         } else {
217             buf.append("-");
218         }
219         buf.append(" ");
220         if (isStructured || includeMDC) {
221             StructuredDataId id = null;
222             String text;
223             if (isStructured) {
224                 final StructuredDataMessage data = (StructuredDataMessage) msg;
225                 final Map<String, String> map = data.getData();
226                 id = data.getId();
227                 formatStructuredElement(id, map, buf, noopChecker);
228                 text = data.getFormat();
229             } else {
230                 text = msg.getFormattedMessage();
231             }
232             if (includeMDC) {
233                 if (mdcRequired != null) {
234                     checkRequired(event.getContextMap());
235                 }
236                 final int ein = id == null || id.getEnterpriseNumber() < 0 ?
237                     enterpriseNumber : id.getEnterpriseNumber();
238                 final StructuredDataId mdcSDID = new StructuredDataId(mdcId, ein, null, null);
239                 formatStructuredElement(mdcSDID, event.getContextMap(), buf, checker);
240             }
241             if (text != null && text.length() > 0) {
242                 buf.append(" ").append(escapeNewlines(text, escapeNewLine));
243             }
244         } else {
245             buf.append("- ");
246             buf.append(escapeNewlines(msg.getFormattedMessage(), escapeNewLine));
247         }
248         if (formatters != null && event.getThrown() != null) {
249             final StringBuilder exception = new StringBuilder("\n");
250             for (final PatternFormatter formatter : formatters) {
251                 formatter.format(event, exception);
252             }
253             buf.append(escapeNewlines(exception.toString(), escapeNewLine));
254         }
255         if (includeNewLine) {
256             buf.append("\n");
257         }
258         return buf.toString();
259     }
260 
261     private String escapeNewlines(final String text, final String escapeNewLine)
262     {
263         if (null == escapeNewLine) {
264             return text;
265         }
266         return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine);
267     }
268 
269     protected String getProcId() {
270         return "-";
271     }
272 
273     protected List<String> getMdcExcludes() {
274         return mdcExcludes;
275     }
276 
277     protected List<String> getMdcIncludes() {
278         return mdcIncludes;
279     }
280 
281     private String computeTimeStampString(final long now) {
282         long last;
283         synchronized (this) {
284             last = lastTimestamp;
285             if (now == lastTimestamp) {
286                 return timestamppStr;
287             }
288         }
289 
290         final StringBuilder buf = new StringBuilder();
291         final Calendar cal = new GregorianCalendar();
292         cal.setTimeInMillis(now);
293         buf.append(Integer.toString(cal.get(Calendar.YEAR)));
294         buf.append("-");
295         pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buf);
296         buf.append("-");
297         pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buf);
298         buf.append("T");
299         pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buf);
300         buf.append(":");
301         pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buf);
302         buf.append(":");
303         pad(cal.get(Calendar.SECOND), TWO_DIGITS, buf);
304 
305         final int millis = cal.get(Calendar.MILLISECOND);
306         if (millis != 0) {
307             buf.append('.');
308             pad(millis, THREE_DIGITS, buf);
309         }
310 
311         int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
312         if (tzmin == 0) {
313             buf.append("Z");
314         } else {
315             if (tzmin < 0) {
316                 tzmin = -tzmin;
317                 buf.append("-");
318             } else {
319                 buf.append("+");
320             }
321             final int tzhour = tzmin / MINUTES_PER_HOUR;
322             tzmin -= tzhour * MINUTES_PER_HOUR;
323             pad(tzhour, TWO_DIGITS, buf);
324             buf.append(":");
325             pad(tzmin, TWO_DIGITS, buf);
326         }
327         synchronized (this) {
328             if (last == lastTimestamp) {
329                 lastTimestamp = now;
330                 timestamppStr = buf.toString();
331             }
332         }
333         return buf.toString();
334     }
335 
336     private void pad(final int val, int max, final StringBuilder buf) {
337         while (max > 1) {
338             if (val < max) {
339                 buf.append("0");
340             }
341             max = max / TWO_DIGITS;
342         }
343         buf.append(Integer.toString(val));
344     }
345 
346     private void formatStructuredElement(final StructuredDataId id, final Map<String, String> data,
347                                          final StringBuilder sb, final ListChecker checker) {
348         if (id == null && defaultId == null) {
349             return;
350         }
351         sb.append("[");
352         sb.append(getId(id));
353         appendMap(data, sb, checker);
354         sb.append("]");
355     }
356 
357     private String getId(final StructuredDataId id) {
358         final StringBuilder sb = new StringBuilder();
359         if (id.getName() == null) {
360             sb.append(defaultId);
361         } else {
362             sb.append(id.getName());
363         }
364         int ein = id.getEnterpriseNumber();
365         if (ein < 0) {
366             ein = enterpriseNumber;
367         }
368         if (ein >= 0) {
369             sb.append("@").append(ein);
370         }
371         return sb.toString();
372     }
373 
374     private void checkRequired(final Map<String, String> map) {
375         for (final String key : mdcRequired) {
376             final String value = map.get(key);
377             if (value == null) {
378                 throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
379             }
380         }
381     }
382 
383     private void appendMap(final Map<String, String> map, final StringBuilder sb, final ListChecker checker)
384     {
385         final SortedMap<String, String> sorted = new TreeMap<String, String>(map);
386         for (final Map.Entry<String, String> entry : sorted.entrySet()) {
387             if (checker.check(entry.getKey()) && entry.getValue() != null) {
388                 sb.append(" ");
389                 sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
390             }
391         }
392     }
393 
394     /**
395      * Interface used to check keys in a Map.
396      */
397     private interface ListChecker {
398         boolean check(String key);
399     }
400 
401     /**
402      * Includes only the listed keys.
403      */
404     private class IncludeChecker implements ListChecker {
405         public boolean check(final String key) {
406             return mdcIncludes.contains(key);
407         }
408     }
409 
410     /**
411      * Excludes the listed keys.
412      */
413     private class ExcludeChecker implements ListChecker {
414         public boolean check(final String key) {
415             return !mdcExcludes.contains(key);
416         }
417     }
418 
419     /**
420      * Does nothing.
421      */
422     private class NoopChecker implements ListChecker {
423         public boolean check(final String key) {
424             return true;
425         }
426     }
427 
428     @Override
429     public String toString() {
430         final StringBuilder sb = new StringBuilder();
431         sb.append("facility=").append(facility.name());
432         sb.append(" appName=").append(appName);
433         sb.append(" defaultId=").append(defaultId);
434         sb.append(" enterpriseNumber=").append(enterpriseNumber);
435         sb.append(" newLine=").append(includeNewLine);
436         sb.append(" includeMDC=").append(includeMDC);
437         sb.append(" messageId=").append(messageId);
438         return sb.toString();
439     }
440 
441     /**
442      * Create the RFC 5424 Layout.
443      * @param facility The Facility is used to try to classify the message.
444      * @param id The default structured data id to use when formatting according to RFC 5424.
445      * @param ein The IANA enterprise number.
446      * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
447      * record. Defaults to "true:.
448      * @param mdcId The id to use for the MDC Structured Data Element.
449      * @param includeNL If true, a newline will be appended to the end of the syslog record. The default is false.
450      * @param escapeNL String that should be used to replace newlines within the message text.
451      * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record.
452      * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records.
453      * @param excludes A comma separated list of mdc keys that should be excluded from the LogEvent.
454      * @param includes A comma separated list of mdc keys that should be included in the FlumeEvent.
455      * @param required A comma separated list of mdc keys that must be present in the MDC.
456      * @param charsetName The character set.
457      * @param exceptionPattern The pattern for formatting exceptions.
458      * @param config The Configuration. Some Converters require access to the Interpolator.
459      * @return An RFC5424Layout.
460      */
461     @PluginFactory
462     public static RFC5424Layout createLayout(@PluginAttr("facility") final String facility,
463                                              @PluginAttr("id") final String id,
464                                              @PluginAttr("enterpriseNumber") final String ein,
465                                              @PluginAttr("includeMDC") final String includeMDC,
466                                              @PluginAttr("mdcId") String mdcId,
467                                              @PluginAttr("newLine") final String includeNL,
468                                              @PluginAttr("newLineEscape") final String escapeNL,
469                                              @PluginAttr("appName") final String appName,
470                                              @PluginAttr("messageId") final String msgId,
471                                              @PluginAttr("mdcExcludes") final String excludes,
472                                              @PluginAttr("mdcIncludes") String includes,
473                                              @PluginAttr("mdcRequired") final String required,
474                                              @PluginAttr("charset") final String charsetName,
475                                              @PluginAttr("exceptionPattern") final String exceptionPattern,
476                                              @PluginConfiguration final Configuration config) {
477         final Charset charset = Charsets.getSupportedCharset(charsetName);
478         if (includes != null && excludes != null) {
479             LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
480             includes = null;
481         }
482         final Facility f = Facility.toFacility(facility, Facility.LOCAL0);
483         final int enterpriseNumber = ein == null ? DEFAULT_ENTERPRISE_NUMBER : Integer.parseInt(ein);
484         final boolean isMdc = includeMDC == null ? true : Boolean.valueOf(includeMDC);
485         final boolean includeNewLine = includeNL == null ? false : Boolean.valueOf(includeNL);
486         if (mdcId == null) {
487             mdcId = DEFAULT_MDCID;
488         }
489 
490         return new RFC5424Layout(config, f, id, enterpriseNumber, isMdc, includeNewLine, escapeNL, mdcId, appName,
491                                  msgId, excludes, includes, required, charset, exceptionPattern);
492     }
493 }