diff --git a/docs/content/core/logging.mdx b/docs/content/core/logging.mdx index 4e6590582..3a07a3f62 100644 --- a/docs/content/core/logging.mdx +++ b/docs/content/core/logging.mdx @@ -37,6 +37,20 @@ Powertools extends the functionality of Log4J. Below is an example log4j2.xml fi ``` +You can also override log level by setting `LOG_LEVEL` env var - Here is an example using AWS Serverless Application Model (SAM) + +```yaml:title=template.yaml +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + ... + Runtime: java8 + Environment: + Variables: + LOG_LEVEL: DEBUG # highlight-line +``` + You can also explicitly set a service name via `POWERTOOLS_SERVICE_NAME` env var. This sets **service** key that will be present across all log statements. ## Standard structured keys @@ -137,6 +151,14 @@ public class App implements RequestHandler customKeys = new HashMap<>(); + customKeys.put("test", "value"); + customKeys.put("test1", "value1"); + + PowertoolsLogger.appendKeys(customKeys); + ... } } ``` \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogger.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogger.java index c947a03b9..6a9d976b0 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogger.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogger.java @@ -13,6 +13,8 @@ */ package software.amazon.lambda.powertools.logging; +import java.util.Map; + import org.apache.logging.log4j.ThreadContext; /** @@ -32,4 +34,15 @@ public class PowertoolsLogger { public static void appendKey(String key, String value) { ThreadContext.put(key, value); } + + + /** + * Appends additional key and value to each log entry made. Duplicate values + * for the same key will be replaced with the latest. + * + * @param customKeys Map of custom keys values to be appended to logs + */ + public static void appendKeys(Map customKeys) { + ThreadContext.putAll(customKeys); + } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java index 647ae2363..835812f60 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java @@ -49,9 +49,13 @@ @Aspect public final class LambdaLoggingAspect { private static final ObjectMapper mapper = new ObjectMapper(); - private static final String LOG_LEVEL = System.getenv("LOG_LEVEL"); + private static String LOG_LEVEL = System.getenv("LOG_LEVEL"); static { + resetLogLevels(); + } + + private static void resetLogLevels() { if (LOG_LEVEL != null) { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); Configurator.setAllLevels(LogManager.getRootLogger().getName(), Level.getLevel(LOG_LEVEL)); @@ -59,7 +63,7 @@ public final class LambdaLoggingAspect { } } - @SuppressWarnings({"EmptyMethod", "unused"}) + @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(powertoolsLogging)") public void callAt(PowertoolsLogging powertoolsLogging) { } diff --git a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java index 4823bce75..4b92d8cf6 100644 --- a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java +++ b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java @@ -14,6 +14,8 @@ package org.apache.logging.log4j.core.layout; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Paths; @@ -28,8 +30,10 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; +import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; import static java.util.Collections.emptyMap; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Mockito.when; @@ -43,11 +47,13 @@ class LambdaJsonLayoutTest { private Context context; @BeforeEach - void setUp() throws IOException { + void setUp() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { initMocks(this); setupContext(); //Make sure file is cleaned up before running full stack logging regression FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + writeStaticField(LambdaLoggingAspect.class, "LOG_LEVEL", "INFO", true); + resetLogLevel(); } @Test @@ -66,6 +72,32 @@ void shouldLogInStructuredFormat() throws IOException { .containsKey("service")); } + @Test + void shouldModifyLogLevelBasedOnEnvVariable() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { + writeStaticField(LambdaLoggingAspect.class, "LOG_LEVEL", "DEBUG", true); + resetLogLevel(); + + handler.handleRequest("test", context); + + assertThat(Files.lines(Paths.get("target/logfile.json"))) + .hasSize(2) + .satisfies(line -> { + assertThat(parseToMap(line.get(0))) + .containsEntry("level", "INFO") + .containsEntry("message", "Test event"); + + assertThat(parseToMap(line.get(1))) + .containsEntry("level", "DEBUG") + .containsEntry("message", "Test debug event"); + }); + } + + private void resetLogLevel() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels"); + resetLogLevels.setAccessible(true); + resetLogLevels.invoke(null); + } + private Map parseToMap(String stringAsJson) { try { return new ObjectMapper().readValue(stringAsJson, Map.class); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggerTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggerTest.java index d7c1a6420..7f2035255 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggerTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggerTest.java @@ -13,6 +13,9 @@ */ package software.amazon.lambda.powertools.logging; +import java.util.HashMap; +import java.util.Map; + import org.apache.logging.log4j.ThreadContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,4 +38,18 @@ void shouldSetCustomKeyOnThreadContext() { .hasSize(1) .containsEntry("test", "value"); } + + @Test + void shouldSetCustomKeyAsMapOnThreadContext() { + Map customKeys = new HashMap<>(); + customKeys.put("test", "value"); + customKeys.put("test1", "value1"); + + PowertoolsLogger.appendKeys(customKeys); + + assertThat(ThreadContext.getImmutableContext()) + .hasSize(2) + .containsEntry("test", "value") + .containsEntry("test1", "value1"); + } } \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java index 569ce5443..5678c0e95 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java @@ -26,6 +26,7 @@ public class PowerLogToolEnabled implements RequestHandler { @PowertoolsLogging public Object handleRequest(Object input, Context context) { LOG.info("Test event"); + LOG.debug("Test debug event"); return null; }