/* * 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.jackson.json.layout; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.nio.charset.Charset; 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.categories.Layouts; 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.async.RingBufferLogEvent; 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.impl.MutableLogEvent; import org.apache.logging.log4j.core.layout.LogEventFixtures; import org.apache.logging.log4j.core.lookup.JavaLookup; import org.apache.logging.log4j.core.time.internal.DummyNanoClock; import org.apache.logging.log4j.core.time.internal.SystemClock; import org.apache.logging.log4j.core.util.KeyValuePair; import org.apache.logging.log4j.jackson.AbstractJacksonLayout; import org.apache.logging.log4j.jackson.json.Log4jJsonObjectMapper; import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.ObjectMessage; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.message.ReusableMessageFactory; 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.SortedArrayStringMap; import org.apache.logging.log4j.util.Strings; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; /** * Tests the JsonLayout class. */ @Category(Layouts.Json.class) public class JsonLayoutTest { private static class TestClass { private int value; public int getValue() { return value; } public void setValue(final int value) { this.value = value; } } 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.getRootLogger(); 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 boolean contextMapAslist) { this.toPropertySeparator(compact); if (contextMapAslist) { // {"key":"KEY", "value":"VALUE"} final String expected = String.format("{\"key\":\"%s\",\"value\":\"%s\"}", key, value); assertTrue("Cannot find contextMapAslist " + expected + " in " + str, str.contains(expected)); } else { // "KEY":"VALUE" final String expected = String.format("\"%s\":\"%s\"", key, value); assertTrue("Cannot find contextMap " + expected + " in " + str, str.contains(expected)); } } private void checkProperty(final String key, final String value, final boolean compact, final String str) { final String propSep = this.toPropertySeparator(compact); // {"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 String propSep = this.toPropertySeparator(compact); assertTrue(str, str.contains(DQUOTE + name + DQUOTE + propSep)); } private void checkPropertyNameAbsent(final String name, final boolean compact, final String str) { final String propSep = this.toPropertySeparator(compact); assertFalse(str, str.contains(DQUOTE + name + DQUOTE + propSep)); } private String prepareJsonForObjectMessageAsJsonObjectTests(final int value, final boolean objectMessageAsJsonObject) { final TestClass testClass = new TestClass(); testClass.setValue(value); // @formatter:off final Log4jLogEvent expected = Log4jLogEvent.newBuilder() .setLoggerName("a.B") .setLoggerFqcn("f.q.c.n") .setLevel(Level.DEBUG) .setMessage(new ObjectMessage(testClass)) .setThreadName("threadName") .setTimeMillis(1).build(); // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setCompact(true) .setObjectMessageAsJsonObject(objectMessageAsJsonObject) .build(); // @formatter:off return layout.toSerializable(expected); } private String prepareJsonForStacktraceTests(final boolean stacktraceAsString) { final Log4jLogEvent expected = LogEventFixtures.createLogEvent(); // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setCompact(true) .setIncludeStacktrace(true) .setStacktraceAsString(stacktraceAsString) .build(); // @formatter:off return layout.toSerializable(expected); } @Test public void testAdditionalFields() throws Exception { final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setComplete(false) .setCompact(true) .setEventEol(false) .setIncludeStacktrace(false) .setAdditionalFields(new KeyValuePair[] { new KeyValuePair("KEY1", "VALUE1"), new KeyValuePair("KEY2", "${java:runtime}"), }) .setCharset(StandardCharsets.UTF_8) .setConfiguration(ctx.getConfiguration()) .build(); final String str = layout.toSerializable(LogEventFixtures.createLogEvent()); assertTrue(str, str.contains("\"KEY1\":\"VALUE1\"")); assertTrue(str, str.contains("\"KEY2\":\"" + new JavaLookup().getRuntime() + "\"")); } @Test public void testMutableLogEvent() throws Exception { final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setComplete(false) .setCompact(true) .setEventEol(false) .setIncludeStacktrace(false) .setAdditionalFields(new KeyValuePair[] { new KeyValuePair("KEY1", "VALUE1"), new KeyValuePair("KEY2", "${java:runtime}"), }) .setCharset(StandardCharsets.UTF_8) .setConfiguration(ctx.getConfiguration()) .build(); Log4jLogEvent logEvent = LogEventFixtures.createLogEvent(); final MutableLogEvent mutableEvent = new MutableLogEvent(); mutableEvent.initFrom(logEvent); final String strLogEvent = layout.toSerializable(logEvent); final String strMutableEvent = layout.toSerializable(mutableEvent); assertEquals(strMutableEvent, strLogEvent, strMutableEvent); } private void testAllFeatures(final boolean locationInfo, final boolean compact, final boolean eventEol, final String endOfLine, final boolean includeContext, final boolean contextMapAslist, final boolean includeStacktrace) throws Exception { final Log4jLogEvent expected = LogEventFixtures.createLogEvent(); // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(locationInfo) .setProperties(includeContext) .setPropertiesAsList(contextMapAslist) .setComplete(false) .setCompact(compact) .setEventEol(eventEol) .setEndOfLine(endOfLine) .setCharset(StandardCharsets.UTF_8) .setIncludeStacktrace(includeStacktrace) .build(); // @formatter:off final String str = layout.toSerializable(expected); this.toPropertySeparator(compact); if (endOfLine == null) { // Just check for \n since \r might or might not be there. assertEquals(str, !compact || eventEol, str.contains("\n")); } else { assertEquals(str, !compact || eventEol, str.contains(endOfLine)); assertEquals(str, compact && eventEol, str.endsWith(endOfLine)); } assertEquals(str, locationInfo, str.contains("source")); assertEquals(str, includeContext, str.contains("contextMap")); final Log4jLogEvent actual = new Log4jJsonObjectMapper(contextMapAslist, includeStacktrace, false, false).readValue(str, Log4jLogEvent.class); LogEventFixtures.assertEqualLogEvents(expected, actual, locationInfo, includeContext, includeStacktrace); if (includeContext) { this.checkMapEntry("MDC.A", "A_Value", compact, str, contextMapAslist); this.checkMapEntry("MDC.B", "B_Value", compact, str, contextMapAslist); } // assertNull(actual.getThrown()); // make sure the names we want are used this.checkPropertyName("instant", compact, str); this.checkPropertyName("thread", compact, str); // and not threadName this.checkPropertyName("level", compact, str); this.checkPropertyName("loggerName", compact, str); this.checkPropertyName("marker", compact, str); this.checkPropertyName("name", compact, str); this.checkPropertyName("parents", compact, str); this.checkPropertyName("message", compact, str); this.checkPropertyName("thrown", compact, str); this.checkPropertyName("cause", compact, str); this.checkPropertyName("commonElementCount", compact, str); this.checkPropertyName("localizedMessage", compact, str); if (includeStacktrace) { this.checkPropertyName("extendedStackTrace", compact, str); this.checkPropertyName("class", compact, str); this.checkPropertyName("method", compact, str); this.checkPropertyName("file", compact, str); this.checkPropertyName("line", compact, str); this.checkPropertyName("exact", compact, str); this.checkPropertyName("location", compact, str); this.checkPropertyName("version", compact, str); } else { this.checkPropertyNameAbsent("extendedStackTrace", compact, str); } this.checkPropertyName("suppressed", compact, str); this.checkPropertyName("loggerFqcn", compact, str); this.checkPropertyName("endOfBatch", compact, str); if (includeContext) { this.checkPropertyName("contextMap", compact, str); } else { this.checkPropertyNameAbsent("contextMap", compact, str); } this.checkPropertyName("contextStack", compact, str); if (locationInfo) { this.checkPropertyName("source", compact, str); } else { this.checkPropertyNameAbsent("source", compact, str); } // check some attrs this.checkProperty("loggerFqcn", "f.q.c.n", compact, str); this.checkProperty("loggerName", "a.B", compact, str); } @Test public void testContentType() { final AbstractJacksonLayout layout = JsonLayout.createDefaultLayout(); assertEquals("application/json; charset=UTF-8", layout.getContentType()); } @Test public void testDefaultCharset() { final AbstractJacksonLayout layout = JsonLayout.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 boolean propertiesAsList = false; // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setConfiguration(configuration) .setLocationInfo(true) .setProperties(true) .setPropertiesAsList(propertiesAsList) .setComplete(true) .setCompact(false) .setEventEol(false) .setIncludeStacktrace(true) .build(); // @formatter:on 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.checkAt("{", 1, 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 public void testExcludeStacktrace() throws Exception { this.testAllFeatures(false, false, false, null, false, false, false); } @Test public void testLocationOnCustomEndOfLine() throws Exception { this.testAllFeatures(true, true, true, "CUSTOM_END_OF_LINE", true, false, true); } @Test public void testIncludeNullDelimiterFalse() throws Exception { final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setCompact(true) .setIncludeNullDelimiter(false) .build(); final String str = layout.toSerializable(LogEventFixtures.createLogEvent()); assertFalse(str.endsWith("\0")); } @Test public void testIncludeNullDelimiterTrue() throws Exception { final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setCompact(true) .setIncludeNullDelimiter(true) .build(); final String str = layout.toSerializable(LogEventFixtures.createLogEvent()); assertTrue(str.endsWith("\0")); } /** * 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 boolean propertiesAsList = false; // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setConfiguration(configuration) .setLocationInfo(true) .setProperties(true) .setPropertiesAsList(propertiesAsList) .setComplete(true) .setCompact(false) .setEventEol(false) .setHeader("[[".getBytes(Charset.defaultCharset())) .setFooter("]]".getBytes(Charset.defaultCharset())) .setIncludeStacktrace(true) .build(); // @formatter:on 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.checkAt("{", 1, 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 boolean propertiesAsList = false; // @formatter:off final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setPropertiesAsList(propertiesAsList) .setComplete(false) .setCompact(true) .setEventEol(false) .setCharset(StandardCharsets.UTF_8) .setIncludeStacktrace(true) .build(); // @formatter:on // @formatter:off 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(); // @formatter:on final String str = layout.toSerializable(expected); assertTrue(str, str.contains("\"loggerName\":\"a.B\"")); final Log4jLogEvent actual = new Log4jJsonObjectMapper(propertiesAsList, true, false, false).readValue(str, Log4jLogEvent.class); assertEquals(expected.getLoggerName(), actual.getLoggerName()); assertEquals(expected, actual); } @Test public void testLayoutMessageWithCurlyBraces() throws Exception { final boolean propertiesAsList = false; final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setPropertiesAsList(propertiesAsList) .setComplete(false) .setCompact(true) .setEventEol(false) .setCharset(StandardCharsets.UTF_8) .setIncludeStacktrace(true) .build(); final Log4jLogEvent expected = Log4jLogEvent.newBuilder() .setLoggerName("a.B") .setLoggerFqcn("f.q.c.n") .setLevel(Level.DEBUG) .setMessage(new ParameterizedMessage("Testing {}", new TestObj())) .setThreadName("threadName") .setTimeMillis(1).build(); final String str = layout.toSerializable(expected); final String expectedMessage = "Testing " + TestObj.TO_STRING_VALUE; assertTrue(str, str.contains("\"message\":\"" + expectedMessage + '"')); final Log4jLogEvent actual = new Log4jJsonObjectMapper(propertiesAsList, true, false, false).readValue(str, Log4jLogEvent.class); assertEquals(expectedMessage, actual.getMessage().getFormattedMessage()); } // Test for LOG4J2-2345 @Test public void testReusableLayoutMessageWithCurlyBraces() throws Exception { final boolean propertiesAsList = false; final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setPropertiesAsList(propertiesAsList) .setComplete(false) .setCompact(true) .setEventEol(false) .setCharset(StandardCharsets.UTF_8) .setIncludeStacktrace(true) .build(); Message message = ReusableMessageFactory.INSTANCE.newMessage("Testing {}", new TestObj()); try { final Log4jLogEvent expected = Log4jLogEvent.newBuilder() .setLoggerName("a.B") .setLoggerFqcn("f.q.c.n") .setLevel(Level.DEBUG) .setMessage(message) .setThreadName("threadName") .setTimeMillis(1).build(); MutableLogEvent mutableLogEvent = new MutableLogEvent(); mutableLogEvent.initFrom(expected); final String str = layout.toSerializable(mutableLogEvent); final String expectedMessage = "Testing " + TestObj.TO_STRING_VALUE; assertTrue(str, str.contains("\"message\":\"" + expectedMessage + '"')); final Log4jLogEvent actual = new Log4jJsonObjectMapper(propertiesAsList, true, false, false).readValue(str, Log4jLogEvent.class); assertEquals(expectedMessage, actual.getMessage().getFormattedMessage()); } finally { ReusableMessageFactory.release(message); } } // Test for LOG4J2-2312 LOG4J2-2341 @Test public void testLayoutRingBufferEventReusableMessageWithCurlyBraces() throws Exception { final boolean propertiesAsList = false; final AbstractJacksonLayout layout = JsonLayout.newBuilder() .setLocationInfo(false) .setProperties(false) .setPropertiesAsList(propertiesAsList) .setComplete(false) .setCompact(true) .setEventEol(false) .setCharset(StandardCharsets.UTF_8) .setIncludeStacktrace(true) .build(); Message message = ReusableMessageFactory.INSTANCE.newMessage("Testing {}", new TestObj()); try { RingBufferLogEvent ringBufferEvent = new RingBufferLogEvent(); ringBufferEvent.setValues( null, "a.B", null, "f.q.c.n", Level.DEBUG, message, null, new SortedArrayStringMap(), ThreadContext.EMPTY_STACK, 1L, "threadName", 1, null, new SystemClock(), new DummyNanoClock()); final String str = layout.toSerializable(ringBufferEvent); final String expectedMessage = "Testing " + TestObj.TO_STRING_VALUE; assertThat(str, containsString("\"message\":\"" + expectedMessage + '"')); final Log4jLogEvent actual = new Log4jJsonObjectMapper(propertiesAsList, true, false, false).readValue(str, Log4jLogEvent.class); assertEquals(expectedMessage, actual.getMessage().getFormattedMessage()); } finally { ReusableMessageFactory.release(message); } } static class TestObj { static final String TO_STRING_VALUE = "This is my toString {} with curly braces"; @Override public String toString() { return TO_STRING_VALUE; } } @Test public void testLocationOffCompactOffMdcOff() throws Exception { this.testAllFeatures(false, false, false, null, false, false, true); } @Test public void testLocationOnCompactOnEventEolOnMdcOn() throws Exception { this.testAllFeatures(true, true, true, null, true, false, true); } @Test public void testLocationOnCompactOnEventEolOnMdcOnMdcAsList() throws Exception { this.testAllFeatures(true, true, true, null, true, true, true); } @Test public void testLocationOnCompactOnMdcOn() throws Exception { this.testAllFeatures(true, true, false, null, true, false, true); } @Test public void testObjectMessageAsJsonObject() { final String str = prepareJsonForObjectMessageAsJsonObjectTests(1234, true); assertTrue(str, str.contains("\"message\":{\"value\":1234}")); } @Test public void testObjectMessageAsJsonString() { final String str = prepareJsonForObjectMessageAsJsonObjectTests(1234, false); assertTrue(str, str.contains("\"message\":\"" + this.getClass().getCanonicalName() + "$TestClass@")); } @Test public void testStacktraceAsNonString() throws Exception { final String str = prepareJsonForStacktraceTests(false); assertTrue(str, str.contains("\"extendedStackTrace\":[")); } @Test public void testStacktraceAsString() throws Exception { final String str = prepareJsonForStacktraceTests(true); assertTrue(str, str.contains("\"extendedStackTrace\":\"java.lang.NullPointerException")); } private String toPropertySeparator(final boolean compact) { return compact ? ":" : " : "; } }