From 9b1147395b71b6ef7d000abaf488e3c1f05cf0e5 Mon Sep 17 00:00:00 2001 From: Pratap Chandra Deo <61131823+Pratapchandradeo@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:26:57 +0530 Subject: [PATCH 1/3] feat: add automatic TenantId logging to Logger default properties - Add TENANT_ID to PowertoolsLoggedFields enum with null-safety checks - Create TENANT_ID_RESOLVER in Log4j2 PowertoolsResolver with isResolvable check - Register TENANT_ID resolver in eventResolverMap for Log4j2 support - Add TENANT_ID_ATTR_NAME constant to LambdaEcsEncoder (ECS format: tenant.id) - Add conditional null-safe serialization of tenant_id in Logback ECS encoder - Update LambdaJsonLayout.json to include tenant_id field for Log4j2 JSON - Update LambdaEcsLayout.json to include tenant.id field for Log4j2 ECS - Update LambdaJsonEncoder JavaDoc to include tenant_id in listed fields - Add getTenantId() override in TestLambdaContext test stub - Update test assertions in LambdaJsonEncoderTest to verify tenant_id in output Fixes: #2348 References: #2358 This implementation follows AWS Lambda Tenant Isolation feature requirements and applies null-safety patterns consistent with correlation_id handling. --- .../common/stubs/TestLambdaContext.java | 5 +++++ .../json/resolver/PowertoolsResolver.java | 19 +++++++++++++++++++ .../src/main/resources/LambdaEcsLayout.json | 6 +++++- .../src/main/resources/LambdaJsonLayout.json | 6 +++++- .../logging/logback/LambdaEcsEncoder.java | 8 ++++++++ .../logging/logback/LambdaJsonEncoder.java | 1 + .../internal/LambdaJsonEncoderTest.java | 8 +++++--- .../internal/PowertoolsLoggedFields.java | 7 ++++++- 8 files changed, 54 insertions(+), 6 deletions(-) diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/stubs/TestLambdaContext.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/stubs/TestLambdaContext.java index 6b66b66b7..f54db723b 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/stubs/TestLambdaContext.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/stubs/TestLambdaContext.java @@ -74,4 +74,9 @@ public int getMemoryLimitInMB() { public LambdaLogger getLogger() { return null; } + + @Override + public String getTenantId() { + return "test-tenant"; + } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java index cef5b86ee..96f566130 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java @@ -25,6 +25,7 @@ import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SAMPLING_RATE; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.TENANT_ID; import java.io.IOException; import java.util.Collections; @@ -186,6 +187,23 @@ public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { } }; + private static final EventResolver TENANT_ID_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String tenantId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.TENANT_ID.getName()); + return null != tenantId; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String tenantId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.TENANT_ID.getName()); + jsonWriter.writeString(tenantId); + } + }; + + @SuppressWarnings("java:S106") private static final EventResolver NON_POWERTOOLS_FIELD_RESOLVER = (LogEvent logEvent, JsonWriter jsonWriter) -> { @@ -233,6 +251,7 @@ public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { { FUNCTION_TRACE_ID.getName(), XRAY_TRACE_RESOLVER }, { CORRELATION_ID.getName(), CORRELATION_ID_RESOLVER }, { SAMPLING_RATE.getName(), SAMPLING_RATE_RESOLVER }, + { TENANT_ID.getName(), TENANT_ID_RESOLVER }, { "region", REGION_RESOLVER }, { "account_id", ACCOUNT_ID_RESOLVER } }).collect(Collectors.toMap(data -> (String) data[0], data -> (EventResolver) data[1]))); diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json index 58b30f60e..24dbaa0ba 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json @@ -87,7 +87,11 @@ "$resolver": "powertools", "field": "correlation_id" }, + "tenant.id": { + "$resolver": "powertools", + "field": "tenant_id" + }, "": { "$resolver": "powertools" } -} +} \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json index 793006502..1e9b5f4db 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json @@ -69,7 +69,11 @@ "$resolver": "powertools", "field": "correlation_id" }, + "tenant_id": { + "$resolver": "powertools", + "field": "tenant_id" + }, "": { "$resolver": "powertools" } -} +} \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java index b934f378d..039f0b93d 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -24,6 +24,7 @@ import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; import static software.amazon.lambda.powertools.logging.logback.JsonUtils.*; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.TENANT_ID; import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -72,6 +73,7 @@ public class LambdaEcsEncoder extends EncoderBase { protected static final String FUNCTION_MEMORY_ATTR_NAME = "faas.memory"; protected static final String FUNCTION_TRACE_ID_ATTR_NAME = "trace.id"; protected static final String CORRELATION_ID_ATTR_NAME = "correlation.id"; + protected static final String TENANT_ID_ATTR_NAME = "tenant.id"; protected static final String ECS_VERSION = "1.2.0"; protected static final String CLOUD_PROVIDER = "aws"; @@ -169,6 +171,12 @@ private void serializeFunctionInfo(JsonSerializer serializer, String arn, Mapxray_trace_id *
  • sampling_rate
  • *
  • service
  • + *
  • tenant_id
  • * *
    * We strongly recommend to keep these information. 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 ca256ad5d..238257f4c 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 @@ -111,8 +111,8 @@ void shouldLogInJsonFormat() { // THEN File logFile = new File("target/logfile.json"); assertThat(contentOf(logFile)).contains( - "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":"); - } + "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"service\":\"testLogback\",\"tenant_id\":\"test-tenant\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":\""); + } @Test void shouldLogArgumentsAsJsonWhenUsingRawJson() { @@ -203,7 +203,9 @@ void shouldNotLogPowertoolsInfo() { // THEN assertThat(result).contains( - "{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"timestamp\":"); + assertThat(result).contains( + "{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"tenant_id\":\"test-tenant\",\"timestamp\":\"" + ); // WHEN (powertoolsInfo = false) encoder.setIncludePowertoolsInfo(false); diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java index 2545396d2..ad47608a8 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java @@ -35,7 +35,8 @@ public enum PowertoolsLoggedFields { FUNCTION_TRACE_ID("xray_trace_id"), SAMPLING_RATE("sampling_rate"), CORRELATION_ID("correlation_id"), - SERVICE("service"); + SERVICE("service"), + TENANT_ID("tenant_id"); private final String name; @@ -55,6 +56,10 @@ public static Map setValuesFromLambdaContext(Context context) { hashMap.put(FUNCTION_ARN.name, context.getInvokedFunctionArn()); hashMap.put(FUNCTION_MEMORY_SIZE.name, String.valueOf(context.getMemoryLimitInMB())); hashMap.put(FUNCTION_REQUEST_ID.name, String.valueOf(context.getAwsRequestId())); + String tenantId = context.getTenantId(); + if (tenantId != null && !tenantId.isEmpty()) { + hashMap.put(TENANT_ID.name, tenantId); + } return hashMap; } From 42d9e9d49cc2f25d25dde0856324ea3065ed79d3 Mon Sep 17 00:00:00 2001 From: Pratap Chandra Deo <61131823+Pratapchandradeo@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:26:11 +0530 Subject: [PATCH 2/3] chnges done as for the comment --- .../json/resolver/PowertoolsResolver.java | 2 +- .../logging/logback/LambdaEcsEncoder.java | 2 +- .../internal/LambdaJsonEncoderTest.java | 29 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java index 96f566130..a3283e38a 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java @@ -192,7 +192,7 @@ public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { public boolean isResolvable(LogEvent logEvent) { final String tenantId = logEvent.getContextData().getValue(PowertoolsLoggedFields.TENANT_ID.getName()); - return null != tenantId; + return null != tenantId && !tenantId.isEmpty(); } @Override diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java index 039f0b93d..17feac731 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -173,7 +173,7 @@ private void serializeFunctionInfo(JsonSerializer serializer, String arn, Map Date: Thu, 16 Apr 2026 16:14:48 +0530 Subject: [PATCH 3/3] feat: add null-safe tenant_id logging for Lambda Tenant Isolation --- .../powertools/logging/logback/LambdaEcsEncoder.java | 2 +- .../powertools/logging/logback/LambdaJsonEncoder.java | 4 +++- .../logging/internal/LambdaJsonEncoderTest.java | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java index 17feac731..629224011 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -173,7 +173,7 @@ private void serializeFunctionInfo(JsonSerializer serializer, String arn, Map sortedMap, JsonSerializ if (includePowertoolsInfo) { for (Map.Entry entry : sortedMap.entrySet()) { if (PowertoolsLoggedFields.stringValues().contains(entry.getKey()) - && !(entry.getKey().equals(PowertoolsLoggedFields.SAMPLING_RATE.getName()) && entry.getValue().equals("0.0"))) { + && !(entry.getKey().equals(PowertoolsLoggedFields.SAMPLING_RATE.getName()) && "0.0".equals(entry.getValue())) + && !(entry.getKey().equals(PowertoolsLoggedFields.TENANT_ID.getName()) + && (entry.getValue() == null || entry.getValue().isEmpty()))) { serializeMDCEntry(entry, serializer); } } 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 82149d22e..75ef79369 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 @@ -196,6 +196,7 @@ void shouldNotLogPowertoolsInfo() { MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + MDC.put(PowertoolsLoggedFields.TENANT_ID.getName(), "test-tenant"); // WHEN byte[] encoded = encoder.encode(loggingEvent); @@ -203,7 +204,6 @@ void shouldNotLogPowertoolsInfo() { // THEN assertThat(result).contains( - assertThat(result).contains( "{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:us-east-1:123456789012:function:test\",\"function_memory_size\":128,\"function_name\":\"test-function\",\"function_request_id\":\"test-request-id\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"tenant_id\":\"test-tenant\",\"timestamp\":\"" ); @@ -214,7 +214,7 @@ void shouldNotLogPowertoolsInfo() { // THEN (no powertools info in logs) assertThat(result).doesNotContain("cold_start", "function_arn", "function_memory_size", "function_name", - "function_request_id", "function_version", "sampling_rate", "service"); + "function_request_id", "function_version", "sampling_rate", "service", "tenant_id"); } @Test @@ -443,7 +443,9 @@ void shouldLogException() { // THEN (stack is logged with root cause first) assertThat(result).contains("\"message\":\"Unexpected value\"") .contains("\"name\":\"java.lang.IllegalStateException\"") - .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\\n"); + .containsAnyOf( + "\"stack\":\"java.lang.IllegalStateException: Unexpected value\\n", + "\"stack\":\"java.lang.IllegalStateException: Unexpected value\\r\\n"); } @Test