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 org.apache.logging.log4j.LoggingException;
020    import org.apache.logging.log4j.core.LogEvent;
021    import org.apache.logging.log4j.core.config.Configuration;
022    import org.apache.logging.log4j.core.config.plugins.Plugin;
023    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
024    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
025    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
026    import org.apache.logging.log4j.core.helpers.Charsets;
027    import org.apache.logging.log4j.core.helpers.NetUtils;
028    import org.apache.logging.log4j.core.net.Facility;
029    import org.apache.logging.log4j.core.net.Priority;
030    import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
031    import org.apache.logging.log4j.core.pattern.PatternFormatter;
032    import org.apache.logging.log4j.core.pattern.PatternParser;
033    import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
034    import org.apache.logging.log4j.message.Message;
035    import org.apache.logging.log4j.message.StructuredDataId;
036    import org.apache.logging.log4j.message.StructuredDataMessage;
037    
038    import java.nio.charset.Charset;
039    import java.util.ArrayList;
040    import java.util.Calendar;
041    import java.util.GregorianCalendar;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.SortedMap;
045    import java.util.TreeMap;
046    import java.util.regex.Matcher;
047    import java.util.regex.Pattern;
048    
049    
050    /**
051     * Formats a log event in accordance with RFC 5424.
052     */
053    @Plugin(name = "RFC5424Layout", type = "Core", elementType = "layout", printObject = true)
054    public final class RFC5424Layout extends AbstractStringLayout {
055    
056        /**
057         * Not a very good default - it is the Apache Software Foundation's enterprise number.
058         */
059        public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
060        /**
061         * The default event id.
062         */
063        public static final String DEFAULT_ID = "Audit";
064        /**
065         * Match newlines in a platform-independent manner.
066         */
067        public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
068    
069        private static final String DEFAULT_MDCID = "mdc";
070        private static final int TWO_DIGITS = 10;
071        private static final int THREE_DIGITS = 100;
072        private static final int MILLIS_PER_MINUTE = 60000;
073        private static final int MINUTES_PER_HOUR = 60;
074    
075        private static final String COMPONENT_KEY = "RFC5424-Converter";
076    
077        private final Facility facility;
078        private final String defaultId;
079        private final Integer enterpriseNumber;
080        private final boolean includeMDC;
081        private final String mdcId;
082        private final String localHostName;
083        private final String appName;
084        private final String messageId;
085        private final String configName;
086        private final List<String> mdcExcludes;
087        private final List<String> mdcIncludes;
088        private final List<String> mdcRequired;
089        private final ListChecker checker;
090        private final ListChecker noopChecker = new NoopChecker();
091        private final boolean includeNewLine;
092        private final String escapeNewLine;
093    
094        private long lastTimestamp = -1;
095        private String timestamppStr;
096    
097        private final List<PatternFormatter> formatters;
098    
099        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    }