diff --git a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java index 46c802ca..1f56073c 100644 --- a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java +++ b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java @@ -26,8 +26,11 @@ import java.io.PrintWriter; import java.io.Writer; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -38,6 +41,18 @@ public class EcsJsonSerializer { private static final String NEW_LINE = System.getProperty("line.separator"); private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\\r\\n|\\n|\\r"); + // Those keys are not expected to be used in MDC, thus we filter-out those keys to prevent major issues + // when they are present as top-level MDC keys. + private static final Set RESERVED_KEYS = new HashSet(Arrays.asList( + "@timestamp", + "message", + "log.logger", + "log.level", + "event.dataset", + "process.thread.name", + "process.thread.id", + "ecs.version")); + public static CharSequence toNullSafeString(final CharSequence s) { return s == null ? "" : s; } @@ -194,16 +209,24 @@ public static void serializeOrigin(StringBuilder builder, String fileName, Strin public static void serializeMDC(StringBuilder builder, Map properties) { if (properties != null && !properties.isEmpty()) { for (Map.Entry entry : properties.entrySet()) { - builder.append('\"'); String key = entry.getKey(); - JsonUtils.quoteAsString(key, builder); - builder.append("\":\""); - JsonUtils.quoteAsString(toNullSafeString(String.valueOf(entry.getValue())), builder); - builder.append("\","); + String value = String.valueOf(entry.getValue()); + serializeMdcEntry(builder, key, value); } } } + public static void serializeMdcEntry(StringBuilder builder, String key, String value) { + if (RESERVED_KEYS.contains(key)) { + return; + } + builder.append('\"'); + JsonUtils.quoteAsString(key, builder); + builder.append("\":\""); + JsonUtils.quoteAsString(toNullSafeString(value), builder); + builder.append("\","); + } + public static void serializeException(StringBuilder builder, Throwable thrown, boolean stackTraceAsArray) { if (thrown != null) { builder.append("\"error.type\":\""); diff --git a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java index 3eca5e9e..267e62c1 100644 --- a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java +++ b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java @@ -32,7 +32,9 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -99,7 +101,7 @@ void testEscaping() throws IOException { } @Test - void serializeNullDoesNotThrowAnException() throws JsonProcessingException { + void serializeNullDoesNotThrowAnException() { StringBuilder stringBuilder = new StringBuilder(); EcsJsonSerializer.serializeFormattedMessage(stringBuilder, null); assertThat(stringBuilder.toString()).isEqualTo("\"message\":\"null\","); @@ -197,4 +199,24 @@ private void assertRemoveIfEndsWith(String builder, String ending, String expect EcsJsonSerializer.removeIfEndsWith(sb, ending); assertThat(sb.toString()).isEqualTo(expected); } + + @Test + void serializeMdc() throws JsonProcessingException { + StringBuilder jsonBuilder = new StringBuilder(); + EcsJsonSerializer.serializeObjectStart(jsonBuilder, 0); + Map mdc = new HashMap(); + mdc.put("message", "mdc message"); + mdc.put("@timestamp", "mdc timestamp"); + mdc.put("mdc.key1", "mdc value 1"); + mdc.put("mdc_key2", "mdc value 2"); + EcsJsonSerializer.serializeFormattedMessage(jsonBuilder, "formatted message"); + EcsJsonSerializer.serializeMDC(jsonBuilder, mdc); + EcsJsonSerializer.serializeObjectEnd(jsonBuilder); + + JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString()); + assertThat(jsonNode.get("message").textValue()).isEqualTo("formatted message"); + assertThat(jsonNode.get("@timestamp").textValue()).isEqualTo("1970-01-01T00:00:00.000Z"); + assertThat(jsonNode.get("mdc.key1").textValue()).isEqualTo("mdc value 1"); + assertThat(jsonNode.get("mdc_key2").textValue()).isEqualTo("mdc value 2"); + } } diff --git a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java index 573114de..ea92a0fe 100644 --- a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java +++ b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java @@ -55,7 +55,7 @@ void setUp() { } @Test - public void testFormatWithIncludeOriginFlag() throws Exception { + public void testFormatWithIncludeOriginFlag() { formatter.setIncludeOrigin(true); final String result = formatter.format(record); @@ -65,13 +65,13 @@ public void testFormatWithIncludeOriginFlag() throws Exception { } @Test - public void testFormatWithoutIncludeOriginFlag() throws Exception { + public void testFormatWithoutIncludeOriginFlag() { final JsonNode result = parseJson(formatter.format(record)); assertThat(result.get("log.origin")).isNull(); } @Test - public void testFormatWithoutLoggerName() throws Exception { + public void testFormatWithoutLoggerName() { record.setLoggerName(null); final JsonNode result = parseJson(formatter.format(record)); @@ -80,7 +80,7 @@ public void testFormatWithoutLoggerName() throws Exception { } @Test - public void testFormatWithEmptyLoggerName() throws Exception { + public void testFormatWithEmptyLoggerName() { record.setLoggerName(""); final JsonNode result = parseJson(formatter.format(record)); @@ -89,7 +89,7 @@ public void testFormatWithEmptyLoggerName() throws Exception { } @Test - public void testFormatWithInnerClassName() throws Exception { + public void testFormatWithInnerClassName() { formatter.setIncludeOrigin(true); record.setSourceClassName("test.ExampleClass$InnerClass"); @@ -99,7 +99,7 @@ public void testFormatWithInnerClassName() throws Exception { } @Test - public void testFormatWithInvalidClassName() throws Exception { + public void testFormatWithInvalidClassName() { formatter.setIncludeOrigin(true); record.setSourceClassName("$test.ExampleClass"); @@ -110,13 +110,23 @@ public void testFormatWithInvalidClassName() throws Exception { @Test void testMdcSerialization_singleEntry() { - Map mdc = new HashMap<>(); + Map mdc = new HashMap(); TestMdcEcsFormatter mdcFormatter = new TestMdcEcsFormatter(mdc); mdc.put("mdc.key", "value"); JsonNode result = parseJson(mdcFormatter.format(record)); assertThat(result.get("mdc.key").textValue()).isEqualTo("value"); } + @Test + void testMdcSerialization_filterEntries() { + Map mdc = new HashMap(); + TestMdcEcsFormatter mdcFormatter = new TestMdcEcsFormatter(mdc); + mdc.put("message", "mdc message"); + mdc.put("@timestamp", "mdc timestamp"); + JsonNode result = parseJson(mdcFormatter.format(record)); + assertThat(result.get("message").textValue()).isEqualTo(record.getMessage()); + } + private static JsonNode parseJson(String formatter) { try { return objectMapper.readTree(formatter); diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/DefaultMdcSerializer.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/DefaultMdcSerializer.java index 3da690ef..a5fc6990 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/DefaultMdcSerializer.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/DefaultMdcSerializer.java @@ -43,11 +43,7 @@ enum UsingContextData implements MdcSerializer { private static final TriConsumer WRITE_MDC = new TriConsumer() { @Override public void accept(final String key, final Object value, final StringBuilder stringBuilder) { - stringBuilder.append('\"'); - JsonUtils.quoteAsString(key, stringBuilder); - stringBuilder.append("\":\""); - JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder); - stringBuilder.append("\","); + EcsJsonSerializer.serializeMdcEntry(stringBuilder, key, String.valueOf(value)); } }; diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CustomMdcSerializer.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CustomMdcSerializer.java index c3bba8b1..af13021f 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CustomMdcSerializer.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CustomMdcSerializer.java @@ -42,11 +42,7 @@ public void serializeMdc(LogEvent event, StringBuilder builder) { // Default function for serializing MDC entries private static final TriConsumer DEFAULT_WRITE_MDC_FUNCTION = (key, value, stringBuilder) -> { - stringBuilder.append('\"'); - JsonUtils.quoteAsString(key, stringBuilder); - stringBuilder.append("\":\""); - JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder); - stringBuilder.append("\","); + EcsJsonSerializer.serializeMdcEntry(stringBuilder, key, String.valueOf(value)); }; // Custom function for handling a specific key diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithCustomMdcSerializerTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithCustomMdcSerializerTest.java index 2cda0eaf..d8e09df5 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithCustomMdcSerializerTest.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithCustomMdcSerializerTest.java @@ -1,3 +1,27 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2026 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ package co.elastic.logging.log4j2; import static co.elastic.logging.log4j2.CustomMdcSerializer.CUSTOM_MDC_SERIALIZER_TEST_KEY; diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithNotExistCustomMdcSerializerTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithNotExistCustomMdcSerializerTest.java index 170660e9..38e5af48 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithNotExistCustomMdcSerializerTest.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/EcsLayoutWithNotExistCustomMdcSerializerTest.java @@ -1,3 +1,27 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2026 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ package co.elastic.logging.log4j2; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/MdcSerializerResolverTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/MdcSerializerResolverTest.java index 906fe9e6..7abdc4be 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/MdcSerializerResolverTest.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/MdcSerializerResolverTest.java @@ -1,3 +1,27 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2026 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ package co.elastic.logging.log4j2; import org.junit.jupiter.api.Test;