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