From 2b4b6a3aa2e71b0ff505b5570496d003eb128f11 Mon Sep 17 00:00:00 2001 From: Cornelius Matejka Date: Wed, 4 Feb 2026 17:19:36 +0100 Subject: [PATCH] feat(logging): add support for key-value pairs in LambdaJsonEncoder --- .../powertools/logging/logback/JsonUtils.java | 6 ++ .../logging/logback/LambdaJsonEncoder.java | 20 ++++--- .../internal/LambdaJsonEncoderTest.java | 59 +++++++++++++++++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java index 67d6b268d..fbdec8e4a 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java @@ -73,6 +73,12 @@ static void serializeMDCEntry(Map.Entry entry, JsonSerializer se } } + static void serializeKVPEntry(String key, Object value, JsonSerializer serializer) { + serializer.writeRaw(','); + serializer.writeFieldName(key); + serializer.writeObject(value); + } + static void serializeArguments(ILoggingEvent event, JsonSerializer serializer) throws IOException { Object[] arguments = event.getArgumentArray(); if (arguments != null) { diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java index 9afaf0ab7..894c3081b 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java @@ -15,10 +15,7 @@ package software.amazon.lambda.powertools.logging.logback; import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeArguments; -import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntries; -import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntry; -import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeTimestamp; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.*; import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -27,10 +24,8 @@ import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.core.encoder.EncoderBase; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; + import software.amazon.lambda.powertools.logging.internal.JsonSerializer; import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; @@ -88,6 +83,8 @@ public byte[] encode(ILoggingEvent event) { serializeMDCEntries(sortedMap, serializer); + serializeKeyValuePairs(event, serializer); + serializeArguments(event, serializer); serializeThreadInfo(event, serializer); @@ -104,6 +101,13 @@ public byte[] encode(ILoggingEvent event) { return builder.toString().getBytes(UTF_8); } + private void serializeKeyValuePairs(ILoggingEvent event, JsonSerializer serializer) { + Optional.ofNullable(event.getKeyValuePairs()) + .orElse(Collections.emptyList()).stream() + .filter(Objects::nonNull) + .forEach(kvp -> serializeKVPEntry(String.valueOf(kvp.key), kvp.value, serializer)); + } + private void serializeThreadInfo(ILoggingEvent event, JsonSerializer serializer) { if (includeThreadInfo) { if (event.getThreadName() != null) { diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java index 16bd9e92a..7e77b3e89 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java @@ -40,6 +40,7 @@ import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.List; import java.util.Collections; import java.util.Date; import java.util.TimeZone; @@ -50,6 +51,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.slf4j.event.KeyValuePair; import software.amazon.lambda.powertools.common.stubs.TestLambdaContext; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -442,4 +444,61 @@ void shouldLogException() { .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\\n"); } + @Test + void shouldLogKeyValuePairs() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.start(); + + Object[] arguments = { + "argument_01", + StructuredArguments.entry("structured_argument_01_retain", "retained"), + StructuredArguments.entry("structured_argument_02_overwrite", "to_be_overwritten") + }; + LoggingEvent keyValuePairsLoggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Key Value Pairs Test with argument: {}", + null, arguments); + + MDC.put("mdc_01_retain", "retained"); + MDC.put("mdc_02_overwrite", "to_be_overwritten"); + + keyValuePairsLoggingEvent.setKeyValuePairs(List.of( + new KeyValuePair("key_01_string", "value_01"), + new KeyValuePair("key_02_numeric", 2), + new KeyValuePair("key_03_decimal", 2.333), + new KeyValuePair("key_04_null", null), + new KeyValuePair("", "value_05_empty_key"), + new KeyValuePair(null, "value_06_null_key"), + new KeyValuePair("key_07_boolean_true", true), + new KeyValuePair("key_08_boolean_false", false), + new KeyValuePair("mdc_02_overwrite", "overwritten_by_kvp"), + new KeyValuePair("structured_argument_02_overwrite", "overwritten_by_kvp") + )); + + // WHEN + byte[] encoded = encoder.encode(keyValuePairsLoggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result) + // Arguments + .contains("Key Value Pairs Test with argument: argument_01") + .contains("\"structured_argument_01_retain\":\"retained\"") + // .doesNotContain("\"structured_argument_02_overwrite\":\"to_be_overwritten\"") TODO: Deduplication not implemented vor Arguments + // MDC + .contains("\"mdc_01_retain\":\"retained\"") + // .doesNotContain("\"mdc_02_overwrite\":\"to_be_overwritten\"") TODO: Deduplication not implemented vor Arguments + // Key Value Pairs + .contains("\"key_01_string\":\"value_01\"") + .contains("\"key_02_numeric\":2") + .contains("\"key_03_decimal\":2.333") + .contains("\"key_04_null\":null") + .contains("\"\":\"value_05_empty_key\"") + .contains("\"null\":\"value_06_null_key\"") + .contains("\"key_07_boolean_true\":true") + .contains("\"key_08_boolean_false\":false") + .contains("\"mdc_02_overwrite\":\"overwritten_by_kvp\"") + .contains("\"structured_argument_02_overwrite\":\"overwritten_by_kvp\"") + ; + } + }