[LOG4J2-1362] Create a YAML layout. Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/80a43988 Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/80a43988 Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/80a43988
Branch: refs/heads/LOG4J2-1365 Commit: 80a43988a017a8e95f6188386cd5fe7ddf3945bd Parents: 71fbda8 Author: ggregory <ggreg...@apache.org> Authored: Sun Apr 17 15:27:19 2016 -0700 Committer: ggregory <ggreg...@apache.org> Committed: Sun Apr 17 15:27:19 2016 -0700 ---------------------------------------------------------------------- .../org/apache/logging/log4j/util/Strings.java | 12 + .../log4j/core/jackson/Log4jYamlModule.java | 48 ++++ .../core/jackson/Log4jYamlObjectMapper.java | 41 +++ .../log4j/core/layout/JacksonFactory.java | 34 +++ .../logging/log4j/core/layout/YamlLayout.java | 193 +++++++++++++ .../logging/log4j/MarkerMixInYamlTest.java | 31 ++ .../log4j/core/jackson/LevelMixInYamlTest.java | 29 ++ .../jackson/StackTraceElementMixInTest.java | 5 + .../log4j/core/layout/YamlLayoutTest.java | 287 +++++++++++++++++++ .../log4j/web/ServletRequestThreadContext.java | 29 ++ 10 files changed, 709 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java index 6ee7dca..294b771 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java @@ -16,6 +16,8 @@ */ package org.apache.logging.log4j.util; +import java.util.Locale; + /** * <em>Consider this class private.</em> * @@ -127,6 +129,16 @@ public final class Strings { } /** + * Shorthand for {@code str.toUpperCase(Locale.ROOT);} + * @param str The string to upper case. + * @return a new string + * @see String#toLowerCase(Locale) + */ + public String toRootUpperCase(final String str) { + return str.toUpperCase(Locale.ROOT); + } + + /** * <p> * Removes control characters (char <= 32) from both ends of this String returning {@code null} if the String is * empty ("") after the trim or if it is {@code null}. http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlModule.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlModule.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlModule.java new file mode 100644 index 0000000..4052320 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlModule.java @@ -0,0 +1,48 @@ +/* + * 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.jackson; + +import org.apache.logging.log4j.core.jackson.Initializers.SetupContextInitializer; +import org.apache.logging.log4j.core.jackson.Initializers.SimpleModuleInitializer; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * <p> + * <em>Consider this class private.</em> + * </p> + */ +final class Log4jYamlModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + Log4jYamlModule() { + super(Log4jYamlModule.class.getName(), new Version(2, 0, 0, null, null, null)); + // MUST init here. + // Calling this from setupModule is too late! + //noinspection ThisEscapedInObjectConstruction + new SimpleModuleInitializer().initialize(this); + } + + @Override + public void setupModule(final SetupContext context) { + // Calling super is a MUST! + super.setupModule(context); + new SetupContextInitializer().setupModule(context); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlObjectMapper.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlObjectMapper.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlObjectMapper.java new file mode 100644 index 0000000..9ab787a --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/Log4jYamlObjectMapper.java @@ -0,0 +1,41 @@ +/* + * 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.jackson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; + +/** + * A Jackson {@link ObjectMapper} initialized for Log4j. + * <p> + * <em>Consider this class private.</em> + * </p> + */ +public class Log4jYamlObjectMapper extends YAMLMapper { + + private static final long serialVersionUID = 1L; + + /** + * Create a new instance using the {@link Log4jYamlModule}. + */ + public Log4jYamlObjectMapper() { + this.registerModule(new Log4jYamlModule()); + this.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JacksonFactory.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JacksonFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JacksonFactory.java index 537b634..44731f4 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JacksonFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JacksonFactory.java @@ -23,6 +23,7 @@ import org.apache.logging.log4j.core.impl.Log4jLogEvent; import org.apache.logging.log4j.core.jackson.JsonConstants; import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper; import org.apache.logging.log4j.core.jackson.Log4jXmlObjectMapper; +import org.apache.logging.log4j.core.jackson.Log4jYamlObjectMapper; import org.apache.logging.log4j.core.jackson.XmlConstants; import com.fasterxml.jackson.core.PrettyPrinter; @@ -103,6 +104,39 @@ abstract class JacksonFactory { } } + static class YAML extends JacksonFactory { + + @Override + protected String getPropertNameForContextMap() { + return JsonConstants.ELT_CONTEXT_MAP; + } + + @Override + protected String getPropertNameForSource() { + return JsonConstants.ELT_SOURCE; + } + + @Override + protected String getPropertNameForNanoTime() { + return JsonConstants.ELT_NANO_TIME; + } + + @Override + protected PrettyPrinter newCompactPrinter() { + return new MinimalPrettyPrinter(); + } + + @Override + protected ObjectMapper newObjectMapper() { + return new Log4jYamlObjectMapper(); + } + + @Override + protected PrettyPrinter newPrettyPrinter() { + return new DefaultPrettyPrinter(); + } + } + abstract protected String getPropertNameForContextMap(); abstract protected String getPropertNameForSource(); http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/YamlLayout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/YamlLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/YamlLayout.java new file mode 100644 index 0000000..0426fea --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/YamlLayout.java @@ -0,0 +1,193 @@ +/* + * 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.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.DefaultConfiguration; +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.PluginFactory; + +/** + * Appends a series of YAML events as strings serialized as bytes. + * + * <h3>Complete well-formed YAML vs. fragment YAML</h3> + * <p> + * If you configure {@code complete="true"}, the appender outputs a well-formed YAML document. By default, with + * {@code complete="false"}, you should include the output as an <em>external file</em> in a separate file to form a + * well-formed YAML document. + * </p> + * <p> + * A well-formed YAML event follows this pattern: + * </p> + * + * <pre> + * + * </pre> + * <p> + * If {@code complete="false"}, the appender does not write the YAML open array character "[" at the start of the + * document, "]" and the end, nor comma "," between records. + * </p> + * <p> + * This approach enforces the independence of the YamlLayout and the appender where you embed it. + * </p> + * <h3>Encoding</h3> + * <p> + * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise + * events containing non ASCII characters could result in corrupted log files. + * </p> + * <h3>Pretty vs. compact YAML</h3> + * <p> + * By default, the YAML layout is not compact (a.k.a. "pretty") with {@code compact="false"}, which means the appender + * uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no end-of-line or + * indentation is used. Message content may contain, of course, escaped end-of-lines. + * </p> + */ +@Plugin(name = "JsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public final class YamlLayout extends AbstractJacksonLayout { + + private static final String DEFAULT_FOOTER = ""; // TODO maybe + + private static final String DEFAULT_HEADER = ""; // TODO maybe + + static final String CONTENT_TYPE = "application/yaml"; + + protected YamlLayout(final Configuration config, final boolean locationInfo, final boolean properties, + final boolean complete, final boolean compact, final boolean eventEol, final String headerPattern, + final String footerPattern, final Charset charset) { + super(config, new JacksonFactory.YAML().newWriter(locationInfo, properties, compact), charset, compact, + complete, eventEol, + PatternLayout.createSerializer(config, null, headerPattern, DEFAULT_HEADER, null, false, false), + PatternLayout.createSerializer(config, null, footerPattern, DEFAULT_FOOTER, null, false, false)); + } + + /** + * Returns appropriate YAML header. + * + * @return a byte array containing the header, opening the YAML array. + */ + @Override + public byte[] getHeader() { + if (!this.complete) { + return null; + } + final StringBuilder buf = new StringBuilder(); + final String str = serializeToString(getHeaderSerializer()); + if (str != null) { + buf.append(str); + } + buf.append(this.eol); + return getBytes(buf.toString()); + } + + /** + * Returns appropriate YAML footer. + * + * @return a byte array containing the footer, closing the YAML array. + */ + @Override + public byte[] getFooter() { + if (!this.complete) { + return null; + } + final StringBuilder buf = new StringBuilder(); + buf.append(this.eol); + final String str = serializeToString(getFooterSerializer()); + if (str != null) { + buf.append(str); + } + buf.append(this.eol); + return getBytes(buf.toString()); + } + + @Override + public Map<String, String> getContentFormat() { + final Map<String, String> result = new HashMap<>(); + result.put("version", "2.0"); + return result; + } + + @Override + /** + * @return The content type. + */ + public String getContentType() { + return CONTENT_TYPE + "; charset=" + this.getCharset(); + } + + /** + * Creates a YAML Layout. + * + * @param config + * The plugin configuration. + * @param locationInfo + * If "true", includes the location information in the generated YAML. + * @param properties + * If "true", includes the thread context in the generated YAML. + * @param headerPattern + * The header pattern, defaults to {@code ""} if null. + * @param footerPattern + * The header pattern, defaults to {@code ""} if null. + * @param footerPattern + * @param charset + * The character set to use, if {@code null}, uses "UTF-8". + * @return A YAML Layout. + */ + @PluginFactory + public static AbstractJacksonLayout createLayout( + // @formatter:off + @PluginConfiguration final Configuration config, + @PluginAttribute(value = "locationInfo", defaultBoolean = false) final boolean locationInfo, + @PluginAttribute(value = "properties", defaultBoolean = false) final boolean properties, + @PluginAttribute(value = "header", defaultString = DEFAULT_HEADER) final String headerPattern, + @PluginAttribute(value = "footer", defaultString = DEFAULT_FOOTER) final String footerPattern, + @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset + // @formatter:on + ) { + return new YamlLayout(config, locationInfo, properties, false, false, true, headerPattern, footerPattern, + charset); + } + + /** + * Creates a YAML Layout using the default settings. Useful for testing. + * + * @return A YAML Layout. + */ + public static AbstractJacksonLayout createDefaultLayout() { + return new YamlLayout(new DefaultConfiguration(), false, false, false, false, false, DEFAULT_HEADER, + DEFAULT_FOOTER, StandardCharsets.UTF_8); + } + + @Override + public void toSerializable(final LogEvent event, final Writer writer) throws IOException { + if (complete && eventCount > 0) { + writer.append(", "); + } + super.toSerializable(event, writer); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/test/java/org/apache/logging/log4j/MarkerMixInYamlTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/MarkerMixInYamlTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/MarkerMixInYamlTest.java new file mode 100644 index 0000000..89f02a1 --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/MarkerMixInYamlTest.java @@ -0,0 +1,31 @@ +/* +* 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; + +import org.apache.logging.log4j.core.jackson.Log4jYamlObjectMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class MarkerMixInYamlTest extends MarkerMixInTest { + + @Override + protected ObjectMapper newObjectMapper() { + return new Log4jYamlObjectMapper(); + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/LevelMixInYamlTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/LevelMixInYamlTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/LevelMixInYamlTest.java new file mode 100644 index 0000000..cff034e --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/LevelMixInYamlTest.java @@ -0,0 +1,29 @@ +/* + * 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.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class LevelMixInYamlTest extends LevelMixInTest { + + @Override + protected ObjectMapper newObjectMapper() { + return new Log4jYamlObjectMapper(); + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/StackTraceElementMixInTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/StackTraceElementMixInTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/StackTraceElementMixInTest.java index ed9b3da..5f3383b 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/StackTraceElementMixInTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/jackson/StackTraceElementMixInTest.java @@ -34,6 +34,11 @@ public class StackTraceElementMixInTest { this.roundtrip(new Log4jJsonObjectMapper()); } + @Test + public void testLog4jYamlObjectMapper() throws Exception { + this.roundtrip(new Log4jYamlObjectMapper()); + } + /** * @param mapper * @throws JsonProcessingException http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/YamlLayoutTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/YamlLayoutTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/YamlLayoutTest.java new file mode 100644 index 0000000..fd30bbd --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/YamlLayoutTest.java @@ -0,0 +1,287 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.BasicConfigurationFactory; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.core.jackson.Log4jYamlObjectMapper; +import org.apache.logging.log4j.message.SimpleMessage; +import org.apache.logging.log4j.spi.AbstractLogger; +import org.apache.logging.log4j.test.appender.ListAppender; +import org.apache.logging.log4j.util.Strings; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests the YamlLayout class. + */ +public class YamlLayoutTest { + static ConfigurationFactory cf = new BasicConfigurationFactory(); + + private static final String DQUOTE = "\""; + + @AfterClass + public static void cleanupClass() { + ConfigurationFactory.removeConfigurationFactory(cf); + ThreadContext.clearAll(); + } + + @BeforeClass + public static void setupClass() { + ThreadContext.clearAll(); + ConfigurationFactory.setConfigurationFactory(cf); + final LoggerContext ctx = LoggerContext.getContext(); + ctx.reconfigure(); + } + + LoggerContext ctx = LoggerContext.getContext(); + + Logger rootLogger = this.ctx.getLogger(""); + + private void checkAt(final String expected, final int lineIndex, final List<String> list) { + final String trimedLine = list.get(lineIndex).trim(); + assertTrue("Incorrect line index " + lineIndex + ": " + Strings.dquote(trimedLine), + trimedLine.equals(expected)); + } + + private void checkContains(final String expected, final List<String> list) { + for (final String string : list) { + final String trimedLine = string.trim(); + if (trimedLine.equals(expected)) { + return; + } + } + Assert.fail("Cannot find " + expected + " in " + list); + } + + private void checkMapEntry(final String key, final String value, final boolean compact, final String str) { + final String propSep = this.toPropertySeparator(compact, true); + // "name":"value" + final String expected = String.format("- key: \"%s\"\n value: \"%s\"", key, value); + assertTrue("Cannot find " + expected + " in " + str, str.contains(expected)); + } + + private void checkProperty(final String key, final String value, final boolean compact, final String str, + final boolean isValue) { + final String propSep = this.toPropertySeparator(compact, isValue); + // {"key":"MDC.B","value":"B_Value"} + final String expected = String.format("%s%s\"%s\"", key, propSep, value); + assertTrue("Cannot find " + expected + " in " + str, str.contains(expected)); + } + + private void checkPropertyName(final String name, final boolean compact, final String str, final boolean isValue) { + final String propSep = this.toPropertySeparator(compact, isValue); + assertTrue(str, str.contains(name + propSep)); + } + + private void testAllFeatures(final boolean includeSource, final boolean compact, final boolean eventEol, + final boolean includeContext) throws Exception { + final Log4jLogEvent expected = LogEventFixtures.createLogEvent(); + final AbstractJacksonLayout layout = YamlLayout.createLayout(null, includeSource, includeContext, null, null, + StandardCharsets.UTF_8); + final String str = layout.toSerializable(expected); + // System.out.println(str); + final String propSep = this.toPropertySeparator(compact, true); + // Just check for \n since \r might or might not be there. + assertEquals(str, !compact || eventEol, str.contains("\n")); + assertEquals(str, includeSource, str.contains("source")); + assertEquals(str, includeContext, str.contains("contextMap")); + final Log4jLogEvent actual = new Log4jYamlObjectMapper().readValue(str, Log4jLogEvent.class); + LogEventFixtures.assertEqualLogEvents(expected, actual, includeSource, includeContext); + if (includeContext) { + this.checkMapEntry("MDC.A", "A_Value", compact, str); + this.checkMapEntry("MDC.B", "B_Value", compact, str); + } + // + assertNull(actual.getThrown()); + // make sure the names we want are used + this.checkPropertyName("timeMillis", compact, str, true); + this.checkPropertyName("thread", compact, str, true); // and not threadName + this.checkPropertyName("level", compact, str, true); + this.checkPropertyName("loggerName", compact, str, true); + this.checkPropertyName("marker", compact, str, false); + this.checkPropertyName("name", compact, str, true); + this.checkPropertyName("parents", compact, str, false); + this.checkPropertyName("message", compact, str, true); + this.checkPropertyName("thrown", compact, str, false); + this.checkPropertyName("cause", compact, str, false); + this.checkPropertyName("class", compact, str, true); + this.checkPropertyName("method", compact, str, true); + this.checkPropertyName("file", compact, str, true); + this.checkPropertyName("line", compact, str, true); + this.checkPropertyName("exact", compact, str, true); + this.checkPropertyName("location", compact, str, true); + this.checkPropertyName("version", compact, str, true); + this.checkPropertyName("commonElementCount", compact, str, true); + this.checkPropertyName("localizedMessage", compact, str, true); + this.checkPropertyName("extendedStackTrace", compact, str, false); + this.checkPropertyName("suppressed", compact, str, false); + this.checkPropertyName("loggerFqcn", compact, str, true); + this.checkPropertyName("endOfBatch", compact, str, true); + if (includeContext) { + this.checkPropertyName("contextMap", compact, str, false); + } + this.checkPropertyName("contextStack", compact, str, false); + if (includeSource) { + this.checkPropertyName("source", compact, str, false); + } + // check some attrs + this.checkProperty("loggerFqcn", "f.q.c.n", compact, str, true); + this.checkProperty("loggerName", "a.B", compact, str, true); + } + + @Test + public void testContentType() { + final AbstractJacksonLayout layout = YamlLayout.createDefaultLayout(); + assertEquals("application/yaml; charset=UTF-8", layout.getContentType()); + } + + @Test + public void testDefaultCharset() { + final AbstractJacksonLayout layout = YamlLayout.createDefaultLayout(); + assertEquals(StandardCharsets.UTF_8, layout.getCharset()); + } + + @Test + public void testEscapeLayout() throws Exception { + final Map<String, Appender> appenders = this.rootLogger.getAppenders(); + for (final Appender appender : appenders.values()) { + this.rootLogger.removeAppender(appender); + } + final Configuration configuration = rootLogger.getContext().getConfiguration(); + // set up appender + final AbstractJacksonLayout layout = YamlLayout.createLayout(configuration, true, true, null, null, null); + final ListAppender appender = new ListAppender("List", null, layout, true, false); + appender.start(); + + // set appender on root and set level to debug + this.rootLogger.addAppender(appender); + this.rootLogger.setLevel(Level.DEBUG); + + // output starting message + this.rootLogger.debug("Here is a quote ' and then a double quote \""); + + appender.stop(); + + final List<String> list = appender.getMessages(); + + this.checkAt("---", 0, list); + this.checkContains("level: \"DEBUG\"", list); + this.checkContains("message: \"Here is a quote ' and then a double quote \\\"\"", list); + this.checkContains("loggerFqcn: \"" + AbstractLogger.class.getName() + "\"", list); + for (final Appender app : appenders.values()) { + this.rootLogger.addAppender(app); + } + } + + /** + * Test case for MDC conversion pattern. + */ + @Test + public void testLayout() throws Exception { + final Map<String, Appender> appenders = this.rootLogger.getAppenders(); + for (final Appender appender : appenders.values()) { + this.rootLogger.removeAppender(appender); + } + final Configuration configuration = rootLogger.getContext().getConfiguration(); + // set up appender + // Use [[ and ]] to test header and footer (instead of [ and ]) + final AbstractJacksonLayout layout = YamlLayout.createLayout(configuration, true, true, "[[", "]]", null); + final ListAppender appender = new ListAppender("List", null, layout, true, false); + appender.start(); + + // set appender on root and set level to debug + this.rootLogger.addAppender(appender); + this.rootLogger.setLevel(Level.DEBUG); + + // output starting message + this.rootLogger.debug("starting mdc pattern test"); + + this.rootLogger.debug("empty mdc"); + + ThreadContext.put("key1", "value1"); + ThreadContext.put("key2", "value2"); + + this.rootLogger.debug("filled mdc"); + + ThreadContext.remove("key1"); + ThreadContext.remove("key2"); + + this.rootLogger.error("finished mdc pattern test", new NullPointerException("test")); + + appender.stop(); + + final List<String> list = appender.getMessages(); + + this.checkAt("---", 0, list); + this.checkContains("loggerFqcn: \"" + AbstractLogger.class.getName() + "\"", list); + this.checkContains("level: \"DEBUG\"", list); + this.checkContains("message: \"starting mdc pattern test\"", list); + for (final Appender app : appenders.values()) { + this.rootLogger.addAppender(app); + } + } + + @Test + public void testLayoutLoggerName() throws Exception { + final AbstractJacksonLayout layout = YamlLayout.createLayout(null, false, false, null, null, + StandardCharsets.UTF_8); + final Log4jLogEvent expected = Log4jLogEvent.newBuilder() // + .setLoggerName("a.B") // + .setLoggerFqcn("f.q.c.n") // + .setLevel(Level.DEBUG) // + .setMessage(new SimpleMessage("M")) // + .setThreadName("threadName") // + .setTimeMillis(1).build(); + final String str = layout.toSerializable(expected); + assertTrue(str, str.contains("loggerName: \"a.B\"")); + final Log4jLogEvent actual = new Log4jYamlObjectMapper().readValue(str, Log4jLogEvent.class); + assertEquals(expected.getLoggerName(), actual.getLoggerName()); + assertEquals(expected, actual); + } + + @Test + public void testLocationOffCompactOffMdcOff() throws Exception { + this.testAllFeatures(false, false, false, false); + } + + @Test + public void testLocationOnCompactOffEventEolOffMdcOn() throws Exception { + this.testAllFeatures(true, false, false, true); + } + + private String toPropertySeparator(final boolean compact, final boolean value) { + return value ? ": " : ":"; + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/80a43988/log4j-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java ---------------------------------------------------------------------- diff --git a/log4j-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java b/log4j-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java new file mode 100644 index 0000000..6615531 --- /dev/null +++ b/log4j-web/src/main/java/org/apache/logging/log4j/web/ServletRequestThreadContext.java @@ -0,0 +1,29 @@ +package org.apache.logging.log4j.web; + +import java.util.Objects; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.apache.logging.log4j.ThreadContext; + +public class ServletRequestThreadContext { + + public static void put(String key, ServletRequest servletRequest) { + put(key, "RemoteAddr", servletRequest.getRemoteAddr()); + put(key, "RemoteHost", servletRequest.getRemoteHost()); + put(key, "RemotePort", servletRequest.getRemotePort()); + } + + public static void put(String key, String field, Object value) { + put(key + "." + field, Objects.toString(value)); + } + + public static void put(String key, String value) { + ThreadContext.put(key, value); + } + + public static void put(String key, HttpServletRequest servletRequest) { + put(key, (ServletRequest) servletRequest); + } +}