replace StringBuilder based API with Buffer-based API (providing separate TextBuffer and BinaryBuffer implementations where appropriate)
Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/08386d0e Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/08386d0e Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/08386d0e Branch: refs/heads/LOG4J2-930 Commit: 08386d0e0dd578fc1d020f24975da54da9f15651 Parents: 8b748ca Author: rpopma <[email protected]> Authored: Mon Jan 12 17:42:30 2015 +0900 Committer: rpopma <[email protected]> Committed: Mon Jan 12 17:42:30 2015 +0900 ---------------------------------------------------------------------- .../log4j/core/layout/PatternLayout.java | 154 +- .../log4j/core/layout/Rfc5424Layout.java | 1438 +++++++++--------- .../pattern/AbstractStyleNameConverter.java | 129 +- .../core/pattern/ArrayPatternConverter.java | 4 +- .../core/pattern/ClassNamePatternConverter.java | 36 +- .../core/pattern/DatePatternConverter.java | 187 ++- .../core/pattern/EncodingPatternConverter.java | 95 +- .../ExtendedThrowablePatternConverter.java | 44 +- .../core/pattern/FileDatePatternConverter.java | 14 +- .../pattern/FileLocationPatternConverter.java | 30 +- .../log4j/core/pattern/FormattingInfo.java | 86 +- .../pattern/FullLocationPatternConverter.java | 29 +- .../log4j/core/pattern/HighlightConverter.java | 472 +++--- .../core/pattern/IntegerPatternConverter.java | 19 +- .../core/pattern/LevelPatternConverter.java | 102 +- .../pattern/LineLocationPatternConverter.java | 43 +- .../pattern/LineSeparatorPatternConverter.java | 32 +- .../core/pattern/LiteralPatternConverter.java | 38 +- .../core/pattern/LogEventPatternConverter.java | 151 +- .../core/pattern/LoggerPatternConverter.java | 32 +- .../log4j/core/pattern/MapPatternConverter.java | 63 +- .../core/pattern/MarkerPatternConverter.java | 26 +- .../log4j/core/pattern/MdcPatternConverter.java | 66 +- .../core/pattern/MessagePatternConverter.java | 59 +- .../pattern/MethodLocationPatternConverter.java | 29 +- .../core/pattern/NamePatternConverter.java | 39 +- .../log4j/core/pattern/NdcPatternConverter.java | 59 +- .../core/pattern/RegexReplacementConverter.java | 40 +- .../pattern/RelativeTimePatternConverter.java | 38 +- .../pattern/RootThrowablePatternConverter.java | 42 +- .../pattern/SequenceNumberPatternConverter.java | 37 +- .../log4j/core/pattern/StyleConverter.java | 80 +- .../core/pattern/ThreadPatternConverter.java | 42 +- .../core/pattern/ThrowablePatternConverter.java | 80 +- .../core/pattern/UuidPatternConverter.java | 28 +- .../core/pattern/DatePatternConverterTest.java | 41 +- .../pattern/EncodingPatternConverterTest.java | 4 +- .../ExtendedThrowablePatternConverterTest.java | 32 +- .../log4j/core/pattern/FormattingInfoTest.java | 142 ++ .../core/pattern/LevelPatternConverterTest.java | 30 +- .../core/pattern/MapPatternConverterTest.java | 8 +- .../pattern/MarkerPatternConverterTest.java | 4 +- .../pattern/MessagePatternConverterTest.java | 16 +- .../log4j/core/pattern/PatternParserTest.java | 6 +- .../log4j/core/pattern/PatternParserTest2.java | 16 +- .../pattern/RegexReplacementConverterTest.java | 4 +- .../RootThrowablePatternConverterTest.java | 8 +- .../pattern/ThrowablePatternConverterTest.java | 37 +- 48 files changed, 2479 insertions(+), 1732 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/08386d0e/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java index 45079fa..75ce6a4 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java @@ -33,10 +33,13 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.pattern.BinaryBuffer; import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; import org.apache.logging.log4j.core.pattern.PatternFormatter; import org.apache.logging.log4j.core.pattern.PatternParser; import org.apache.logging.log4j.core.pattern.RegexReplacement; +import org.apache.logging.log4j.core.pattern.TextBuffer; +import org.apache.logging.log4j.util.PropertiesUtil; /** * A flexible layout configurable with pattern string. @@ -58,25 +61,20 @@ public final class PatternLayout extends AbstractStringLayout { private static final long serialVersionUID = 1L; /** - * Default pattern string for log output. Currently set to the - * string <b>"%m%n"</b> which just prints the application supplied - * message. + * Default pattern string for log output. Currently set to the string <b>"%m%n"</b> which just prints the + * application supplied message. */ public static final String DEFAULT_CONVERSION_PATTERN = "%m%n"; /** - * A conversion pattern equivalent to the TTCCCLayout. - * Current value is <b>%r [%t] %p %c %x - %m%n</b>. + * A conversion pattern equivalent to the TTCCCLayout. Current value is <b>%r [%t] %p %c %x - %m%n</b>. */ - public static final String TTCC_CONVERSION_PATTERN = - "%r [%t] %p %c %x - %m%n"; + public static final String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n"; /** - * A simple pattern. - * Current value is <b>%d [%t] %p %c - %m%n</b>. + * A simple pattern. Current value is <b>%d [%t] %p %c - %m%n</b>. */ - public static final String SIMPLE_CONVERSION_PATTERN = - "%d [%t] %p %c - %m%n"; + public static final String SIMPLE_CONVERSION_PATTERN = "%d [%t] %p %c - %m%n"; /** Key to identify pattern converters. */ public static final String KEY = "Converter"; @@ -84,14 +82,14 @@ public final class PatternLayout extends AbstractStringLayout { /** * Initial converter for pattern. */ - private final List<PatternFormatter> formatters; + // store in raw array instead of a collection for performance reasons + private final PatternFormatter[] formatters; /** * Conversion pattern. */ private final String conversionPattern; - /** * The current Configuration. */ @@ -103,22 +101,24 @@ public final class PatternLayout extends AbstractStringLayout { private final boolean noConsoleNoAnsi; + private final boolean supportsBinaryOutput; + /** - * Constructs a EnhancedPatternLayout using the supplied conversion pattern. + * Constructs a PatternLayout using the supplied conversion pattern. * * @param config The Configuration. * @param replace The regular expression to match. * @param pattern conversion pattern. * @param charset The character set. * @param alwaysWriteExceptions Whether or not exceptions should always be handled in this pattern (if {@code true}, - * exceptions will be written even if the pattern does not specify so). - * @param noConsoleNoAnsi - * If {@code "true"} (default) and {@link System#console()} is null, do not output ANSI escape codes + * exceptions will be written even if the pattern does not specify so). + * @param noConsoleNoAnsi If {@code "true"} (default) and {@link System#console()} is null, do not output ANSI + * escape codes * @param header */ private PatternLayout(final Configuration config, final RegexReplacement replace, final String pattern, - final Charset charset, final boolean alwaysWriteExceptions, final boolean noConsoleNoAnsi, - final String header, final String footer) { + final Charset charset, final boolean alwaysWriteExceptions, final boolean noConsoleNoAnsi, + final String header, final String footer) { super(charset, toBytes(header, charset), toBytes(footer, charset)); this.replace = replace; this.conversionPattern = pattern; @@ -126,7 +126,17 @@ public final class PatternLayout extends AbstractStringLayout { this.alwaysWriteExceptions = alwaysWriteExceptions; this.noConsoleNoAnsi = noConsoleNoAnsi; final PatternParser parser = createPatternParser(config); - this.formatters = parser.parse(pattern == null ? DEFAULT_CONVERSION_PATTERN : pattern, this.alwaysWriteExceptions, this.noConsoleNoAnsi); + final List<PatternFormatter> list = parser.parse(pattern == null ? DEFAULT_CONVERSION_PATTERN : pattern, + this.alwaysWriteExceptions, this.noConsoleNoAnsi); + this.formatters = (PatternFormatter[]) list.toArray(new PatternFormatter[list.size()]); + this.supportsBinaryOutput = replace == null && supportBinaryOutput(formatters); + for (PatternFormatter patternFormatter : list) { + patternFormatter.getConverter().setCharset(charset); + } + } + + private static boolean supportBinaryOutput(final PatternFormatter[] formatters) { + return PropertiesUtil.getProperties().getBooleanProperty("log4j.use.BinaryBuffers", true); } private static byte[] toBytes(final String str, final Charset charset) { @@ -136,7 +146,7 @@ public final class PatternLayout extends AbstractStringLayout { return null; } - private byte[] strSubstitutorReplace(final byte... b) { + private byte[] strSubstitutorReplace(final byte[] b) { if (b != null && config != null) { return getBytes(config.getStrSubstitutor().replace(new String(b, getCharset()))); } @@ -173,14 +183,20 @@ public final class PatternLayout extends AbstractStringLayout { * @return Map of content format keys supporting PatternLayout */ @Override - public Map<String, String> getContentFormat() - { + public Map<String, String> getContentFormat() { final Map<String, String> result = new HashMap<String, String>(); result.put("structured", "false"); result.put("formatType", "conversion"); result.put("format", conversionPattern); return result; } + + private ThreadLocal<TextBuffer> textBuffer = new ThreadLocal<TextBuffer>() { + @Override + protected TextBuffer initialValue() { + return new TextBuffer(); + } + }; /** * Formats a logging event to a writer. @@ -191,9 +207,12 @@ public final class PatternLayout extends AbstractStringLayout { */ @Override public String toSerializable(final LogEvent event) { - final StringBuilder buf = new StringBuilder(); - for (final PatternFormatter formatter : formatters) { - formatter.format(event, buf); + final TextBuffer buf = textBuffer.get(); + buf.setLength(0); + + // PERFORMANCE-SENSITIVE CODE: do not refactor to for-each loop + for (int i = 0, len = formatters.length; i < len; i++) { + formatters[i].format(event, buf); } String str = buf.toString(); if (replace != null) { @@ -201,9 +220,39 @@ public final class PatternLayout extends AbstractStringLayout { } return str; } + + private ThreadLocal<BinaryBuffer> binaryBuffer = new ThreadLocal<BinaryBuffer>() { + @Override + protected BinaryBuffer initialValue() { + return new BinaryBuffer(getCharset()); + } + }; + + /** + * Formats the Log Event as a byte array. + * + * @param event The Log Event. + * @return The formatted event as a byte array. + */ + @Override + public byte[] toByteArray(final LogEvent event) { + if (!supportsBinaryOutput) { + return getBytes(toSerializable(event)); + } + final Charset charset = getCharset(); + final BinaryBuffer buf = binaryBuffer.get(); + buf.setLength(0); + + // PERFORMANCE-SENSITIVE CODE: do not refactor to for-each loop + for (int i = 0, len = formatters.length; i < len; i++) { + formatters[i].format(event, buf, charset); + } + return buf.toByteArray(); + } /** * Create a PatternParser. + * * @param config The Configuration. * @return The PatternParser. */ @@ -228,22 +277,16 @@ public final class PatternLayout extends AbstractStringLayout { /** * Create a pattern layout. * - * @param pattern - * The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN. - * @param config - * The Configuration. Some Converters require access to the Interpolator. - * @param replace - * A Regex replacement String. - * @param charset - * The character set. - * @param alwaysWriteExceptions - * If {@code "true"} (default) exceptions are always written even if the pattern contains no exception tokens. - * @param noConsoleNoAnsi - * If {@code "true"} (default is false) and {@link System#console()} is null, do not output ANSI escape codes - * @param header - * The footer to place at the top of the document, once. - * @param footer - * The footer to place at the bottom of the document, once. + * @param pattern The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN. + * @param config The Configuration. Some Converters require access to the Interpolator. + * @param replace A Regex replacement String. + * @param charset The character set. + * @param alwaysWriteExceptions If {@code "true"} (default) exceptions are always written even if the pattern + * contains no exception tokens. + * @param noConsoleNoAnsi If {@code "true"} (default is false) and {@link System#console()} is null, do not output + * ANSI escape codes + * @param header The footer to place at the top of the document, once. + * @param footer The footer to place at the bottom of the document, once. * @return The PatternLayout. */ @PluginFactory @@ -251,21 +294,14 @@ public final class PatternLayout extends AbstractStringLayout { @PluginAttribute(value = "pattern", defaultString = DEFAULT_CONVERSION_PATTERN) final String pattern, @PluginConfiguration final Configuration config, @PluginElement("Replace") final RegexReplacement replace, - @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset, + /* Cannot do defaultString = Charset.defaultCharset().name() for charset... */ + @PluginAttribute(value = "charset") final Charset charset, @PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) final boolean alwaysWriteExceptions, @PluginAttribute(value = "noConsoleNoAnsi", defaultBoolean = false) final boolean noConsoleNoAnsi, - @PluginAttribute("header") final String header, - @PluginAttribute("footer") final String footer) { - return newBuilder() - .withPattern(pattern) - .withConfiguration(config) - .withRegexReplacement(replace) - .withCharset(charset) - .withAlwaysWriteExceptions(alwaysWriteExceptions) - .withNoConsoleNoAnsi(noConsoleNoAnsi) - .withHeader(header) - .withFooter(footer) - .build(); + @PluginAttribute("header") final String header, @PluginAttribute("footer") final String footer) { + return newBuilder().withPattern(pattern).withConfiguration(config).withRegexReplacement(replace) + .withCharset(charset).withAlwaysWriteExceptions(alwaysWriteExceptions) + .withNoConsoleNoAnsi(noConsoleNoAnsi).withHeader(header).withFooter(footer).build(); } /** @@ -281,6 +317,7 @@ public final class PatternLayout extends AbstractStringLayout { /** * Creates a builder for a custom PatternLayout. + * * @return a PatternLayout builder. */ @PluginBuilderFactory @@ -331,7 +368,6 @@ public final class PatternLayout extends AbstractStringLayout { return this; } - public Builder withConfiguration(final Configuration configuration) { this.configuration = configuration; return this; @@ -343,7 +379,9 @@ public final class PatternLayout extends AbstractStringLayout { } public Builder withCharset(final Charset charset) { - this.charset = charset; + if (charset != null) { + this.charset = charset; // else use platform default (NOT utf-8!) + } return this; } @@ -374,7 +412,7 @@ public final class PatternLayout extends AbstractStringLayout { configuration = new DefaultConfiguration(); } return new PatternLayout(configuration, regexReplacement, pattern, charset, alwaysWriteExceptions, - noConsoleNoAnsi, header, footer); + noConsoleNoAnsi, header, footer); } } } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/08386d0e/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java index 340c5b3..8aa7ab1 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java @@ -1,718 +1,720 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache license, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the license for the specific language governing permissions and - * limitations under the license. - */ -package org.apache.logging.log4j.core.layout; - -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LoggingException; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.appender.TlsSyslogFrame; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.Node; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.core.net.Facility; -import org.apache.logging.log4j.core.net.Priority; -import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; -import org.apache.logging.log4j.core.pattern.PatternConverter; -import org.apache.logging.log4j.core.pattern.PatternFormatter; -import org.apache.logging.log4j.core.pattern.PatternParser; -import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter; -import org.apache.logging.log4j.core.util.Charsets; -import org.apache.logging.log4j.core.util.NetUtils; -import org.apache.logging.log4j.core.util.Patterns; -import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.message.StructuredDataId; -import org.apache.logging.log4j.message.StructuredDataMessage; -import org.apache.logging.log4j.util.Strings; - - -/** - * Formats a log event in accordance with RFC 5424. - * - * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a> - */ -@Plugin(name = "Rfc5424Layout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) -public final class Rfc5424Layout extends AbstractStringLayout { - - private static final long serialVersionUID = 1L; - - private static final String LF = "\n"; - - /** - * Not a very good default - it is the Apache Software Foundation's enterprise number. - */ - public static final int DEFAULT_ENTERPRISE_NUMBER = 18060; - /** - * The default event id. - */ - public static final String DEFAULT_ID = "Audit"; - /** - * Match newlines in a platform-independent manner. - */ - public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n"); - /** - * Match characters which require escaping - */ - public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]"); - - public static final String DEFAULT_MDCID = "mdc"; - private static final int TWO_DIGITS = 10; - private static final int THREE_DIGITS = 100; - private static final int MILLIS_PER_MINUTE = 60000; - private static final int MINUTES_PER_HOUR = 60; - - private static final String COMPONENT_KEY = "RFC5424-Converter"; - - private final Facility facility; - private final String defaultId; - private final int enterpriseNumber; - private final boolean includeMdc; - private final String mdcId; - private final StructuredDataId mdcSdId; - private final String localHostName; - private final String appName; - private final String messageId; - private final String configName; - private final String mdcPrefix; - private final String eventPrefix; - private final List<String> mdcExcludes; - private final List<String> mdcIncludes; - private final List<String> mdcRequired; - private final ListChecker checker; - private final ListChecker noopChecker = new NoopChecker(); - private final boolean includeNewLine; - private final String escapeNewLine; - private final boolean useTlsMessageFormat; - - private long lastTimestamp = -1; - private String timestamppStr; - - private final List<PatternFormatter> exceptionFormatters; - private final Map<String, FieldFormatter> fieldFormatters; - - private Rfc5424Layout(final Configuration config, final Facility facility, final String id, final int ein, - final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId, - final String mdcPrefix, final String eventPrefix, - final String appName, final String messageId, final String excludes, final String includes, - final String required, final Charset charset, final String exceptionPattern, - final boolean useTLSMessageFormat, final LoggerFields[] loggerFields) { - super(charset); - final PatternParser exceptionParser = createPatternParser(config, ThrowablePatternConverter.class); - exceptionFormatters = exceptionPattern == null ? null : exceptionParser.parse(exceptionPattern, false, false); - this.facility = facility; - this.defaultId = id == null ? DEFAULT_ID : id; - this.enterpriseNumber = ein; - this.includeMdc = includeMDC; - this.includeNewLine = includeNL; - this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL); - this.mdcId = mdcId; - this.mdcSdId = new StructuredDataId(mdcId, enterpriseNumber, null, null); - this.mdcPrefix = mdcPrefix; - this.eventPrefix = eventPrefix; - this.appName = appName; - this.messageId = messageId; - this.useTlsMessageFormat = useTLSMessageFormat; - this.localHostName = NetUtils.getLocalHostname(); - ListChecker c = null; - if (excludes != null) { - final String[] array = excludes.split(Patterns.COMMA_SEPARATOR); - if (array.length > 0) { - c = new ExcludeChecker(); - mdcExcludes = new ArrayList<String>(array.length); - for (final String str : array) { - mdcExcludes.add(str.trim()); - } - } else { - mdcExcludes = null; - } - } else { - mdcExcludes = null; - } - if (includes != null) { - final String[] array = includes.split(Patterns.COMMA_SEPARATOR); - if (array.length > 0) { - c = new IncludeChecker(); - mdcIncludes = new ArrayList<String>(array.length); - for (final String str : array) { - mdcIncludes.add(str.trim()); - } - } else { - mdcIncludes = null; - } - } else { - mdcIncludes = null; - } - if (required != null) { - final String[] array = required.split(Patterns.COMMA_SEPARATOR); - if (array.length > 0) { - mdcRequired = new ArrayList<String>(array.length); - for (final String str : array) { - mdcRequired.add(str.trim()); - } - } else { - mdcRequired = null; - } - - } else { - mdcRequired = null; - } - this.checker = c != null ? c : noopChecker; - final String name = config == null ? null : config.getName(); - configName = name != null && name.length() > 0 ? name : null; - this.fieldFormatters = createFieldFormatters(loggerFields, config); - } - - private Map<String, FieldFormatter> createFieldFormatters(final LoggerFields[] loggerFields, - final Configuration config) { - final Map<String, FieldFormatter> sdIdMap = new HashMap<String, FieldFormatter>(); - - if (loggerFields != null) { - for (final LoggerFields lField : loggerFields) { - final StructuredDataId key = lField.getSdId() == null ? mdcSdId : lField.getSdId(); - final Map<String, List<PatternFormatter>> sdParams = new HashMap<String, List<PatternFormatter>>(); - final Map<String, String> fields = lField.getMap(); - if (!fields.isEmpty()) { - final PatternParser fieldParser = createPatternParser(config, null); - - for (final Map.Entry<String, String> entry : fields.entrySet()) { - final List<PatternFormatter> formatters = fieldParser.parse(entry.getValue(), false, false); - sdParams.put(entry.getKey(), formatters); - } - final FieldFormatter fieldFormatter = new FieldFormatter(sdParams, - lField.getDiscardIfAllFieldsAreEmpty()); - sdIdMap.put(key.toString(), fieldFormatter); - } - } - } - return sdIdMap.size() > 0 ? sdIdMap : null; - } - - /** - * Create a PatternParser. - * - * @param config The Configuration. - * @param filterClass Filter the returned plugins after calling the plugin manager. - * @return The PatternParser. - */ - private static PatternParser createPatternParser(final Configuration config, - final Class<? extends PatternConverter> filterClass) { - if (config == null) { - return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class, filterClass); - } - PatternParser parser = config.getComponent(COMPONENT_KEY); - if (parser == null) { - parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class); - config.addComponent(COMPONENT_KEY, parser); - parser = (PatternParser) config.getComponent(COMPONENT_KEY); - } - return parser; - } - - /** - * Gets this Rfc5424Layout's content format. Specified by: - * <ul> - * <li>Key: "structured" Value: "true"</li> - * <li>Key: "format" Value: "RFC5424"</li> - * </ul> - * - * @return Map of content format keys supporting Rfc5424Layout - */ - @Override - public Map<String, String> getContentFormat() { - final Map<String, String> result = new HashMap<String, String>(); - result.put("structured", "true"); - result.put("formatType", "RFC5424"); - return result; - } - - /** - * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification. - * - * @param event The LogEvent. - * @return The RFC 5424 String representation of the LogEvent. - */ - @Override - public String toSerializable(final LogEvent event) { - final StringBuilder buf = new StringBuilder(); - appendPriority(buf, event.getLevel()); - appendTimestamp(buf, event.getTimeMillis()); - appendSpace(buf); - appendHostName(buf); - appendSpace(buf); - appendAppName(buf); - appendSpace(buf); - appendProcessId(buf); - appendSpace(buf); - appendMessageId(buf, event.getMessage()); - appendSpace(buf); - appendStructuredElements(buf, event); - appendMessage(buf, event); - if (useTlsMessageFormat) { - return new TlsSyslogFrame(buf.toString()).toString(); - } - return buf.toString(); - } - - private void appendPriority(final StringBuilder buffer, final Level logLevel) { - buffer.append('<'); - buffer.append(Priority.getPriority(facility, logLevel)); - buffer.append(">1 "); - } - - private void appendTimestamp(final StringBuilder buffer, final long milliseconds) { - buffer.append(computeTimeStampString(milliseconds)); - } - - private void appendSpace(final StringBuilder buffer) { - buffer.append(' '); - } - - private void appendHostName(final StringBuilder buffer) { - buffer.append(localHostName); - } - - private void appendAppName(final StringBuilder buffer) { - if (appName != null) { - buffer.append(appName); - } else if (configName != null) { - buffer.append(configName); - } else { - buffer.append('-'); - } - } - - private void appendProcessId(final StringBuilder buffer) { - buffer.append(getProcId()); - } - - private void appendMessageId(final StringBuilder buffer, final Message message) { - final boolean isStructured = message instanceof StructuredDataMessage; - final String type = isStructured ? ((StructuredDataMessage) message).getType() : null; - if (type != null) { - buffer.append(type); - } else if (messageId != null) { - buffer.append(messageId); - } else { - buffer.append('-'); - } - } - - private void appendMessage(final StringBuilder buffer, final LogEvent event) { - final Message message = event.getMessage(); - // This layout formats StructuredDataMessages instead of delegating to the Message itself. - final String text = (message instanceof StructuredDataMessage) ? message.getFormat() : message.getFormattedMessage(); - - if (text != null && text.length() > 0) { - buffer.append(' ').append(escapeNewlines(text, escapeNewLine)); - } - - if (exceptionFormatters != null && event.getThrown() != null) { - final StringBuilder exception = new StringBuilder(LF); - for (final PatternFormatter formatter : exceptionFormatters) { - formatter.format(event, exception); - } - buffer.append(escapeNewlines(exception.toString(), escapeNewLine)); - } - if (includeNewLine) { - buffer.append(LF); - } - } - - private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) { - final Message message = event.getMessage(); - final boolean isStructured = message instanceof StructuredDataMessage; - - if (!isStructured && (fieldFormatters!= null && fieldFormatters.isEmpty()) && !includeMdc) { - buffer.append('-'); - return; - } - - final Map<String, StructuredDataElement> sdElements = new HashMap<String, StructuredDataElement>(); - final Map<String, String> contextMap = event.getContextMap(); - - if (mdcRequired != null) { - checkRequired(contextMap); - } - - if (fieldFormatters != null) { - for (final Map.Entry<String, FieldFormatter> sdElement: fieldFormatters.entrySet()) { - final String sdId = sdElement.getKey(); - final StructuredDataElement elem = sdElement.getValue().format(event); - sdElements.put(sdId, elem); - } - } - - if (includeMdc && contextMap.size() > 0) { - if (sdElements.containsKey(mdcSdId.toString())) { - final StructuredDataElement union = sdElements.get(mdcSdId.toString()); - union.union(contextMap); - sdElements.put(mdcSdId.toString(), union); - } else { - final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false); - sdElements.put(mdcSdId.toString(), formattedContextMap); - } - } - - if (isStructured) { - final StructuredDataMessage data = (StructuredDataMessage) message; - final Map<String, String> map = data.getData(); - final StructuredDataId id = data.getId(); - final String sdId = getId(id); - - if (sdElements.containsKey(sdId)) { - final StructuredDataElement union = sdElements.get(id.toString()); - union.union(map); - sdElements.put(sdId, union); - } else { - final StructuredDataElement formattedData = new StructuredDataElement(map, false); - sdElements.put(sdId, formattedData); - } - } - - if (sdElements.isEmpty()) { - buffer.append('-'); - return; - } - - for (final Map.Entry<String, StructuredDataElement> entry: sdElements.entrySet()) { - formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, checker); - } - } - - private String escapeNewlines(final String text, final String escapeNewLine) { - if (null == escapeNewLine) { - return text; - } - return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine); - } - - protected String getProcId() { - return "-"; - } - - protected List<String> getMdcExcludes() { - return mdcExcludes; - } - - protected List<String> getMdcIncludes() { - return mdcIncludes; - } - - private String computeTimeStampString(final long now) { - long last; - synchronized (this) { - last = lastTimestamp; - if (now == lastTimestamp) { - return timestamppStr; - } - } - - final StringBuilder buffer = new StringBuilder(); - final Calendar cal = new GregorianCalendar(); - cal.setTimeInMillis(now); - buffer.append(Integer.toString(cal.get(Calendar.YEAR))); - buffer.append('-'); - pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer); - buffer.append('-'); - pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer); - buffer.append('T'); - pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer); - buffer.append(':'); - pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer); - buffer.append(':'); - pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer); - - final int millis = cal.get(Calendar.MILLISECOND); - if (millis != 0) { - buffer.append('.'); - pad(millis, THREE_DIGITS, buffer); - } - - int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE; - if (tzmin == 0) { - buffer.append('Z'); - } else { - if (tzmin < 0) { - tzmin = -tzmin; - buffer.append('-'); - } else { - buffer.append('+'); - } - final int tzhour = tzmin / MINUTES_PER_HOUR; - tzmin -= tzhour * MINUTES_PER_HOUR; - pad(tzhour, TWO_DIGITS, buffer); - buffer.append(':'); - pad(tzmin, TWO_DIGITS, buffer); - } - synchronized (this) { - if (last == lastTimestamp) { - lastTimestamp = now; - timestamppStr = buffer.toString(); - } - } - return buffer.toString(); - } - - private void pad(final int val, int max, final StringBuilder buf) { - while (max > 1) { - if (val < max) { - buf.append('0'); - } - max = max / TWO_DIGITS; - } - buf.append(Integer.toString(val)); - } - - private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data, - final StringBuilder sb, final ListChecker checker) { - if ((id == null && defaultId == null) || data.discard()) { - return; - } - - sb.append('['); - sb.append(id); - if (!mdcSdId.toString().equals(id)) { - appendMap(prefix, data.getFields(), sb, noopChecker); - } else { - appendMap(prefix, data.getFields(), sb, checker); - } - sb.append(']'); - } - - private String getId(final StructuredDataId id) { - final StringBuilder sb = new StringBuilder(); - if (id == null || id.getName() == null) { - sb.append(defaultId); - } else { - sb.append(id.getName()); - } - int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber; - if (ein < 0) { - ein = enterpriseNumber; - } - if (ein >= 0) { - sb.append('@').append(ein); - } - return sb.toString(); - } - - private void checkRequired(final Map<String, String> map) { - for (final String key : mdcRequired) { - final String value = map.get(key); - if (value == null) { - throw new LoggingException("Required key " + key + " is missing from the " + mdcId); - } - } - } - - private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb, - final ListChecker checker) { - final SortedMap<String, String> sorted = new TreeMap<String, String>(map); - for (final Map.Entry<String, String> entry : sorted.entrySet()) { - if (checker.check(entry.getKey()) && entry.getValue() != null) { - sb.append(' '); - if (prefix != null) { - sb.append(prefix); - } - sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"") - .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"'); - } - } - } - - private String escapeSDParams(final String value) { - return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0"); - } - - /** - * Interface used to check keys in a Map. - */ - private interface ListChecker { - boolean check(String key); - } - - /** - * Includes only the listed keys. - */ - private class IncludeChecker implements ListChecker { - @Override - public boolean check(final String key) { - return mdcIncludes.contains(key); - } - } - - /** - * Excludes the listed keys. - */ - private class ExcludeChecker implements ListChecker { - @Override - public boolean check(final String key) { - return !mdcExcludes.contains(key); - } - } - - /** - * Does nothing. - */ - private class NoopChecker implements ListChecker { - @Override - public boolean check(final String key) { - return true; - } - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - sb.append("facility=").append(facility.name()); - sb.append(" appName=").append(appName); - sb.append(" defaultId=").append(defaultId); - sb.append(" enterpriseNumber=").append(enterpriseNumber); - sb.append(" newLine=").append(includeNewLine); - sb.append(" includeMDC=").append(includeMdc); - sb.append(" messageId=").append(messageId); - return sb.toString(); - } - - /** - * Create the RFC 5424 Layout. - * - * @param facility The Facility is used to try to classify the message. - * @param id The default structured data id to use when formatting according to RFC 5424. - * @param enterpriseNumber The IANA enterprise number. - * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog - * record. Defaults to "true:. - * @param mdcId The id to use for the MDC Structured Data Element. - * @param mdcPrefix The prefix to add to MDC key names. - * @param eventPrefix The prefix to add to event key names. - * @param newLine If true, a newline will be appended to the end of the syslog record. The default is false. - * @param escapeNL String that should be used to replace newlines within the message text. - * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record. - * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records. - * @param excludes A comma separated list of MDC keys that should be excluded from the LogEvent. - * @param includes A comma separated list of MDC keys that should be included in the FlumeEvent. - * @param required A comma separated list of MDC keys that must be present in the MDC. - * @param exceptionPattern The pattern for formatting exceptions. - * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425. - * @param loggerFields Container for the KeyValuePairs containing the patterns - * @param config The Configuration. Some Converters require access to the Interpolator. - * @return An Rfc5424Layout. - */ - @PluginFactory - public static Rfc5424Layout createLayout( - @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility, - @PluginAttribute("id") final String id, - @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber, - @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC, - @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId, - @PluginAttribute("mdcPrefix") final String mdcPrefix, - @PluginAttribute("eventPrefix") final String eventPrefix, - @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine, - @PluginAttribute("newLineEscape") final String escapeNL, - @PluginAttribute("appName") final String appName, - @PluginAttribute("messageId") final String msgId, - @PluginAttribute("mdcExcludes") final String excludes, - @PluginAttribute("mdcIncludes") String includes, - @PluginAttribute("mdcRequired") final String required, - @PluginAttribute("exceptionPattern") final String exceptionPattern, - @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat, // RFC 5425 - @PluginElement("LoggerFields") final LoggerFields[] loggerFields, - @PluginConfiguration final Configuration config) { - final Charset charset = Charsets.UTF_8; - if (includes != null && excludes != null) { - LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored"); - includes = null; - } - - return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix, - eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern, - useTlsMessageFormat, loggerFields); - } - - private class FieldFormatter { - - private final Map<String, List<PatternFormatter>> delegateMap; - private final boolean discardIfEmpty; - - public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) { - this.discardIfEmpty = discardIfEmpty; - this.delegateMap = fieldMap; - } - - public StructuredDataElement format(final LogEvent event) { - final Map<String, String> map = new HashMap<String, String>(); - - for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) { - final StringBuilder buffer = new StringBuilder(); - for (final PatternFormatter formatter : entry.getValue()) { - formatter.format(event, buffer); - } - map.put(entry.getKey(), buffer.toString()); - } - return new StructuredDataElement(map, discardIfEmpty); - } - } - - private class StructuredDataElement { - - private final Map<String, String> fields; - private final boolean discardIfEmpty; - - public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) { - this.discardIfEmpty = discardIfEmpty; - this.fields = fields; - } - - boolean discard() { - if (discardIfEmpty == false) { - return false; - } - boolean foundNotEmptyValue = false; - for (final Map.Entry<String, String> entry : fields.entrySet()) { - if (Strings.isNotEmpty(entry.getValue())) { - foundNotEmptyValue = true; - break; - } - } - return !foundNotEmptyValue; - } - - void union(final Map<String, String> fields) { - this.fields.putAll(fields); - } - - Map<String, String> getFields() { - return this.fields; - } - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +package org.apache.logging.log4j.core.layout; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LoggingException; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.TlsSyslogFrame; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.net.Facility; +import org.apache.logging.log4j.core.net.Priority; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; +import org.apache.logging.log4j.core.pattern.PatternConverter; +import org.apache.logging.log4j.core.pattern.PatternFormatter; +import org.apache.logging.log4j.core.pattern.PatternParser; +import org.apache.logging.log4j.core.pattern.TextBuffer; +import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter; +import org.apache.logging.log4j.core.util.Charsets; +import org.apache.logging.log4j.core.util.NetUtils; +import org.apache.logging.log4j.core.util.Patterns; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.StructuredDataId; +import org.apache.logging.log4j.message.StructuredDataMessage; +import org.apache.logging.log4j.util.Strings; + + +/** + * Formats a log event in accordance with RFC 5424. + * + * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a> + */ +@Plugin(name = "Rfc5424Layout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public final class Rfc5424Layout extends AbstractStringLayout { + + private static final long serialVersionUID = 1L; + + private static final String LF = "\n"; + + /** + * Not a very good default - it is the Apache Software Foundation's enterprise number. + */ + public static final int DEFAULT_ENTERPRISE_NUMBER = 18060; + /** + * The default event id. + */ + public static final String DEFAULT_ID = "Audit"; + /** + * Match newlines in a platform-independent manner. + */ + public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n"); + /** + * Match characters which require escaping + */ + public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]"); + + public static final String DEFAULT_MDCID = "mdc"; + private static final int TWO_DIGITS = 10; + private static final int THREE_DIGITS = 100; + private static final int MILLIS_PER_MINUTE = 60000; + private static final int MINUTES_PER_HOUR = 60; + + private static final String COMPONENT_KEY = "RFC5424-Converter"; + + private final Facility facility; + private final String defaultId; + private final int enterpriseNumber; + private final boolean includeMdc; + private final String mdcId; + private final StructuredDataId mdcSdId; + private final String localHostName; + private final String appName; + private final String messageId; + private final String configName; + private final String mdcPrefix; + private final String eventPrefix; + private final List<String> mdcExcludes; + private final List<String> mdcIncludes; + private final List<String> mdcRequired; + private final ListChecker checker; + private final ListChecker noopChecker = new NoopChecker(); + private final boolean includeNewLine; + private final String escapeNewLine; + private final boolean useTlsMessageFormat; + + private long lastTimestamp = -1; + private String timestamppStr; + + private final List<PatternFormatter> exceptionFormatters; + private final Map<String, FieldFormatter> fieldFormatters; + + private Rfc5424Layout(final Configuration config, final Facility facility, final String id, final int ein, + final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId, + final String mdcPrefix, final String eventPrefix, + final String appName, final String messageId, final String excludes, final String includes, + final String required, final Charset charset, final String exceptionPattern, + final boolean useTLSMessageFormat, final LoggerFields[] loggerFields) { + super(charset); + final PatternParser exceptionParser = createPatternParser(config, ThrowablePatternConverter.class); + exceptionFormatters = exceptionPattern == null ? null : exceptionParser.parse(exceptionPattern, false, false); + this.facility = facility; + this.defaultId = id == null ? DEFAULT_ID : id; + this.enterpriseNumber = ein; + this.includeMdc = includeMDC; + this.includeNewLine = includeNL; + this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL); + this.mdcId = mdcId; + this.mdcSdId = new StructuredDataId(mdcId, enterpriseNumber, null, null); + this.mdcPrefix = mdcPrefix; + this.eventPrefix = eventPrefix; + this.appName = appName; + this.messageId = messageId; + this.useTlsMessageFormat = useTLSMessageFormat; + this.localHostName = NetUtils.getLocalHostname(); + ListChecker c = null; + if (excludes != null) { + final String[] array = excludes.split(Patterns.COMMA_SEPARATOR); + if (array.length > 0) { + c = new ExcludeChecker(); + mdcExcludes = new ArrayList<String>(array.length); + for (final String str : array) { + mdcExcludes.add(str.trim()); + } + } else { + mdcExcludes = null; + } + } else { + mdcExcludes = null; + } + if (includes != null) { + final String[] array = includes.split(Patterns.COMMA_SEPARATOR); + if (array.length > 0) { + c = new IncludeChecker(); + mdcIncludes = new ArrayList<String>(array.length); + for (final String str : array) { + mdcIncludes.add(str.trim()); + } + } else { + mdcIncludes = null; + } + } else { + mdcIncludes = null; + } + if (required != null) { + final String[] array = required.split(Patterns.COMMA_SEPARATOR); + if (array.length > 0) { + mdcRequired = new ArrayList<String>(array.length); + for (final String str : array) { + mdcRequired.add(str.trim()); + } + } else { + mdcRequired = null; + } + + } else { + mdcRequired = null; + } + this.checker = c != null ? c : noopChecker; + final String name = config == null ? null : config.getName(); + configName = name != null && name.length() > 0 ? name : null; + this.fieldFormatters = createFieldFormatters(loggerFields, config); + } + + private Map<String, FieldFormatter> createFieldFormatters(final LoggerFields[] loggerFields, + final Configuration config) { + final Map<String, FieldFormatter> sdIdMap = new HashMap<String, FieldFormatter>(); + + if (loggerFields != null) { + for (final LoggerFields lField : loggerFields) { + final StructuredDataId key = lField.getSdId() == null ? mdcSdId : lField.getSdId(); + final Map<String, List<PatternFormatter>> sdParams = new HashMap<String, List<PatternFormatter>>(); + final Map<String, String> fields = lField.getMap(); + if (!fields.isEmpty()) { + final PatternParser fieldParser = createPatternParser(config, null); + + for (final Map.Entry<String, String> entry : fields.entrySet()) { + final List<PatternFormatter> formatters = fieldParser.parse(entry.getValue(), false, false); + sdParams.put(entry.getKey(), formatters); + } + final FieldFormatter fieldFormatter = new FieldFormatter(sdParams, + lField.getDiscardIfAllFieldsAreEmpty()); + sdIdMap.put(key.toString(), fieldFormatter); + } + } + } + return sdIdMap.size() > 0 ? sdIdMap : null; + } + + /** + * Create a PatternParser. + * + * @param config The Configuration. + * @param filterClass Filter the returned plugins after calling the plugin manager. + * @return The PatternParser. + */ + private static PatternParser createPatternParser(final Configuration config, + final Class<? extends PatternConverter> filterClass) { + if (config == null) { + return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class, filterClass); + } + PatternParser parser = config.getComponent(COMPONENT_KEY); + if (parser == null) { + parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class); + config.addComponent(COMPONENT_KEY, parser); + parser = (PatternParser) config.getComponent(COMPONENT_KEY); + } + return parser; + } + + /** + * Gets this Rfc5424Layout's content format. Specified by: + * <ul> + * <li>Key: "structured" Value: "true"</li> + * <li>Key: "format" Value: "RFC5424"</li> + * </ul> + * + * @return Map of content format keys supporting Rfc5424Layout + */ + @Override + public Map<String, String> getContentFormat() { + final Map<String, String> result = new HashMap<String, String>(); + result.put("structured", "true"); + result.put("formatType", "RFC5424"); + return result; + } + + /** + * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification. + * + * @param event The LogEvent. + * @return The RFC 5424 String representation of the LogEvent. + */ + @Override + public String toSerializable(final LogEvent event) { + final StringBuilder buf = new StringBuilder(); + appendPriority(buf, event.getLevel()); + appendTimestamp(buf, event.getTimeMillis()); + appendSpace(buf); + appendHostName(buf); + appendSpace(buf); + appendAppName(buf); + appendSpace(buf); + appendProcessId(buf); + appendSpace(buf); + appendMessageId(buf, event.getMessage()); + appendSpace(buf); + appendStructuredElements(buf, event); + appendMessage(buf, event); + if (useTlsMessageFormat) { + return new TlsSyslogFrame(buf.toString()).toString(); + } + return buf.toString(); + } + + private void appendPriority(final StringBuilder buffer, final Level logLevel) { + buffer.append('<'); + buffer.append(Priority.getPriority(facility, logLevel)); + buffer.append(">1 "); + } + + private void appendTimestamp(final StringBuilder buffer, final long milliseconds) { + buffer.append(computeTimeStampString(milliseconds)); + } + + private void appendSpace(final StringBuilder buffer) { + buffer.append(' '); + } + + private void appendHostName(final StringBuilder buffer) { + buffer.append(localHostName); + } + + private void appendAppName(final StringBuilder buffer) { + if (appName != null) { + buffer.append(appName); + } else if (configName != null) { + buffer.append(configName); + } else { + buffer.append('-'); + } + } + + private void appendProcessId(final StringBuilder buffer) { + buffer.append(getProcId()); + } + + private void appendMessageId(final StringBuilder buffer, final Message message) { + final boolean isStructured = message instanceof StructuredDataMessage; + final String type = isStructured ? ((StructuredDataMessage) message).getType() : null; + if (type != null) { + buffer.append(type); + } else if (messageId != null) { + buffer.append(messageId); + } else { + buffer.append('-'); + } + } + + private void appendMessage(final StringBuilder buffer, final LogEvent event) { + final Message message = event.getMessage(); + // This layout formats StructuredDataMessages instead of delegating to the Message itself. + final String text = (message instanceof StructuredDataMessage) ? message.getFormat() : message.getFormattedMessage(); + + if (text != null && text.length() > 0) { + buffer.append(' ').append(escapeNewlines(text, escapeNewLine)); + } + + if (exceptionFormatters != null && event.getThrown() != null) { + final TextBuffer exception = new TextBuffer(); + exception.append(LF); + for (final PatternFormatter formatter : exceptionFormatters) { + formatter.format(event, exception); + } + buffer.append(escapeNewlines(exception.toString(), escapeNewLine)); + } + if (includeNewLine) { + buffer.append(LF); + } + } + + private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) { + final Message message = event.getMessage(); + final boolean isStructured = message instanceof StructuredDataMessage; + + if (!isStructured && (fieldFormatters!= null && fieldFormatters.isEmpty()) && !includeMdc) { + buffer.append('-'); + return; + } + + final Map<String, StructuredDataElement> sdElements = new HashMap<String, StructuredDataElement>(); + final Map<String, String> contextMap = event.getContextMap(); + + if (mdcRequired != null) { + checkRequired(contextMap); + } + + if (fieldFormatters != null) { + for (final Map.Entry<String, FieldFormatter> sdElement: fieldFormatters.entrySet()) { + final String sdId = sdElement.getKey(); + final StructuredDataElement elem = sdElement.getValue().format(event); + sdElements.put(sdId, elem); + } + } + + if (includeMdc && contextMap.size() > 0) { + if (sdElements.containsKey(mdcSdId.toString())) { + final StructuredDataElement union = sdElements.get(mdcSdId.toString()); + union.union(contextMap); + sdElements.put(mdcSdId.toString(), union); + } else { + final StructuredDataElement formattedContextMap = new StructuredDataElement(contextMap, false); + sdElements.put(mdcSdId.toString(), formattedContextMap); + } + } + + if (isStructured) { + final StructuredDataMessage data = (StructuredDataMessage) message; + final Map<String, String> map = data.getData(); + final StructuredDataId id = data.getId(); + final String sdId = getId(id); + + if (sdElements.containsKey(sdId)) { + final StructuredDataElement union = sdElements.get(id.toString()); + union.union(map); + sdElements.put(sdId, union); + } else { + final StructuredDataElement formattedData = new StructuredDataElement(map, false); + sdElements.put(sdId, formattedData); + } + } + + if (sdElements.isEmpty()) { + buffer.append('-'); + return; + } + + for (final Map.Entry<String, StructuredDataElement> entry: sdElements.entrySet()) { + formatStructuredElement(entry.getKey(), mdcPrefix, entry.getValue(), buffer, checker); + } + } + + private String escapeNewlines(final String text, final String escapeNewLine) { + if (null == escapeNewLine) { + return text; + } + return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine); + } + + protected String getProcId() { + return "-"; + } + + protected List<String> getMdcExcludes() { + return mdcExcludes; + } + + protected List<String> getMdcIncludes() { + return mdcIncludes; + } + + private String computeTimeStampString(final long now) { + long last; + synchronized (this) { + last = lastTimestamp; + if (now == lastTimestamp) { + return timestamppStr; + } + } + + final StringBuilder buffer = new StringBuilder(); + final Calendar cal = new GregorianCalendar(); + cal.setTimeInMillis(now); + buffer.append(Integer.toString(cal.get(Calendar.YEAR))); + buffer.append('-'); + pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buffer); + buffer.append('-'); + pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buffer); + buffer.append('T'); + pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buffer); + buffer.append(':'); + pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buffer); + buffer.append(':'); + pad(cal.get(Calendar.SECOND), TWO_DIGITS, buffer); + + final int millis = cal.get(Calendar.MILLISECOND); + if (millis != 0) { + buffer.append('.'); + pad(millis, THREE_DIGITS, buffer); + } + + int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE; + if (tzmin == 0) { + buffer.append('Z'); + } else { + if (tzmin < 0) { + tzmin = -tzmin; + buffer.append('-'); + } else { + buffer.append('+'); + } + final int tzhour = tzmin / MINUTES_PER_HOUR; + tzmin -= tzhour * MINUTES_PER_HOUR; + pad(tzhour, TWO_DIGITS, buffer); + buffer.append(':'); + pad(tzmin, TWO_DIGITS, buffer); + } + synchronized (this) { + if (last == lastTimestamp) { + lastTimestamp = now; + timestamppStr = buffer.toString(); + } + } + return buffer.toString(); + } + + private void pad(final int val, int max, final StringBuilder buf) { + while (max > 1) { + if (val < max) { + buf.append('0'); + } + max = max / TWO_DIGITS; + } + buf.append(Integer.toString(val)); + } + + private void formatStructuredElement(final String id, final String prefix, final StructuredDataElement data, + final StringBuilder sb, final ListChecker checker) { + if ((id == null && defaultId == null) || data.discard()) { + return; + } + + sb.append('['); + sb.append(id); + if (!mdcSdId.toString().equals(id)) { + appendMap(prefix, data.getFields(), sb, noopChecker); + } else { + appendMap(prefix, data.getFields(), sb, checker); + } + sb.append(']'); + } + + private String getId(final StructuredDataId id) { + final StringBuilder sb = new StringBuilder(); + if (id == null || id.getName() == null) { + sb.append(defaultId); + } else { + sb.append(id.getName()); + } + int ein = id != null ? id.getEnterpriseNumber() : enterpriseNumber; + if (ein < 0) { + ein = enterpriseNumber; + } + if (ein >= 0) { + sb.append('@').append(ein); + } + return sb.toString(); + } + + private void checkRequired(final Map<String, String> map) { + for (final String key : mdcRequired) { + final String value = map.get(key); + if (value == null) { + throw new LoggingException("Required key " + key + " is missing from the " + mdcId); + } + } + } + + private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb, + final ListChecker checker) { + final SortedMap<String, String> sorted = new TreeMap<String, String>(map); + for (final Map.Entry<String, String> entry : sorted.entrySet()) { + if (checker.check(entry.getKey()) && entry.getValue() != null) { + sb.append(' '); + if (prefix != null) { + sb.append(prefix); + } + sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"") + .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append('"'); + } + } + } + + private String escapeSDParams(final String value) { + return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0"); + } + + /** + * Interface used to check keys in a Map. + */ + private interface ListChecker { + boolean check(String key); + } + + /** + * Includes only the listed keys. + */ + private class IncludeChecker implements ListChecker { + @Override + public boolean check(final String key) { + return mdcIncludes.contains(key); + } + } + + /** + * Excludes the listed keys. + */ + private class ExcludeChecker implements ListChecker { + @Override + public boolean check(final String key) { + return !mdcExcludes.contains(key); + } + } + + /** + * Does nothing. + */ + private class NoopChecker implements ListChecker { + @Override + public boolean check(final String key) { + return true; + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("facility=").append(facility.name()); + sb.append(" appName=").append(appName); + sb.append(" defaultId=").append(defaultId); + sb.append(" enterpriseNumber=").append(enterpriseNumber); + sb.append(" newLine=").append(includeNewLine); + sb.append(" includeMDC=").append(includeMdc); + sb.append(" messageId=").append(messageId); + return sb.toString(); + } + + /** + * Create the RFC 5424 Layout. + * + * @param facility The Facility is used to try to classify the message. + * @param id The default structured data id to use when formatting according to RFC 5424. + * @param enterpriseNumber The IANA enterprise number. + * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog + * record. Defaults to "true:. + * @param mdcId The id to use for the MDC Structured Data Element. + * @param mdcPrefix The prefix to add to MDC key names. + * @param eventPrefix The prefix to add to event key names. + * @param newLine If true, a newline will be appended to the end of the syslog record. The default is false. + * @param escapeNL String that should be used to replace newlines within the message text. + * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record. + * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records. + * @param excludes A comma separated list of MDC keys that should be excluded from the LogEvent. + * @param includes A comma separated list of MDC keys that should be included in the FlumeEvent. + * @param required A comma separated list of MDC keys that must be present in the MDC. + * @param exceptionPattern The pattern for formatting exceptions. + * @param useTlsMessageFormat If true the message will be formatted according to RFC 5425. + * @param loggerFields Container for the KeyValuePairs containing the patterns + * @param config The Configuration. Some Converters require access to the Interpolator. + * @return An Rfc5424Layout. + */ + @PluginFactory + public static Rfc5424Layout createLayout( + @PluginAttribute(value = "facility", defaultString = "LOCAL0") final Facility facility, + @PluginAttribute("id") final String id, + @PluginAttribute(value = "enterpriseNumber", defaultInt = DEFAULT_ENTERPRISE_NUMBER) final int enterpriseNumber, + @PluginAttribute(value = "includeMDC", defaultBoolean = true) final boolean includeMDC, + @PluginAttribute(value = "mdcId", defaultString = DEFAULT_MDCID) final String mdcId, + @PluginAttribute("mdcPrefix") final String mdcPrefix, + @PluginAttribute("eventPrefix") final String eventPrefix, + @PluginAttribute(value = "newLine", defaultBoolean = false) final boolean newLine, + @PluginAttribute("newLineEscape") final String escapeNL, + @PluginAttribute("appName") final String appName, + @PluginAttribute("messageId") final String msgId, + @PluginAttribute("mdcExcludes") final String excludes, + @PluginAttribute("mdcIncludes") String includes, + @PluginAttribute("mdcRequired") final String required, + @PluginAttribute("exceptionPattern") final String exceptionPattern, + @PluginAttribute(value = "useTlsMessageFormat", defaultBoolean = false) final boolean useTlsMessageFormat, // RFC 5425 + @PluginElement("LoggerFields") final LoggerFields[] loggerFields, + @PluginConfiguration final Configuration config) { + final Charset charset = Charsets.UTF_8; + if (includes != null && excludes != null) { + LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored"); + includes = null; + } + + return new Rfc5424Layout(config, facility, id, enterpriseNumber, includeMDC, newLine, escapeNL, mdcId, mdcPrefix, + eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern, + useTlsMessageFormat, loggerFields); + } + + private class FieldFormatter { + + private final Map<String, List<PatternFormatter>> delegateMap; + private final boolean discardIfEmpty; + + public FieldFormatter(final Map<String, List<PatternFormatter>> fieldMap, final boolean discardIfEmpty) { + this.discardIfEmpty = discardIfEmpty; + this.delegateMap = fieldMap; + } + + public StructuredDataElement format(final LogEvent event) { + final Map<String, String> map = new HashMap<String, String>(); + + for (final Map.Entry<String, List<PatternFormatter>> entry : delegateMap.entrySet()) { + final TextBuffer buffer = new TextBuffer(); + for (final PatternFormatter formatter : entry.getValue()) { + formatter.format(event, buffer); + } + map.put(entry.getKey(), buffer.toString()); + } + return new StructuredDataElement(map, discardIfEmpty); + } + } + + private class StructuredDataElement { + + private final Map<String, String> fields; + private final boolean discardIfEmpty; + + public StructuredDataElement(final Map<String, String> fields, final boolean discardIfEmpty) { + this.discardIfEmpty = discardIfEmpty; + this.fields = fields; + } + + boolean discard() { + if (discardIfEmpty == false) { + return false; + } + boolean foundNotEmptyValue = false; + for (final Map.Entry<String, String> entry : fields.entrySet()) { + if (Strings.isNotEmpty(entry.getValue())) { + foundNotEmptyValue = true; + break; + } + } + return !foundNotEmptyValue; + } + + void union(final Map<String, String> fields) { + this.fields.putAll(fields); + } + + Map<String, String> getFields() { + return this.fields; + } + } +}
