From f2d8ca47a849ce45a9c2c7400a2eb4b4af8e215e Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 10:59:07 -0400 Subject: [PATCH 01/10] feat: add custom JsonLayout --- java-sdk-logging/log4j2-extension/pom.xml | 13 +++ .../sdk/logging/SDKLoggingJsonLayout.java | 88 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java diff --git a/java-sdk-logging/log4j2-extension/pom.xml b/java-sdk-logging/log4j2-extension/pom.xml index 6d8b6c79e4..c414f77f44 100644 --- a/java-sdk-logging/log4j2-extension/pom.xml +++ b/java-sdk-logging/log4j2-extension/pom.xml @@ -13,9 +13,22 @@ UTF-8 + 2.24.3 + 2.18.2 + + org.apache.logging.log4j + log4j-core + ${log4j.version} + true + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + org.junit.jupiter junit-jupiter-api diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java new file mode 100644 index 0000000000..e9f9d5b461 --- /dev/null +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.cloud.sdk.logging; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.Charset; +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.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.layout.AbstractStringLayout; + +@Plugin(name = "SDKLoggingJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public class SDKLoggingJsonLayout extends AbstractStringLayout { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final static String EMPTY_STRING = ""; + + protected SDKLoggingJsonLayout(Charset charset) { + super(charset); + } + + @Override + public String toSerializable(LogEvent event) { + Map jsonMap = new HashMap<>(); + jsonMap.put("timestamp", event.getTimeMillis()); + jsonMap.put("severity", event.getLevel().toString()); + jsonMap.put("message", event.getMessage().getFormattedMessage()); + + Map mdcMap = event.getContextData().toMap(); + for (Map.Entry entry : mdcMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null || value == null) { + continue; + } + + try { + jsonMap.put(key, convertToTreeNode(value)); + } catch (JsonProcessingException e) { + // in case of conversion exception, just use String + jsonMap.put(key, value); + } + } + + try { + return objectMapper.writeValueAsString(jsonMap); + } catch (JsonProcessingException e) { + return EMPTY_STRING; + } + } + + private JsonNode convertToTreeNode(String jsonString) throws JsonProcessingException { + return objectMapper.readTree(jsonString); + } +} From 3e5d9de3384f2883143b4908c3e8b7c70d10ccf1 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 11:38:47 -0400 Subject: [PATCH 02/10] add helper --- .../sdk/logging/SDKLoggingJsonLayout.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index e9f9d5b461..b327fae051 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -42,11 +42,19 @@ import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.layout.AbstractStringLayout; -@Plugin(name = "SDKLoggingJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +@Plugin( + name = "SDKLoggingJsonLayout", + category = Node.CATEGORY, + elementType = Layout.ELEMENT_TYPE, + printObject = true) public class SDKLoggingJsonLayout extends AbstractStringLayout { private final ObjectMapper objectMapper = new ObjectMapper(); - private final static String EMPTY_STRING = ""; + private static final String EMPTY_STRING = ""; + private static final String TIME_STAMP = "timestamp"; + private static final String LEVEL = "level"; + private static final String LOGGER_NAME = "logger_name"; + private static final String MESSAGE = "message"; protected SDKLoggingJsonLayout(Charset charset) { super(charset); @@ -55,9 +63,7 @@ protected SDKLoggingJsonLayout(Charset charset) { @Override public String toSerializable(LogEvent event) { Map jsonMap = new HashMap<>(); - jsonMap.put("timestamp", event.getTimeMillis()); - jsonMap.put("severity", event.getLevel().toString()); - jsonMap.put("message", event.getMessage().getFormattedMessage()); + extractNonMdc(event, jsonMap); Map mdcMap = event.getContextData().toMap(); for (Map.Entry entry : mdcMap.entrySet()) { @@ -82,6 +88,13 @@ public String toSerializable(LogEvent event) { } } + private void extractNonMdc(LogEvent event, Map jsonMap) { + jsonMap.put(TIME_STAMP, event.getTimeMillis()); + jsonMap.put(LEVEL, event.getLevel().toString()); + jsonMap.put(LOGGER_NAME, event.getLoggerName()); + jsonMap.put(MESSAGE, event.getMessage().getFormattedMessage()); + } + private JsonNode convertToTreeNode(String jsonString) throws JsonProcessingException { return objectMapper.readTree(jsonString); } From 943c20b1cb4b4fb4b108e96b00e04671769e5feb Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 14:10:54 -0400 Subject: [PATCH 03/10] switch to gson --- java-sdk-logging/log4j2-extension/pom.xml | 8 +++---- .../sdk/logging/SDKLoggingJsonLayout.java | 24 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/pom.xml b/java-sdk-logging/log4j2-extension/pom.xml index c414f77f44..020d9a6329 100644 --- a/java-sdk-logging/log4j2-extension/pom.xml +++ b/java-sdk-logging/log4j2-extension/pom.xml @@ -14,7 +14,7 @@ UTF-8 2.24.3 - 2.18.2 + 2.12.1 @@ -25,9 +25,9 @@ true - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} + com.google.code.gson + gson + ${gson.version} org.junit.jupiter diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index b327fae051..5b5a002534 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -30,9 +30,10 @@ package com.google.cloud.sdk.logging; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; @@ -49,7 +50,8 @@ printObject = true) public class SDKLoggingJsonLayout extends AbstractStringLayout { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final Gson gson = new Gson(); + private static final String EMPTY_STRING = ""; private static final String TIME_STAMP = "timestamp"; private static final String LEVEL = "level"; @@ -74,18 +76,14 @@ public String toSerializable(LogEvent event) { } try { - jsonMap.put(key, convertToTreeNode(value)); - } catch (JsonProcessingException e) { + jsonMap.put(key, toJsonElement(value)); + } catch (JsonParseException e) { // in case of conversion exception, just use String jsonMap.put(key, value); } } - try { - return objectMapper.writeValueAsString(jsonMap); - } catch (JsonProcessingException e) { - return EMPTY_STRING; - } + return gson.toJson(jsonMap); } private void extractNonMdc(LogEvent event, Map jsonMap) { @@ -95,7 +93,7 @@ private void extractNonMdc(LogEvent event, Map jsonMap) { jsonMap.put(MESSAGE, event.getMessage().getFormattedMessage()); } - private JsonNode convertToTreeNode(String jsonString) throws JsonProcessingException { - return objectMapper.readTree(jsonString); + private JsonElement toJsonElement(String jsonString) throws JsonParseException { + return JsonParser.parseString(jsonString); } } From a8ea9c55aad4d024f9d1ecef6e71cf8b94e79e7a Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 14:11:32 -0400 Subject: [PATCH 04/10] format --- .../java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index 5b5a002534..75e172e858 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -51,8 +51,6 @@ public class SDKLoggingJsonLayout extends AbstractStringLayout { private final Gson gson = new Gson(); - - private static final String EMPTY_STRING = ""; private static final String TIME_STAMP = "timestamp"; private static final String LEVEL = "level"; private static final String LOGGER_NAME = "logger_name"; From 58efcfcd689dc58560614dced4485ac99c6e8309 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 16:44:57 -0400 Subject: [PATCH 05/10] add unit tests --- .../sdk/logging/SDKLoggingJsonLayoutTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java new file mode 100644 index 0000000000..0b21d79f4d --- /dev/null +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.cloud.sdk.logging; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.gson.JsonParser; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.message.SimpleMessage; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SDKLoggingJsonLayoutTest { + private final SDKLoggingJsonLayout sdkLoggingJsonLayout = + new SDKLoggingJsonLayout(StandardCharsets.UTF_8); + private final LogEvent logEvent = mock(LogEvent.class); + private final ReadOnlyStringMap map = mock(ReadOnlyStringMap.class); + private final Map mdcMap = new HashMap<>(); + + @BeforeEach + void init() { + when(logEvent.getTimeMillis()).thenReturn(10000L); + when(logEvent.getLevel()).thenReturn(Level.DEBUG); + when(logEvent.getLoggerName()).thenReturn("com.example.Example"); + when(logEvent.getMessage()).thenReturn(new SimpleMessage("example message")); + when(logEvent.getContextData()).thenReturn(map); + when(map.toMap()).thenReturn(mdcMap); + } + + @Test + void testToSerializableContainsNonMdcContents() { + assertEquals( + "{\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + sdkLoggingJsonLayout.toSerializable(logEvent)); + } + + @Test + void testToSerializableSkipNullKey() { + mdcMap.put(null, "example value"); + String logString = sdkLoggingJsonLayout.toSerializable(logEvent); + assertFalse(logString.contains("example value")); + } + + @Test + void testToSerializableSkipNullValue() { + mdcMap.put("example key", null); + String logString = sdkLoggingJsonLayout.toSerializable(logEvent); + assertFalse(logString.contains("example key")); + } + + @Test + void testToSerializableInvalidJsonValue() { + // the last colon is invalid. + mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue,}}"); + assertEquals( + "{\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\",\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + sdkLoggingJsonLayout.toSerializable(logEvent)); + } + + @Test + void testToSerializableValidJsonValue() { + // the last colon is invalid. + mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); + String log = sdkLoggingJsonLayout.toSerializable(logEvent); + assertEquals( + "{\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue}}\",\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + log); + assertDoesNotThrow(() -> JsonParser.parseString(log)); + } +} From 1aa05afcd0e6b5cad5fec49659bf5894b02f701c Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 16:48:44 -0400 Subject: [PATCH 06/10] fix unit tests --- .../com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java index 0b21d79f4d..02495b4e2b 100644 --- a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -100,7 +100,7 @@ void testToSerializableValidJsonValue() { mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); String log = sdkLoggingJsonLayout.toSerializable(logEvent); assertEquals( - "{\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue}}\",\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + "{\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}},\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", log); assertDoesNotThrow(() -> JsonParser.parseString(log)); } From 82413416615c281f2f94a4f827be3399a9169828 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 16:49:51 -0400 Subject: [PATCH 07/10] refactor --- .../google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java index 02495b4e2b..f33d485b07 100644 --- a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -86,7 +86,7 @@ void testToSerializableSkipNullValue() { } @Test - void testToSerializableInvalidJsonValue() { + void testToSerializableInvalidJsonValueWriteString() { // the last colon is invalid. mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue,}}"); assertEquals( @@ -95,7 +95,7 @@ void testToSerializableInvalidJsonValue() { } @Test - void testToSerializableValidJsonValue() { + void testToSerializableValidJsonValueWriteJson() { // the last colon is invalid. mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); String log = sdkLoggingJsonLayout.toSerializable(logEvent); From 1e895efe966788de153e96fb7a54acfe7d5708ec Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 18:10:20 -0400 Subject: [PATCH 08/10] add factory method --- .../sdk/logging/SDKLoggingJsonLayout.java | 41 ++++++++++++++++--- .../sdk/logging/SDKLoggingJsonLayoutTest.java | 9 ++-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index 75e172e858..fcfa576a68 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -35,12 +35,15 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import java.nio.charset.Charset; -import java.util.HashMap; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; 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.Node; import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.layout.AbstractStringLayout; @Plugin( @@ -48,7 +51,7 @@ category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) -public class SDKLoggingJsonLayout extends AbstractStringLayout { +public final class SDKLoggingJsonLayout extends AbstractStringLayout { private final Gson gson = new Gson(); private static final String TIME_STAMP = "timestamp"; @@ -56,16 +59,17 @@ public class SDKLoggingJsonLayout extends AbstractStringLayout { private static final String LOGGER_NAME = "logger_name"; private static final String MESSAGE = "message"; - protected SDKLoggingJsonLayout(Charset charset) { - super(charset); + private SDKLoggingJsonLayout(final Builder builder) { + super(builder.getCharset()); } @Override public String toSerializable(LogEvent event) { - Map jsonMap = new HashMap<>(); + // use LinkedHashMap to fix iteration order. + Map jsonMap = new LinkedHashMap<>(); extractNonMdc(event, jsonMap); - Map mdcMap = event.getContextData().toMap(); + Map mdcMap = new LinkedHashMap<>(event.getContextData().toMap()); for (Map.Entry entry : mdcMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); @@ -94,4 +98,29 @@ private void extractNonMdc(LogEvent event, Map jsonMap) { private JsonElement toJsonElement(String jsonString) throws JsonParseException { return JsonParser.parseString(jsonString); } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder + implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute private Charset charset = StandardCharsets.UTF_8; + + public Charset getCharset() { + return charset; + } + + public Builder setCharset(final Charset charset) { + this.charset = charset; + return this; + } + + @Override + public SDKLoggingJsonLayout build() { + return new SDKLoggingJsonLayout(this); + } + } } diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java index f33d485b07..1029d2334f 100644 --- a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -37,7 +37,6 @@ import static org.mockito.Mockito.when; import com.google.gson.JsonParser; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import org.apache.logging.log4j.Level; @@ -49,7 +48,7 @@ public class SDKLoggingJsonLayoutTest { private final SDKLoggingJsonLayout sdkLoggingJsonLayout = - new SDKLoggingJsonLayout(StandardCharsets.UTF_8); + SDKLoggingJsonLayout.newBuilder().build(); private final LogEvent logEvent = mock(LogEvent.class); private final ReadOnlyStringMap map = mock(ReadOnlyStringMap.class); private final Map mdcMap = new HashMap<>(); @@ -67,7 +66,7 @@ void init() { @Test void testToSerializableContainsNonMdcContents() { assertEquals( - "{\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\"}", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -90,7 +89,7 @@ void testToSerializableInvalidJsonValueWriteString() { // the last colon is invalid. mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue,}}"); assertEquals( - "{\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\",\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\"}", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -100,7 +99,7 @@ void testToSerializableValidJsonValueWriteJson() { mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); String log = sdkLoggingJsonLayout.toSerializable(logEvent); assertEquals( - "{\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}},\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"timestamp\":10000}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}}}", log); assertDoesNotThrow(() -> JsonParser.parseString(log)); } From 997e72d0cde2f7126a0c6040f53025ea071a6fb9 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 18:18:57 -0400 Subject: [PATCH 09/10] write a newline --- .../com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java | 2 +- .../google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index fcfa576a68..a3d71034bf 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -85,7 +85,7 @@ public String toSerializable(LogEvent event) { } } - return gson.toJson(jsonMap); + return String.format("%s\n", gson.toJson(jsonMap)); } private void extractNonMdc(LogEvent event, Map jsonMap) { diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java index 1029d2334f..ce9fdc797b 100644 --- a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -66,7 +66,7 @@ void init() { @Test void testToSerializableContainsNonMdcContents() { assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\"}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\"}\n", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -89,7 +89,7 @@ void testToSerializableInvalidJsonValueWriteString() { // the last colon is invalid. mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue,}}"); assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\"}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\"}\n", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -99,7 +99,7 @@ void testToSerializableValidJsonValueWriteJson() { mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); String log = sdkLoggingJsonLayout.toSerializable(logEvent); assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}}}", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}}}\n", log); assertDoesNotThrow(() -> JsonParser.parseString(log)); } From a95ade1d47e807ec60ed68bdf69f5cb1a783ff52 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Wed, 19 Mar 2025 18:42:08 -0400 Subject: [PATCH 10/10] write thread name and id --- .../google/cloud/sdk/logging/SDKLoggingJsonLayout.java | 4 ++++ .../cloud/sdk/logging/SDKLoggingJsonLayoutTest.java | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java index a3d71034bf..51f2c94c27 100644 --- a/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java +++ b/java-sdk-logging/log4j2-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayout.java @@ -57,6 +57,8 @@ public final class SDKLoggingJsonLayout extends AbstractStringLayout { private static final String TIME_STAMP = "timestamp"; private static final String LEVEL = "level"; private static final String LOGGER_NAME = "logger_name"; + private static final String THREAD_NAME = "thread_name"; + private static final String THREAD_ID = "thread_ID"; private static final String MESSAGE = "message"; private SDKLoggingJsonLayout(final Builder builder) { @@ -92,6 +94,8 @@ private void extractNonMdc(LogEvent event, Map jsonMap) { jsonMap.put(TIME_STAMP, event.getTimeMillis()); jsonMap.put(LEVEL, event.getLevel().toString()); jsonMap.put(LOGGER_NAME, event.getLoggerName()); + jsonMap.put(THREAD_NAME, event.getThreadName()); + jsonMap.put(THREAD_ID, event.getThreadId()); jsonMap.put(MESSAGE, event.getMessage().getFormattedMessage()); } diff --git a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java index ce9fdc797b..044707ac25 100644 --- a/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java +++ b/java-sdk-logging/log4j2-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingJsonLayoutTest.java @@ -58,6 +58,8 @@ void init() { when(logEvent.getTimeMillis()).thenReturn(10000L); when(logEvent.getLevel()).thenReturn(Level.DEBUG); when(logEvent.getLoggerName()).thenReturn("com.example.Example"); + when(logEvent.getThreadName()).thenReturn("example thread name"); + when(logEvent.getThreadId()).thenReturn(123L); when(logEvent.getMessage()).thenReturn(new SimpleMessage("example message")); when(logEvent.getContextData()).thenReturn(map); when(map.toMap()).thenReturn(mdcMap); @@ -66,7 +68,7 @@ void init() { @Test void testToSerializableContainsNonMdcContents() { assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\"}\n", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"thread_name\":\"example thread name\",\"thread_ID\":123,\"message\":\"example message\"}\n", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -89,7 +91,7 @@ void testToSerializableInvalidJsonValueWriteString() { // the last colon is invalid. mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue,}}"); assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\"}\n", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"thread_name\":\"example thread name\",\"thread_ID\":123,\"message\":\"example message\",\"example key\":\"{key:value,jsonKey:{nestedKey:nestedValue,}}\"}\n", sdkLoggingJsonLayout.toSerializable(logEvent)); } @@ -99,7 +101,7 @@ void testToSerializableValidJsonValueWriteJson() { mdcMap.put("example key", "{key:value,jsonKey:{nestedKey:nestedValue}}"); String log = sdkLoggingJsonLayout.toSerializable(logEvent); assertEquals( - "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"message\":\"example message\",\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}}}\n", + "{\"timestamp\":10000,\"level\":\"DEBUG\",\"logger_name\":\"com.example.Example\",\"thread_name\":\"example thread name\",\"thread_ID\":123,\"message\":\"example message\",\"example key\":{\"key\":\"value\",\"jsonKey\":{\"nestedKey\":\"nestedValue\"}}}\n", log); assertDoesNotThrow(() -> JsonParser.parseString(log)); }