From 59011e5ac0470e2758844555186456b013fa5820 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 29 Aug 2025 14:29:58 +0200 Subject: [PATCH 01/45] Log buffer PoC with log4j2 appender implementation. --- .../sam/src/main/java/helloworld/App.java | 75 ++------ .../sam/src/main/resources/log4j2.xml | 12 +- .../sam/template.yaml | 6 +- .../logging/log4j/BufferingAppender.java | 178 ++++++++++++++++++ .../internal/Log4jLoggingManager.java | 36 +++- ...powertools.logging.internal.LoggingManager | 2 +- .../internal/handler/PowertoolsArguments.java | 2 +- .../handler/PowertoolsLogEnabled.java | 8 +- .../internal/Log4jLoggingManagerTest.java | 7 +- .../powertools/logging/PowertoolsLogging.java | 55 ++++++ .../logging/internal/BufferManager.java | 33 ++++ ...anager.java => DefaultLoggingManager.java} | 2 +- .../logging/internal/LambdaLoggingAspect.java | 88 ++------- .../internal/LoggingManagerRegistry.java | 98 ++++++++++ .../logging/PowertoolsLoggingTest.java | 66 +++++++ .../internal/LambdaLoggingAspectTest.java | 47 ----- .../internal/LoggingManagerRegistryTest.java | 136 +++++++++++++ .../logging/internal/TestLoggingManager.java | 33 +++- 18 files changed, 687 insertions(+), 197 deletions(-) create mode 100644 powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java rename powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/{log4 => log4j}/internal/Log4jLoggingManager.java (55%) rename powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/{ => log4j}/internal/Log4jLoggingManagerTest.java (82%) create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogging.java create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/BufferManager.java rename powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/{DefautlLoggingManager.java => DefaultLoggingManager.java} (94%) create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index 2675c96eb..daf59ecf7 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -14,16 +14,8 @@ package helloworld; -import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,79 +27,52 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsFactory; -import software.amazon.lambda.powertools.metrics.model.DimensionSet; -import software.amazon.lambda.powertools.metrics.model.MetricResolution; -import software.amazon.lambda.powertools.metrics.model.MetricUnit; -import software.amazon.lambda.powertools.tracing.CaptureMode; -import software.amazon.lambda.powertools.tracing.Tracing; -import software.amazon.lambda.powertools.tracing.TracingUtils; +import software.amazon.lambda.powertools.logging.PowertoolsLogging; /** * Handler for requests to Lambda function. */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); - private static final Metrics metrics = MetricsFactory.getMetricsInstance(); - @Logging(logEvent = true, samplingRate = 0.7) - @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + public App() { + // Flush immediately because no trace ID is set yet + log.debug("Constructor DEBUG - should not be buffered (no trace ID)"); + log.info("Constructor INFO - should not be buffered (no trace ID)"); + } + + @Logging(logEvent = false) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // Manually set trace ID for testing in SAM local + System.setProperty("com.amazonaws.xray.traceHeader", + "Root=1-63441c4a-abcdef012345678912345678;Parent=0123456789abcdef;Sampled=1"); + Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - - DimensionSet dimensionSet = new DimensionSet(); - dimensionSet.addDimension("AnotherService", "CustomService"); - dimensionSet.addDimension("AnotherService1", "CustomService1"); - metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - - metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); - + log.debug("DEBUG 1"); MDC.put("test", "willBeLogged"); + log.debug("DEBUG 2"); + log.info("INFO 1"); + + // Manually flush buffer to show buffered debug logs + PowertoolsLogging.flushBuffer(); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { - final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info("", entry("ip", pageContents)); - TracingUtils.putAnnotation("Test", "New"); - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - - TracingUtils.withSubsegment("loggingResponse", subsegment -> { - String sampled = "log something out"; - log.info(sampled); - log.info(output); - }); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", "Test"); - log.info("After output"); return response .withStatusCode(200) .withBody(output); - } catch (RuntimeException | IOException e) { + } catch (RuntimeException e) { return response .withBody("{}") .withStatusCode(500); } } - @Tracing - private void log() { - log.info("inside threaded logging for function"); - } - - @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) - private String getPageContents(String address) throws IOException { - URL url = new URL(address); - putMetadata("getPageContents", address); - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining(System.lineSeparator())); - } - } } diff --git a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml index e1fd14cea..e140022e4 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml @@ -4,13 +4,13 @@ + + + - - - - - + + - \ No newline at end of file + diff --git a/examples/powertools-examples-core-utilities/sam/template.yaml b/examples/powertools-examples-core-utilities/sam/template.yaml index 9a51a1ba9..a35e8bd32 100644 --- a/examples/powertools-examples-core-utilities/sam/template.yaml +++ b/examples/powertools-examples-core-utilities/sam/template.yaml @@ -13,9 +13,9 @@ Globals: Environment: Variables: # Powertools for AWS Lambda (Java) env vars: https://docs.powertools.aws.dev/lambda/java/#environment-variables - POWERTOOLS_LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_LOG_LEVEL: DEBUG + # POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: false POWERTOOLS_METRICS_NAMESPACE: Coreutilities Resources: diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java new file mode 100644 index 000000000..52f7ff511 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -0,0 +1,178 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.log4j; + +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.AppenderRef; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; + +/** + * A minimalistic Log4j2 appender that buffers log events based on trace ID + * and flushes them when error logs are encountered or manually triggered. + */ +@Plugin(name = "BufferingAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class BufferingAppender extends AbstractAppender { + + private final AppenderRef[] appenderRefs; + private final Configuration configuration; + private final Level bufferAtVerbosity; + private final int maxBytes; + private final boolean flushOnErrorLog; + private final Map> bufferCache = new ConcurrentHashMap<>(); + private final ThreadLocal bufferOverflowWarned = new ThreadLocal<>(); + + protected BufferingAppender(String name, Filter filter, Layout layout, + AppenderRef[] appenderRefs, Configuration configuration, Level bufferAtVerbosity, int maxBytes, + boolean flushOnErrorLog) { + super(name, filter, layout, false, null); + this.appenderRefs = appenderRefs; + this.configuration = configuration; + this.bufferAtVerbosity = bufferAtVerbosity; + this.maxBytes = maxBytes; + this.flushOnErrorLog = flushOnErrorLog; + } + + @Override + public void append(LogEvent event) { + if (appenderRefs == null || appenderRefs.length == 0) { + return; + } + LambdaHandlerProcessor.getXrayTraceId().ifPresentOrElse( + traceId -> { + // Check if we should buffer this log level + if (shouldBuffer(event.getLevel())) { + bufferEvent(traceId, event); + } else { + callAppenders(event); + } + + // Flush buffer on error logs if configured + if (flushOnErrorLog && event.getLevel().isMoreSpecificThan(Level.WARN)) { + flushBuffer(traceId); + } + }, + () -> callAppenders(event) // No trace ID (INIT phase), log directly + ); + } + + private void callAppenders(LogEvent event) { + for (AppenderRef ref : appenderRefs) { + Appender appender = configuration.getAppender(ref.getRef()); + if (appender != null) { + appender.append(event); + } + } + } + + private boolean shouldBuffer(Level level) { + return level.isLessSpecificThan(bufferAtVerbosity) || level.equals(bufferAtVerbosity); + } + + private void bufferEvent(String traceId, LogEvent event) { + // Create immutable copy to prevent mutation + LogEvent immutableEvent = Log4jLogEvent.createMemento(event); + + // Check if single event is larger than buffer - discard if so + int eventSize = immutableEvent.getMessage().getFormattedMessage().length(); + if (eventSize > maxBytes) { + if (Boolean.TRUE != bufferOverflowWarned.get()) { + bufferOverflowWarned.set(true); + } + return; + } + + bufferCache.computeIfAbsent(traceId, k -> new ArrayDeque<>()).add(immutableEvent); + + // Simple size check - remove oldest if over limit + Deque buffer = bufferCache.get(traceId); + while (getBufferSize(buffer) > maxBytes && !buffer.isEmpty()) { + if (Boolean.TRUE != bufferOverflowWarned.get()) { + bufferOverflowWarned.set(true); + } + buffer.removeFirst(); + } + } + + private int getBufferSize(Deque buffer) { + return buffer.stream() + .mapToInt(event -> event.getMessage().getFormattedMessage().length()) + .sum(); + } + + public void clearBuffer() { + LambdaHandlerProcessor.getXrayTraceId().ifPresent(bufferCache::remove); + } + + public void flushBuffer() { + LambdaHandlerProcessor.getXrayTraceId().ifPresent(this::flushBuffer); + } + + private void flushBuffer(String traceId) { + Deque buffer = bufferCache.remove(traceId); + if (buffer != null) { + // Emit buffer overflow warning if it occurred + if (Boolean.TRUE == bufferOverflowWarned.get()) { + LOGGER.warn("Buffer size exceeded for trace ID: {}. Some log events were discarded.", traceId); + bufferOverflowWarned.remove(); + } + buffer.forEach(this::callAppenders); + } + } + + @PluginFactory + public static BufferingAppender createAppender( + @PluginAttribute("name") String name, + @PluginElement("Filter") Filter filter, + @PluginElement("Layout") Layout layout, + @PluginElement("AppenderRef") AppenderRef[] appenderRefs, + @PluginConfiguration Configuration configuration, + @PluginAttribute(value = "bufferAtVerbosity", defaultString = "DEBUG") String bufferAtVerbosity, + @PluginAttribute(value = "maxBytes", defaultInt = 20480) int maxBytes, + @PluginAttribute(value = "flushOnErrorLog", defaultBoolean = true) boolean flushOnErrorLog) { + + if (name == null) { + LOGGER.error("No name provided for BufferingAppender"); + return null; + } + + Level level = Level.getLevel(bufferAtVerbosity); + if (level == null) { + level = Level.DEBUG; + } + + return new BufferingAppender(name, filter, layout, appenderRefs, configuration, level, maxBytes, + flushOnErrorLog); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java similarity index 55% rename from powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java rename to powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java index 4e57a8e45..83cb91f34 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java @@ -12,19 +12,23 @@ * */ -package software.amazon.lambda.powertools.logging.log4.internal; +package software.amazon.lambda.powertools.logging.log4j.internal; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configurator; import org.slf4j.Logger; + +import software.amazon.lambda.powertools.logging.internal.BufferManager; import software.amazon.lambda.powertools.logging.internal.LoggingManager; +import software.amazon.lambda.powertools.logging.log4j.BufferingAppender; /** - * LoggingManager for Log4j2 (see {@link LoggingManager}). + * LoggingManager for Log4j2 that provides log level management and buffer operations. + * Implements both {@link LoggingManager} and {@link BufferManager} interfaces. */ -public class Log4jLoggingManager implements LoggingManager { +public class Log4jLoggingManager implements LoggingManager, BufferManager { /** * @inheritDoc @@ -45,4 +49,30 @@ public org.slf4j.event.Level getLogLevel(Logger logger) { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); return org.slf4j.event.Level.valueOf(ctx.getLogger(logger.getName()).getLevel().toString()); } + + /** + * @inheritDoc + */ + @Override + public void flushBuffer() { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() + .getAppender("BufferedAppender"); + if (bufferingAppender != null) { + bufferingAppender.flushBuffer(); + } + } + + /** + * @inheritDoc + */ + @Override + public void clearBuffer() { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() + .getAppender("BufferedAppender"); + if (bufferingAppender != null) { + bufferingAppender.clearBuffer(); + } + } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager index d444c5525..d4b2a72a0 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -1 +1 @@ -software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager \ No newline at end of file +software.amazon.lambda.powertools.logging.log4j.internal.Log4jLoggingManager diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java index 1fc235ff7..0d95f29fa 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java @@ -34,7 +34,7 @@ import software.amazon.lambda.powertools.utilities.JsonConfig; public class PowertoolsArguments implements RequestHandler { - private final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); + private static final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); private final ArgumentFormat argumentFormat; public PowertoolsArguments(ArgumentFormat argumentFormat) { diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java index e8c0c5851..0ee7f14fa 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java @@ -14,15 +14,17 @@ package software.amazon.lambda.powertools.logging.internal.handler; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEnabled implements RequestHandler { - private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + private static final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); @Override @Logging(clearState = true) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java similarity index 82% rename from powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java rename to powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index 69e1ee710..53e012d13 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -1,4 +1,4 @@ -package software.amazon.lambda.powertools.logging.internal; +package software.amazon.lambda.powertools.logging.log4j.internal; import static org.assertj.core.api.Assertions.assertThat; import static org.slf4j.event.Level.DEBUG; @@ -10,12 +10,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; -import software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager; class Log4jLoggingManagerTest { - private final static Logger LOG = LoggerFactory.getLogger(Log4jLoggingManagerTest.class); - private final static Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + private static final Logger LOG = LoggerFactory.getLogger(Log4jLoggingManagerTest.class); + private static final Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); @Test @Order(1) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogging.java new file mode 100644 index 000000000..1276c2a87 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogging.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging; + +import software.amazon.lambda.powertools.logging.internal.BufferManager; +import software.amazon.lambda.powertools.logging.internal.LoggingManager; +import software.amazon.lambda.powertools.logging.internal.LoggingManagerRegistry; + +/** + * PowertoolsLogging provides a backend-independent API for log buffering operations. + * This class abstracts away the underlying logging framework (Log4j2, Logback) and + * provides a unified interface for buffer management. + */ +public final class PowertoolsLogging { + + private PowertoolsLogging() { + // Utility class + } + + /** + * Flushes the log buffer for the current Lambda execution. + * This method will flush any buffered logs to the output stream. + * The operation is backend-independent and works with both Log4j2 and Logback. + */ + public static void flushBuffer() { + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof BufferManager) { + ((BufferManager) loggingManager).flushBuffer(); + } + } + + /** + * Clears the log buffer for the current Lambda execution. + * This method will discard any buffered logs without outputting them. + * The operation is backend-independent and works with both Log4j2 and Logback. + */ + public static void clearBuffer() { + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof BufferManager) { + ((BufferManager) loggingManager).clearBuffer(); + } + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/BufferManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/BufferManager.java new file mode 100644 index 000000000..d81d1fd31 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/BufferManager.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +/** + * Interface for logging managers that support buffer operations. + * This extends the logging framework capabilities with buffer-specific functionality. + */ +public interface BufferManager { + /** + * Flushes the log buffer for the current Lambda execution. + * This method will flush any buffered logs to the target appender. + */ + void flushBuffer(); + + /** + * Clears the log buffer for the current Lambda execution. + * This method will discard any buffered logs without outputting them. + */ + void clearBuffer(); +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLoggingManager.java similarity index 94% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLoggingManager.java index 5326f53e6..ed2c14c38 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLoggingManager.java @@ -21,7 +21,7 @@ * When no LoggingManager is found, setting a default one with no action on logging implementation * Powertools cannot change the log level based on the environment variable, will use the logger configuration */ -public class DefautlLoggingManager implements LoggingManager { +public class DefaultLoggingManager implements LoggingManager { @Override public void setLogLevel(Level logLevel) { 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 eccfdae4f..ceff74d8a 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 @@ -35,9 +35,6 @@ import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; -import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.databind.JsonNode; -import io.burt.jmespath.Expression; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -45,14 +42,9 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Random; -import java.util.ServiceLoader; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -63,10 +55,14 @@ import org.slf4j.MDC; import org.slf4j.MarkerFactory; import org.slf4j.event.Level; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.JsonNode; + +import io.burt.jmespath.Expression; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.utilities.JsonConfig; - @Aspect @DeclarePrecedence("*, software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect") public final class LambdaLoggingAspect { @@ -77,7 +73,7 @@ public final class LambdaLoggingAspect { private static final LoggingManager LOGGING_MANAGER; static { - LOGGING_MANAGER = getLoggingManagerFromServiceLoader(); + LOGGING_MANAGER = LoggingManagerRegistry.getLoggingManager(); setLogLevel(); @@ -90,7 +86,8 @@ static void setLogLevel() { if (LAMBDA_LOG_LEVEL != null) { Level lambdaLevel = getLevelFromString(LAMBDA_LOG_LEVEL); if (powertoolsLevel.toInt() < lambdaLevel.toInt()) { - LOG.warn("Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them.", + LOG.warn( + "Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them.", POWERTOOLS_LOG_LEVEL, LAMBDA_LOG_LEVEL); } } @@ -113,58 +110,11 @@ private static Level getLevelFromString(String level) { return Level.INFO; } - /** - * Use {@link ServiceLoader} to lookup for a {@link LoggingManager}. - * A file software.amazon.lambda.powertools.logging.internal.LoggingManager must be created in - * META-INF/services/ folder with the appropriate implementation of the {@link LoggingManager} - * - * @return an instance of {@link LoggingManager} - * @throws IllegalStateException if no {@link LoggingManager} could be found - */ - @SuppressWarnings("java:S106") // S106: System.err is used rather than logger to make sure message is printed - private static LoggingManager getLoggingManagerFromServiceLoader() { - ServiceLoader loggingManagers; - SecurityManager securityManager = System.getSecurityManager(); - if (securityManager == null) { - loggingManagers = ServiceLoader.load(LoggingManager.class); - } else { - final PrivilegedAction> action = () -> ServiceLoader.load(LoggingManager.class); - loggingManagers = AccessController.doPrivileged(action); - } - - List loggingManagerList = new ArrayList<>(); - for (LoggingManager lm : loggingManagers) { - loggingManagerList.add(lm); - } - return getLoggingManager(loggingManagerList, System.err); - } - - static LoggingManager getLoggingManager(List loggingManagerList, PrintStream printStream) { - LoggingManager loggingManager; - if (loggingManagerList.isEmpty()) { - printStream.println("ERROR. No LoggingManager was found on the classpath"); - printStream.println("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored"); - printStream.println("ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); - loggingManager = new DefautlLoggingManager(); - } else { - if (loggingManagerList.size() > 1) { - printStream.println("WARN. Multiple LoggingManagers were found on the classpath"); - for (LoggingManager manager : loggingManagerList) { - printStream.println("WARN. Found LoggingManager: [" + manager + "]"); - } - printStream.println("WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies"); - printStream.println("WARN. Using the first LoggingManager found on the classpath: [" + loggingManagerList.get(0) + "]"); - } - loggingManager = loggingManagerList.get(0); - } - return loggingManager; - } - private static void setLogLevels(Level logLevel) { LOGGING_MANAGER.setLogLevel(logLevel); } - @SuppressWarnings({"EmptyMethod"}) + @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(logging)") public void callAt(Logging logging) { } @@ -174,7 +124,7 @@ public void callAt(Logging logging) { */ @Around(value = "callAt(logging) && execution(@Logging * *.*(..))", argNames = "pjp,logging") public Object around(ProceedingJoinPoint pjp, - Logging logging) throws Throwable { + Logging logging) throws Throwable { boolean isOnRequestHandler = placedOnRequestHandler(pjp); boolean isOnRequestStreamHandler = placedOnStreamHandler(pjp); @@ -189,7 +139,8 @@ public Object around(ProceedingJoinPoint pjp, Object[] proceedArgs = logEvent(pjp, logging, isOnRequestHandler, isOnRequestStreamHandler); if (!logging.correlationIdPath().isEmpty()) { - captureCorrelationId(logging.correlationIdPath(), proceedArgs, isOnRequestHandler, isOnRequestStreamHandler); + captureCorrelationId(logging.correlationIdPath(), proceedArgs, isOnRequestHandler, + isOnRequestStreamHandler); } // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: @@ -226,7 +177,7 @@ public Object around(ProceedingJoinPoint pjp, if (isOnRequestHandler) { logRequestHandlerResponse(pjp, lambdaFunctionResponse); } else if (isOnRequestStreamHandler && backupOutputStream != null) { - byte[] bytes = ((ByteArrayOutputStream)proceedArgs[1]).toByteArray(); + byte[] bytes = ((ByteArrayOutputStream) proceedArgs[1]).toByteArray(); logRequestStreamHandlerResponse(pjp, bytes); backupOutputStream.write(bytes); } @@ -236,7 +187,7 @@ public Object around(ProceedingJoinPoint pjp, } private Object[] logEvent(ProceedingJoinPoint pjp, Logging logging, - boolean isOnRequestHandler, boolean isOnRequestStreamHandler) { + boolean isOnRequestHandler, boolean isOnRequestStreamHandler) { Object[] proceedArgs = pjp.getArgs(); if (logging.logEvent() || POWERTOOLS_LOG_EVENT) { @@ -260,7 +211,7 @@ private void addLambdaContextToLoggingContext(ProceedingJoinPoint pjp) { } private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, - final Logging logging) { + final Logging logging) { double samplingRate = samplingRate(logging); if (isHandlerMethod(pjp)) { @@ -346,9 +297,9 @@ private void logRequestStreamHandlerResponse(final ProceedingJoinPoint pjp, fina } private void captureCorrelationId(final String correlationIdPath, - Object[] proceedArgs, - final boolean isOnRequestHandler, - final boolean isOnRequestStreamHandler) { + Object[] proceedArgs, + final boolean isOnRequestHandler, + final boolean isOnRequestStreamHandler) { if (isOnRequestHandler) { JsonNode jsonNode = JsonConfig.get().getObjectMapper().valueToTree(proceedArgs[0]); setCorrelationIdFromNode(correlationIdPath, jsonNode); @@ -377,7 +328,6 @@ private void setCorrelationIdFromNode(String correlationIdPath, JsonNode jsonNod } } - private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java new file mode 100644 index 000000000..5bcc6d382 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import java.io.PrintStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Thread-safe singleton registry for LoggingManager instances. + * Handles lazy loading and caching of the LoggingManager implementation. + */ +public final class LoggingManagerRegistry { + + private static final AtomicReference instance = new AtomicReference<>(); + + private LoggingManagerRegistry() { + // Utility class + } + + /** + * Gets the LoggingManager instance, loading it lazily on first access. + * + * @return the LoggingManager instance + */ + public static LoggingManager getLoggingManager() { + LoggingManager manager = instance.get(); + if (manager == null) { + synchronized (LoggingManagerRegistry.class) { + manager = instance.get(); + if (manager == null) { + manager = loadLoggingManager(); + instance.set(manager); + } + } + } + return manager; + } + + @SuppressWarnings("java:S106") // S106: System.err is used rather than logger to make sure message is printed + private static LoggingManager loadLoggingManager() { + ServiceLoader loggingManagers; + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager == null) { + loggingManagers = ServiceLoader.load(LoggingManager.class); + } else { + final PrivilegedAction> action = () -> ServiceLoader + .load(LoggingManager.class); + loggingManagers = AccessController.doPrivileged(action); + } + + List loggingManagerList = new ArrayList<>(); + for (LoggingManager lm : loggingManagers) { + loggingManagerList.add(lm); + } + return selectLoggingManager(loggingManagerList, System.err); + } + + static LoggingManager selectLoggingManager(List loggingManagerList, PrintStream printStream) { + LoggingManager loggingManager; + if (loggingManagerList.isEmpty()) { + printStream.println("ERROR. No LoggingManager was found on the classpath"); + printStream.println("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored"); + printStream.println( + "ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); + loggingManager = new DefaultLoggingManager(); + } else { + if (loggingManagerList.size() > 1) { + printStream.println("WARN. Multiple LoggingManagers were found on the classpath"); + for (LoggingManager manager : loggingManagerList) { + printStream.println("WARN. Found LoggingManager: [" + manager + "]"); + } + printStream.println( + "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies"); + printStream.println("WARN. Using the first LoggingManager found on the classpath: [" + + loggingManagerList.get(0) + "]"); + } + loggingManager = loggingManagerList.get(0); + } + return loggingManager; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java new file mode 100644 index 000000000..d63cae3bb --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import software.amazon.lambda.powertools.logging.internal.LoggingManagerRegistry; +import software.amazon.lambda.powertools.logging.internal.TestLoggingManager; + +class PowertoolsLoggingTest { + + private TestLoggingManager testManager; + + @BeforeEach + void setUp() { + // Get the TestLoggingManager instance from registry + testManager = (TestLoggingManager) LoggingManagerRegistry.getLoggingManager(); + testManager.resetBufferState(); + } + + @Test + void testFlushBuffer_shouldNotThrowException() { + // WHEN/THEN + assertThatCode(PowertoolsLogging::flushBuffer).doesNotThrowAnyException(); + } + + @Test + void testClearBuffer_shouldNotThrowException() { + // WHEN/THEN + assertThatCode(PowertoolsLogging::clearBuffer).doesNotThrowAnyException(); + } + + @Test + void testFlushBuffer_shouldCallBufferManager() { + // WHEN + PowertoolsLogging.flushBuffer(); + + // THEN + assertThat(testManager.isBufferFlushed()).isTrue(); + } + + @Test + void testClearBuffer_shouldCallBufferManager() { + // WHEN + PowertoolsLogging.clearBuffer(); + + // THEN + assertThat(testManager.isBufferCleared()).isTrue(); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index 751d195b5..5bf2a9d07 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java @@ -31,8 +31,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.channels.FileChannel; @@ -40,7 +38,6 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -720,50 +717,6 @@ void shouldLogCorrelationIdOnAppSyncEvent() throws IOException { .containsEntry("correlation_id", eventId); } - @Test - void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws UnsupportedEncodingException { - // GIVEN - List list = new ArrayList<>(); - list.add(new TestLoggingManager()); - list.add(new DefautlLoggingManager()); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - PrintStream stream = new PrintStream(outputStream); - - // WHEN - LambdaLoggingAspect.getLoggingManager(list, stream); - - // THEN - String output = outputStream.toString("UTF-8"); - assertThat(output) - .contains("WARN. Multiple LoggingManagers were found on the classpath") - .contains( - "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies") - .contains("WARN. Using the first LoggingManager found on the classpath: [" + list.get(0) + "]"); - } - - @Test - void testNoLoggingManagers_shouldWarnAndCreateDefault() throws UnsupportedEncodingException { - // GIVEN - List list = new ArrayList<>(); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - PrintStream stream = new PrintStream(outputStream); - - // WHEN - LoggingManager loggingManager = LambdaLoggingAspect.getLoggingManager(list, stream); - - // THEN - String output = outputStream.toString("UTF-8"); - assertThat(output) - .contains("ERROR. No LoggingManager was found on the classpath") - .contains("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored") - .contains( - "ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); - - assertThat(loggingManager).isExactlyInstanceOf(DefautlLoggingManager.class); - } - private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method setLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("setLogLevels", Level.class); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java new file mode 100644 index 000000000..4807870b9 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +class LoggingManagerRegistryTest { + + @Test + void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); + list.add(new TestLoggingManager()); + list.add(new DefaultLoggingManager()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); + + // WHEN + LoggingManagerRegistry.selectLoggingManager(list, stream); + + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("WARN. Multiple LoggingManagers were found on the classpath") + .contains( + "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies") + .contains("WARN. Using the first LoggingManager found on the classpath: [" + list.get(0) + "]"); + } + + @Test + void testNoLoggingManagers_shouldWarnAndCreateDefault() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); + + // WHEN + LoggingManager loggingManager = LoggingManagerRegistry.selectLoggingManager(list, stream); + + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("ERROR. No LoggingManager was found on the classpath") + .contains("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored") + .contains( + "ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); + + assertThat(loggingManager).isExactlyInstanceOf(DefaultLoggingManager.class); + } + + @Test + void testSingleLoggingManager_shouldReturnWithoutWarning() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); + TestLoggingManager testManager = new TestLoggingManager(); + list.add(testManager); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); + + // WHEN + LoggingManager loggingManager = LoggingManagerRegistry.selectLoggingManager(list, stream); + + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output).isEmpty(); + assertThat(loggingManager).isSameAs(testManager); + assertThat(loggingManager).isInstanceOf(BufferManager.class); + } + + @Test + void testGetLoggingManager_shouldReturnSameInstance() { + // WHEN + LoggingManager first = LoggingManagerRegistry.getLoggingManager(); + LoggingManager second = LoggingManagerRegistry.getLoggingManager(); + + // THEN + assertThat(first).isSameAs(second); + assertThat(first).isNotNull(); + assertThat(first).isInstanceOf(BufferManager.class); + } + + @Test + void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException { + // GIVEN + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicReference sharedInstance = new AtomicReference<>(); + + // WHEN + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + LoggingManager instance = LoggingManagerRegistry.getLoggingManager(); + sharedInstance.compareAndSet(null, instance); + assertThat(instance).isSameAs(sharedInstance.get()); + } finally { + latch.countDown(); + } + }); + } + + // THEN + latch.await(5, TimeUnit.SECONDS); + executor.shutdown(); + assertThat(sharedInstance.get()).isNotNull(); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java index 0958e0d3b..f2aa2417e 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java @@ -7,17 +7,19 @@ import org.slf4j.test.TestLogger; import org.slf4j.test.TestLoggerFactory; -public class TestLoggingManager implements LoggingManager { +public class TestLoggingManager implements LoggingManager, BufferManager { private final TestLoggerFactory loggerFactory; + private boolean bufferFlushed = false; + private boolean bufferCleared = false; public TestLoggingManager() { - ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); - if (!(loggerFactory instanceof TestLoggerFactory)) { + ILoggerFactory loggerFactoryInstance = LoggerFactory.getILoggerFactory(); + if (!(loggerFactoryInstance instanceof TestLoggerFactory)) { throw new RuntimeException( "LoggerFactory does not match required type: " + TestLoggerFactory.class.getName()); } - this.loggerFactory = (TestLoggerFactory) loggerFactory; + this.loggerFactory = (TestLoggerFactory) loggerFactoryInstance; } @Override @@ -29,4 +31,27 @@ public void setLogLevel(Level logLevel) { public Level getLogLevel(Logger logger) { return org.slf4j.event.Level.intToLevel(((TestLogger) logger).getLogLevel()); } + + @Override + public void flushBuffer() { + bufferFlushed = true; + } + + @Override + public void clearBuffer() { + bufferCleared = true; + } + + public boolean isBufferFlushed() { + return bufferFlushed; + } + + public boolean isBufferCleared() { + return bufferCleared; + } + + public void resetBufferState() { + bufferFlushed = false; + bufferCleared = false; + } } From 6dde0a52e4b2d4ad86e7d6e0a1e297d2c3062541 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 29 Aug 2025 14:56:08 +0200 Subject: [PATCH 02/45] Add buffer cleaning to avoid memory leak and flushBufferOnUncaughtError option to Logging aspect. --- .../lambda/powertools/logging/Logging.java | 6 + .../logging/internal/LambdaLoggingAspect.java | 13 ++ .../handlers/PowertoolsLogErrorNoFlush.java | 15 ++ .../internal/LambdaLoggingAspectTest.java | 134 ++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogErrorNoFlush.java diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java index 9e5e735d1..a0685a256 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java @@ -102,4 +102,10 @@ * Set this attribute to true if you want all custom keys to be deleted on each request. */ boolean clearState() default false; + + /** + * Set to true if you want to flush the log buffer when an uncaught exception occurs. + * This ensures that buffered logs are output when errors happen. + */ + boolean flushBufferOnUncaughtError() default true; } 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 ceff74d8a..b9e01da92 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 @@ -160,6 +160,15 @@ public Object around(ProceedingJoinPoint pjp, // Call Function Handler lambdaFunctionResponse = pjp.proceed(proceedArgs); } catch (Throwable t) { + if (LOGGING_MANAGER instanceof BufferManager) { + if (logging.flushBufferOnUncaughtError()) { + ((BufferManager) LOGGING_MANAGER).flushBuffer(); + } else { + // Clear buffer before error logging to prevent unintended flush. If flushOnErrorLog is enabled on + // the appender the next line would otherwise cause an unintended flush by the appender directly. + ((BufferManager) LOGGING_MANAGER).clearBuffer(); + } + } if (logging.logError() || POWERTOOLS_LOG_ERROR) { // logging the exception with additional context logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t); @@ -169,6 +178,10 @@ public Object around(ProceedingJoinPoint pjp, if (logging.clearState()) { MDC.clear(); } + // Clear buffer after each handler invocation + if (LOGGING_MANAGER instanceof BufferManager) { + ((BufferManager) LOGGING_MANAGER).clearBuffer(); + } coldStartDone(); } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogErrorNoFlush.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogErrorNoFlush.java new file mode 100644 index 000000000..87515654c --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogErrorNoFlush.java @@ -0,0 +1,15 @@ +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogErrorNoFlush implements RequestHandler { + + @Override + @Logging(logError = true, flushBufferOnUncaughtError = false) + public String handleRequest(String input, Context context) { + throw new RuntimeException("This is an error without buffer flush"); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index 5bf2a9d07..3ff531321 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java @@ -78,6 +78,7 @@ import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogErrorNoFlush; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar; @@ -108,6 +109,13 @@ void setUp() throws IllegalAccessException, NoSuchMethodException, InvocationTar writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", null, true); writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_EVENT", false, true); writeStaticField(LoggingConstants.class, "POWERTOOLS_SAMPLING_RATE", null, true); + + // Reset buffer state for clean test isolation + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + ((TestLoggingManager) loggingManager).resetBufferState(); + } + try { FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); } catch (NoSuchFileException e) { @@ -717,6 +725,132 @@ void shouldLogCorrelationIdOnAppSyncEvent() throws IOException { .containsEntry("correlation_id", eventId); } + @Test + void shouldClearBufferAfterSuccessfulHandlerExecution() { + // WHEN + requestHandler.handleRequest(new Object(), context); + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue(); + } + } + + @Test + void shouldClearBufferAfterSuccessfulStreamHandlerExecution() throws IOException { + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), + context); + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue(); + } + } + + @Test + void shouldClearBufferAfterHandlerExceptionWithLogError() { + // GIVEN + requestHandler = new PowertoolsLogError(); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue(); + } + } + + @Test + void shouldClearBufferAfterHandlerExceptionWithEnvVarLogError() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_ERROR = true; + requestHandler = new PowertoolsLogEnabled(true); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue(); + } + } finally { + LoggingConstants.POWERTOOLS_LOG_ERROR = false; + } + } + + @Test + void shouldFlushBufferOnUncaughtErrorWhenEnabled() { + // GIVEN + requestHandler = new PowertoolsLogError(); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferFlushed()).isTrue(); + } + } + + @Test + void shouldNotFlushBufferOnUncaughtErrorWhenDisabled() { + // GIVEN + PowertoolsLogErrorNoFlush handler = new PowertoolsLogErrorNoFlush(); + + // WHEN + try { + handler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferFlushed()).isFalse(); + } + } + + @Test + void shouldClearBufferBeforeErrorLoggingWhenFlushBufferOnUncaughtErrorDisabled() { + // GIVEN + PowertoolsLogErrorNoFlush handler = new PowertoolsLogErrorNoFlush(); + + // WHEN + try { + handler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN - Buffer should be cleared and not flushed + LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager(); + if (loggingManager instanceof TestLoggingManager) { + assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue(); + assertThat(((TestLoggingManager) loggingManager).isBufferFlushed()).isFalse(); + } + } + private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method setLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("setLogLevels", Level.class); From 4ff36d93b2645b520d359dc7bcf26abc757942cf Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 29 Aug 2025 16:09:24 +0200 Subject: [PATCH 03/45] Call appenders with logevent directly from BufferingAppender. --- .../logging/log4j/BufferingAppender.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index 52f7ff511..6f21aebed 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -35,6 +35,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.message.SimpleMessage; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; @@ -51,7 +52,7 @@ public class BufferingAppender extends AbstractAppender { private final int maxBytes; private final boolean flushOnErrorLog; private final Map> bufferCache = new ConcurrentHashMap<>(); - private final ThreadLocal bufferOverflowWarned = new ThreadLocal<>(); + private final ThreadLocal bufferOverflowTriggered = new ThreadLocal<>(); protected BufferingAppender(String name, Filter filter, Layout layout, AppenderRef[] appenderRefs, Configuration configuration, Level bufferAtVerbosity, int maxBytes, @@ -107,8 +108,8 @@ private void bufferEvent(String traceId, LogEvent event) { // Check if single event is larger than buffer - discard if so int eventSize = immutableEvent.getMessage().getFormattedMessage().length(); if (eventSize > maxBytes) { - if (Boolean.TRUE != bufferOverflowWarned.get()) { - bufferOverflowWarned.set(true); + if (Boolean.TRUE != bufferOverflowTriggered.get()) { + bufferOverflowTriggered.set(true); } return; } @@ -118,8 +119,8 @@ private void bufferEvent(String traceId, LogEvent event) { // Simple size check - remove oldest if over limit Deque buffer = bufferCache.get(traceId); while (getBufferSize(buffer) > maxBytes && !buffer.isEmpty()) { - if (Boolean.TRUE != bufferOverflowWarned.get()) { - bufferOverflowWarned.set(true); + if (Boolean.TRUE != bufferOverflowTriggered.get()) { + bufferOverflowTriggered.set(true); } buffer.removeFirst(); } @@ -140,13 +141,22 @@ public void flushBuffer() { } private void flushBuffer(String traceId) { + // Emit buffer overflow warning if it occurred + if (Boolean.TRUE == bufferOverflowTriggered.get()) { + // Create LogEvent directly since Log4j status logger may not reach target appenders + LogEvent warningEvent = Log4jLogEvent.newBuilder() + .setLoggerName("BufferingAppender") + .setLevel(Level.WARN) + .setMessage(new SimpleMessage( + "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.")) + .setTimeMillis(System.currentTimeMillis()) + .build(); + callAppenders(warningEvent); + bufferOverflowTriggered.remove(); + } + Deque buffer = bufferCache.remove(traceId); if (buffer != null) { - // Emit buffer overflow warning if it occurred - if (Boolean.TRUE == bufferOverflowWarned.get()) { - LOGGER.warn("Buffer size exceeded for trace ID: {}. Some log events were discarded.", traceId); - bufferOverflowWarned.remove(); - } buffer.forEach(this::callAppenders); } } From 966e5766f42dafd28a5465fc01b7e0c0aa32e906 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 29 Aug 2025 17:47:01 +0200 Subject: [PATCH 04/45] Extract buffer management logic into KeyBuffer class and keep log4j appender minimal. --- .../logging/log4j/BufferingAppender.java | 113 ++++--- .../log4j/BufferingAppenderConstants.java | 30 ++ .../log4j/internal/Log4jLoggingManager.java | 6 +- .../logging/internal/KeyBuffer.java | 114 +++++++ .../logging/internal/KeyBufferTest.java | 309 ++++++++++++++++++ 5 files changed, 512 insertions(+), 60 deletions(-) create mode 100644 powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index 6f21aebed..43d09987f 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -15,10 +15,7 @@ package software.amazon.lambda.powertools.logging.log4j; import java.io.Serializable; -import java.util.ArrayDeque; import java.util.Deque; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Appender; @@ -35,24 +32,64 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.message.SimpleMessage; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.BufferManager; +import software.amazon.lambda.powertools.logging.internal.KeyBuffer; + +import static software.amazon.lambda.powertools.logging.log4j.BufferingAppenderConstants.NAME; /** - * A minimalistic Log4j2 appender that buffers log events based on trace ID - * and flushes them when error logs are encountered or manually triggered. + * A Log4j2 appender that buffers log events by AWS X-Ray trace ID for optimized Lambda logging. + * + *

This appender is designed specifically for AWS Lambda functions to reduce log ingestion + * by buffering lower-level logs and only outputting them when errors occur, preserving + * full context for troubleshooting while minimizing routine log volume. + * + *

Key Features:

+ *
    + *
  • Trace-based buffering: Groups logs by AWS X-Ray trace ID
  • + *
  • Selective output: Only buffers logs at or below configured verbosity level
  • + *
  • Auto-flush on errors: Automatically outputs buffered logs when ERROR/FATAL events occur
  • + *
  • Memory management: Prevents memory leaks with configurable buffer size limits
  • + *
  • Overflow protection: Warns when logs are discarded due to buffer limits
  • + *
+ * + *

Configuration Example:

+ *
{@code
+ * 
+ *   
+ * 
+ * }
+ * + *

Configuration Parameters:

+ *
    + *
  • bufferAtVerbosity: Log level to buffer (default: DEBUG). Logs at this level and below are buffered
  • + *
  • maxBytes: Maximum buffer size in bytes per trace ID (default: 20480)
  • + *
  • flushOnErrorLog: Whether to flush buffer on ERROR/FATAL logs (default: true)
  • + *
+ * + *

Behavior:

+ *
    + *
  • During Lambda INIT phase (no trace ID): logs are output directly
  • + *
  • During Lambda execution (with trace ID): logs are buffered or output based on level
  • + *
  • When buffer overflows: oldest logs are discarded and a warning is logged
  • + *
  • On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}
  • + *
+ * + * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushLogBuffer() */ -@Plugin(name = "BufferingAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) -public class BufferingAppender extends AbstractAppender { +@Plugin(name = NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class BufferingAppender extends AbstractAppender implements BufferManager { private final AppenderRef[] appenderRefs; private final Configuration configuration; private final Level bufferAtVerbosity; - private final int maxBytes; private final boolean flushOnErrorLog; - private final Map> bufferCache = new ConcurrentHashMap<>(); - private final ThreadLocal bufferOverflowTriggered = new ThreadLocal<>(); + private final KeyBuffer buffer; protected BufferingAppender(String name, Filter filter, Layout layout, AppenderRef[] appenderRefs, Configuration configuration, Level bufferAtVerbosity, int maxBytes, @@ -61,8 +98,8 @@ protected BufferingAppender(String name, Filter filter, Layout(maxBytes, event -> event.getMessage().getFormattedMessage().length()); } @Override @@ -70,9 +107,9 @@ public void append(LogEvent event) { if (appenderRefs == null || appenderRefs.length == 0) { return; } + LambdaHandlerProcessor.getXrayTraceId().ifPresentOrElse( traceId -> { - // Check if we should buffer this log level if (shouldBuffer(event.getLevel())) { bufferEvent(traceId, event); } else { @@ -102,38 +139,12 @@ private boolean shouldBuffer(Level level) { } private void bufferEvent(String traceId, LogEvent event) { - // Create immutable copy to prevent mutation LogEvent immutableEvent = Log4jLogEvent.createMemento(event); - - // Check if single event is larger than buffer - discard if so - int eventSize = immutableEvent.getMessage().getFormattedMessage().length(); - if (eventSize > maxBytes) { - if (Boolean.TRUE != bufferOverflowTriggered.get()) { - bufferOverflowTriggered.set(true); - } - return; - } - - bufferCache.computeIfAbsent(traceId, k -> new ArrayDeque<>()).add(immutableEvent); - - // Simple size check - remove oldest if over limit - Deque buffer = bufferCache.get(traceId); - while (getBufferSize(buffer) > maxBytes && !buffer.isEmpty()) { - if (Boolean.TRUE != bufferOverflowTriggered.get()) { - bufferOverflowTriggered.set(true); - } - buffer.removeFirst(); - } - } - - private int getBufferSize(Deque buffer) { - return buffer.stream() - .mapToInt(event -> event.getMessage().getFormattedMessage().length()) - .sum(); + buffer.add(traceId, immutableEvent); } public void clearBuffer() { - LambdaHandlerProcessor.getXrayTraceId().ifPresent(bufferCache::remove); + LambdaHandlerProcessor.getXrayTraceId().ifPresent(buffer::clear); } public void flushBuffer() { @@ -141,23 +152,9 @@ public void flushBuffer() { } private void flushBuffer(String traceId) { - // Emit buffer overflow warning if it occurred - if (Boolean.TRUE == bufferOverflowTriggered.get()) { - // Create LogEvent directly since Log4j status logger may not reach target appenders - LogEvent warningEvent = Log4jLogEvent.newBuilder() - .setLoggerName("BufferingAppender") - .setLevel(Level.WARN) - .setMessage(new SimpleMessage( - "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.")) - .setTimeMillis(System.currentTimeMillis()) - .build(); - callAppenders(warningEvent); - bufferOverflowTriggered.remove(); - } - - Deque buffer = bufferCache.remove(traceId); - if (buffer != null) { - buffer.forEach(this::callAppenders); + Deque events = buffer.removeAll(traceId); + if (events != null) { + events.forEach(this::callAppenders); } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java new file mode 100644 index 000000000..820d82b15 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.log4j; + +/** + * Constants for BufferingAppender configuration and references. + */ +public final class BufferingAppenderConstants { + + /** + * The name used for BufferingAppender in Log4j2 configuration and references. + */ + public static final String NAME = "BufferingAppender"; + + private BufferingAppenderConstants() { + // Utility class + } +} \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java index 83cb91f34..3f2d695f3 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java @@ -24,6 +24,8 @@ import software.amazon.lambda.powertools.logging.internal.LoggingManager; import software.amazon.lambda.powertools.logging.log4j.BufferingAppender; +import static software.amazon.lambda.powertools.logging.log4j.BufferingAppenderConstants.NAME; + /** * LoggingManager for Log4j2 that provides log level management and buffer operations. * Implements both {@link LoggingManager} and {@link BufferManager} interfaces. @@ -57,7 +59,7 @@ public org.slf4j.event.Level getLogLevel(Logger logger) { public void flushBuffer() { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender("BufferedAppender"); + .getAppender(NAME); if (bufferingAppender != null) { bufferingAppender.flushBuffer(); } @@ -70,7 +72,7 @@ public void flushBuffer() { public void clearBuffer() { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender("BufferedAppender"); + .getAppender(NAME); if (bufferingAppender != null) { bufferingAppender.clearBuffer(); } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java new file mode 100644 index 000000000..c8d5419e2 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Generic buffer data structure for storing events by key with size-based eviction. + * + *

This buffer maintains separate event queues for each key, with configurable size limits + * to prevent memory exhaustion. When buffers exceed their size limit, older events are + * automatically evicted to make room for newer ones. + * + *

Key Features:

+ *
    + *
  • Per-key buffering: Each key maintains its own independent buffer
  • + *
  • Size-based eviction: Oldest events are removed when buffer size exceeds limit
  • + *
  • Overflow protection: Events larger than buffer size are rejected entirely
  • + *
  • Thread-safe: Supports concurrent access across different keys
  • + *
  • Overflow tracking: Logs warnings when events are evicted or rejected
  • + *
+ * + *

Eviction Behavior:

+ *
    + *
  • Buffer overflow: When adding an event would exceed maxBytes, oldest events are evicted first
  • + *
  • Large events: Events larger than maxBytes are rejected without evicting existing events
  • + *
  • FIFO eviction: Events are removed in first-in-first-out order during overflow
  • + *
  • Overflow warnings: Automatic logging when events are evicted or rejected
  • + *
+ * + *

Thread Safety:

+ *

This class is thread-safe for concurrent operations. Different keys can be accessed + * simultaneously, and operations on the same key are synchronized to prevent data corruption. + * + * @param the type of key used for buffering (e.g., String for trace IDs) + * @param the type of events to buffer (must be compatible with the size calculator) + */ +public class KeyBuffer { + + private static final Logger logger = LoggerFactory.getLogger(KeyBuffer.class); + + private final Map> keyBufferCache = new ConcurrentHashMap<>(); + private final Map overflowTriggered = new ConcurrentHashMap<>(); + private final int maxBytes; + private final Function sizeCalculator; + + public KeyBuffer(int maxBytes, Function sizeCalculator) { + this.maxBytes = maxBytes; + this.sizeCalculator = sizeCalculator; + } + + public void add(K key, T event) { + int eventSize = sizeCalculator.apply(event); + if (eventSize > maxBytes) { + overflowTriggered.put(key, true); + return; + } + + Deque buffer = keyBufferCache.computeIfAbsent(key, k -> new ArrayDeque<>()); + synchronized (buffer) { + buffer.add(event); + while (getBufferSize(buffer) > maxBytes && !buffer.isEmpty()) { + overflowTriggered.put(key, true); + buffer.removeFirst(); + } + } + } + + public Deque removeAll(K key) { + logOverflowWarningIfNeeded(key); + Deque buffer = keyBufferCache.remove(key); + if (buffer != null) { + synchronized (buffer) { + return new ArrayDeque<>(buffer); + } + } + return buffer; + } + + public void clear(K key) { + keyBufferCache.remove(key); + overflowTriggered.remove(key); + } + + private void logOverflowWarningIfNeeded(K key) { + if (Boolean.TRUE.equals(overflowTriggered.remove(key))) { + logger.warn( + "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer."); + } + } + + private int getBufferSize(Deque buffer) { + return buffer.stream().mapToInt(sizeCalculator::apply).sum(); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java new file mode 100644 index 000000000..20b4522b8 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Deque; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class KeyBufferTest { + + private KeyBuffer buffer; + private static final int MAX_BYTES = 20; + + @BeforeEach + void setUp() throws IOException { + buffer = new KeyBuffer<>(MAX_BYTES, String::length); + // Clean up log file before each test + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (IOException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { + // Make sure file is cleaned up after each test + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldAddEventToBuffer() { + buffer.add("key1", "test"); + + Deque events = buffer.removeAll("key1"); + assertThat(events).containsExactly("test"); + } + + @Test + void shouldMaintainSeparateBuffersPerKey() { + buffer.add("key1", "event1"); + buffer.add("key2", "event2"); + + Deque events1 = buffer.removeAll("key1"); + Deque events2 = buffer.removeAll("key2"); + + assertThat(events1).containsExactly("event1"); + assertThat(events2).containsExactly("event2"); + } + + @Test + void shouldMaintainFIFOOrder() { + buffer.add("key1", "first"); + buffer.add("key1", "second"); + buffer.add("key1", "third"); + + Deque events = buffer.removeAll("key1"); + assertThat(events).containsExactly("first", "second", "third"); + } + + @Test + void shouldEvictOldestEventsWhenBufferOverflows() { + // Add events that total exactly maxBytes + buffer.add("key1", "12345678901234567890"); // 20 bytes + + // Add another event that causes overflow + buffer.add("key1", "extra"); + + Deque events = buffer.removeAll("key1"); + assertThat(events).containsExactly("extra"); + } + + @Test + void shouldEvictMultipleEventsIfNeeded() { + buffer.add("key1", "1234567890"); // 10 bytes + buffer.add("key1", "1234567890"); // 10 bytes, total 20 + buffer.add("key1", "12345678901234567890"); // 20 bytes, should evict both previous + + Deque events = buffer.removeAll("key1"); + assertThat(events).containsExactly("12345678901234567890"); + } + + @Test + void shouldEvictMultipleSmallEventsForLargeValidEvent() { + // Add many small events that fill the buffer + buffer.add("key1", "12"); // 2 bytes + buffer.add("key1", "34"); // 2 bytes, total 4 + buffer.add("key1", "56"); // 2 bytes, total 6 + buffer.add("key1", "78"); // 2 bytes, total 8 + buffer.add("key1", "90"); // 2 bytes, total 10 + buffer.add("key1", "ab"); // 2 bytes, total 12 + buffer.add("key1", "cd"); // 2 bytes, total 14 + buffer.add("key1", "ef"); // 2 bytes, total 16 + buffer.add("key1", "gh"); // 2 bytes, total 18 + buffer.add("key1", "ij"); // 2 bytes, total 20 (exactly at limit) + + // Add a large event that requires multiple evictions + buffer.add("key1", "123456789012345678"); // 18 bytes, should evict multiple small events + + Deque events = buffer.removeAll("key1"); + // Should only contain the last few small events plus the large event + // 18 bytes for large event leaves 2 bytes, so only "ij" should remain with the large event + assertThat(events).containsExactly("ij", "123456789012345678"); + } + + @Test + void shouldRejectEventLargerThanMaxBytes() { + String largeEvent = "123456789012345678901"; // 21 bytes > 20 max + + buffer.add("key1", largeEvent); + + Deque events = buffer.removeAll("key1"); + assertThat(events).isNull(); + } + + @Test + void shouldNotEvictExistingEventsWhenRejectingLargeEvent() { + buffer.add("key1", "small"); + + String largeEvent = "123456789012345678901"; // 21 bytes > 20 max + buffer.add("key1", largeEvent); + + Deque events = buffer.removeAll("key1"); + assertThat(events).containsExactly("small"); + } + + @Test + void shouldClearSpecificKeyBuffer() { + buffer.add("key1", "event1"); + buffer.add("key2", "event2"); + + buffer.clear("key1"); + + assertThat(buffer.removeAll("key1")).isNull(); + assertThat(buffer.removeAll("key2")).containsExactly("event2"); + } + + @Test + void shouldReturnNullForNonExistentKey() { + Deque events = buffer.removeAll("nonexistent"); + assertThat(events).isNull(); + } + + @Test + void shouldReturnDefensiveCopyOnRemoveAll() { + buffer.add("key1", "event"); + + Deque events1 = buffer.removeAll("key1"); + buffer.add("key1", "event"); + Deque events2 = buffer.removeAll("key1"); + + // Modifying first copy shouldn't affect second + events1.add("modified"); + assertThat(events2).containsExactly("event"); + assertThat(events1).containsExactly("event", "modified"); + } + + @Test + void shouldLogWarningOnOverflow() { + KeyBuffer testBuffer = new KeyBuffer<>(10, String::length); + + // Cause overflow + testBuffer.add("key1", "1234567890"); // 10 bytes + testBuffer.add("key1", "extra"); // causes overflow + + // Trigger warning by removing + testBuffer.removeAll("key1"); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Some logs are not displayed because they were evicted from the buffer"); + } + + @Test + void shouldLogWarningOnLargeEventRejection() { + KeyBuffer testBuffer = new KeyBuffer<>(10, String::length); + + // Add large event that gets rejected + testBuffer.add("key1", "12345678901"); // 11 bytes > 10 max + + // Trigger warning by removing + testBuffer.removeAll("key1"); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Some logs are not displayed because they were evicted from the buffer"); + } + + @Test + void shouldNotLogWarningWhenNoOverflow() { + KeyBuffer testBuffer = new KeyBuffer<>(20, String::length); + + testBuffer.add("key1", "small"); + testBuffer.removeAll("key1"); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .doesNotContain("Some logs are not displayed because they were evicted from the buffer"); + } + + @Test + void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { + int threadCount = 10; + int eventsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // Each thread works with different key + for (int i = 0; i < threadCount; i++) { + final String key = "key" + i; + executor.submit(() -> { + try { + for (int j = 0; j < eventsPerThread; j++) { + buffer.add(key, "event" + j); + } + } finally { + latch.countDown(); + } + }); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Verify each key has its events + for (int i = 0; i < threadCount; i++) { + String key = "key" + i; + Deque events = buffer.removeAll(key); + assertThat(events).isNotNull(); + // Some events might be evicted due to size limits, but should have some + assertThat(events).isNotEmpty(); + } + + executor.shutdown(); + } + + @Test + void shouldBeThreadSafeForSameKey() throws InterruptedException { + int threadCount = 5; + int eventsPerThread = 20; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // All threads work with same key + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + for (int j = 0; j < eventsPerThread; j++) { + buffer.add("sharedKey", "t" + threadId + "e" + j); + } + } finally { + latch.countDown(); + } + }); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + + Deque events = buffer.removeAll("sharedKey"); + assertThat(events).isNotNull(); + // Due to size limits, not all events will be present, but buffer should be consistent + assertThat(events).isNotEmpty(); + + executor.shutdown(); + } + + @Test + void shouldHandleEmptyBuffer() { + buffer.clear("nonexistent"); + assertThat(buffer.removeAll("nonexistent")).isNull(); + } + + @Test + void shouldHandleZeroSizeEvents() { + KeyBuffer zeroBuffer = new KeyBuffer<>(10, s -> 0); + + zeroBuffer.add("key1", "event1"); + zeroBuffer.add("key1", "event2"); + + Deque events = zeroBuffer.removeAll("key1"); + assertThat(events).containsExactly("event1", "event2"); + } +} From dd8473d21d1897c77b69645ef5dc4470d0564e50 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 29 Aug 2025 18:05:27 +0200 Subject: [PATCH 05/45] Unit tests for BufferingAppender --- .../sam/src/main/resources/log4j2.xml | 2 +- .../logging/log4j/BufferingAppenderTest.java | 117 ++++++++++++++++++ .../src/test/resources/log4j2.xml | 11 +- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java diff --git a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml index e140022e4..f60db0fb5 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml @@ -4,7 +4,7 @@ - + diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java new file mode 100644 index 000000000..d42a7b9db --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java @@ -0,0 +1,117 @@ +package software.amazon.lambda.powertools.logging.log4j; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; + +class BufferingAppenderTest { + + private Logger logger; + + @BeforeEach + void setUp() throws IOException { + logger = LogManager.getLogger(BufferingAppenderTest.class); + + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldBufferDebugLogsAndFlushOnError() { + // When - log debug messages (should be buffered) + logger.debug("Debug message 1"); + logger.debug("Debug message 2"); + + // Then - no logs written yet + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).isEmpty(); + + // When - log error (should flush buffer) + logger.error("Error message"); + + // Then - all logs written + assertThat(contentOf(logFile)) + .contains("Debug message 1") + .contains("Debug message 2") + .contains("Error message"); + } + + @Test + @ClearEnvironmentVariable(key = "_X_AMZN_TRACE_ID") + void shouldLogDirectlyWhenNoTraceId() { + // When + logger.debug("Debug without trace"); + + // Then - log written directly + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Debug without trace"); + } + + @Test + void shouldNotBufferInfoLogs() { + // When - log info message (above buffer level) + logger.info("Info message"); + + // Then - log written directly + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Info message"); + } + + @Test + void shouldFlushBufferManually() { + // When - buffer debug logs + logger.debug("Buffered message"); + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).isEmpty(); + + // When - manual flush + BufferingAppender appender = getBufferingAppender(); + appender.flushBuffer(); + + // Then - logs written + assertThat(contentOf(logFile)).contains("Buffered message"); + } + + @Test + void shouldClearBufferManually() { + // When - buffer debug logs then clear + logger.debug("Buffered message"); + BufferingAppender appender = getBufferingAppender(); + appender.clearBuffer(); + + // When - log error (should not flush cleared buffer) + logger.error("Error after clear"); + + // Then - only error logged + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Error after clear") + .doesNotContain("Buffered message"); + } + + private BufferingAppender getBufferingAppender() { + return (BufferingAppender) ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)) + .getConfiguration().getAppender(BufferingAppenderConstants.NAME); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml index 778077bc5..870e3a803 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml @@ -10,8 +10,17 @@ + + + - q + + + + From fb3dcc0dcc812b21aa90b8aa204a0bec61c588b2 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 11:07:44 +0200 Subject: [PATCH 06/45] Rename to Log4jConstants. --- .../powertools/logging/log4j/BufferingAppender.java | 6 +++--- ...ingAppenderConstants.java => Log4jConstants.java} | 12 ++++++------ .../logging/log4j/internal/Log4jLoggingManager.java | 8 ++++---- .../logging/log4j/BufferingAppenderTest.java | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) rename powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/{BufferingAppenderConstants.java => Log4jConstants.java} (70%) diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index 43d09987f..500c11a4d 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -14,6 +14,8 @@ package software.amazon.lambda.powertools.logging.log4j; +import static software.amazon.lambda.powertools.logging.log4j.Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME; + import java.io.Serializable; import java.util.Deque; @@ -37,8 +39,6 @@ import software.amazon.lambda.powertools.logging.internal.BufferManager; import software.amazon.lambda.powertools.logging.internal.KeyBuffer; -import static software.amazon.lambda.powertools.logging.log4j.BufferingAppenderConstants.NAME; - /** * A Log4j2 appender that buffers log events by AWS X-Ray trace ID for optimized Lambda logging. * @@ -82,7 +82,7 @@ * * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushLogBuffer() */ -@Plugin(name = NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +@Plugin(name = BUFFERING_APPENDER_PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class BufferingAppender extends AbstractAppender implements BufferManager { private final AppenderRef[] appenderRefs; diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java similarity index 70% rename from powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java rename to powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java index 820d82b15..5d43eb3dc 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderConstants.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java @@ -15,16 +15,16 @@ package software.amazon.lambda.powertools.logging.log4j; /** - * Constants for BufferingAppender configuration and references. + * Constants for Log4j2 configuration and references. */ -public final class BufferingAppenderConstants { +public final class Log4jConstants { /** - * The name used for BufferingAppender in Log4j2 configuration and references. + * The plugin name for BufferingAppender in Log4j2 configuration. */ - public static final String NAME = "BufferingAppender"; + public static final String BUFFERING_APPENDER_PLUGIN_NAME = "BufferingAppender"; - private BufferingAppenderConstants() { + private Log4jConstants() { // Utility class } -} \ No newline at end of file +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java index 3f2d695f3..a549be794 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java @@ -14,6 +14,8 @@ package software.amazon.lambda.powertools.logging.log4j.internal; +import static software.amazon.lambda.powertools.logging.log4j.Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME; + import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.LoggerContext; @@ -24,8 +26,6 @@ import software.amazon.lambda.powertools.logging.internal.LoggingManager; import software.amazon.lambda.powertools.logging.log4j.BufferingAppender; -import static software.amazon.lambda.powertools.logging.log4j.BufferingAppenderConstants.NAME; - /** * LoggingManager for Log4j2 that provides log level management and buffer operations. * Implements both {@link LoggingManager} and {@link BufferManager} interfaces. @@ -59,7 +59,7 @@ public org.slf4j.event.Level getLogLevel(Logger logger) { public void flushBuffer() { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender(NAME); + .getAppender(BUFFERING_APPENDER_PLUGIN_NAME); if (bufferingAppender != null) { bufferingAppender.flushBuffer(); } @@ -72,7 +72,7 @@ public void flushBuffer() { public void clearBuffer() { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender(NAME); + .getAppender(BUFFERING_APPENDER_PLUGIN_NAME); if (bufferingAppender != null) { bufferingAppender.clearBuffer(); } diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java index d42a7b9db..9ede1fe84 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java @@ -112,6 +112,6 @@ void shouldClearBufferManually() { private BufferingAppender getBufferingAppender() { return (BufferingAppender) ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)) - .getConfiguration().getAppender(BufferingAppenderConstants.NAME); + .getConfiguration().getAppender(Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME); } } From ee0859316ddef6d331ca75f9c193ee5bfd91959f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 11:39:33 +0200 Subject: [PATCH 07/45] Make Appender detection independent of plugin name. --- .../log4j/internal/Log4jLoggingManager.java | 24 ++++---- .../internal/Log4jLoggingManagerTest.java | 55 +++++++++++++++++++ .../resources/log4j2-multiple-buffering.xml | 26 +++++++++ 3 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 powertools-logging/powertools-logging-log4j/src/test/resources/log4j2-multiple-buffering.xml diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java index a549be794..90bbe1d32 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManager.java @@ -14,7 +14,8 @@ package software.amazon.lambda.powertools.logging.log4j.internal; -import static software.amazon.lambda.powertools.logging.log4j.Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME; +import java.util.Collection; +import java.util.stream.Collectors; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -57,12 +58,7 @@ public org.slf4j.event.Level getLogLevel(Logger logger) { */ @Override public void flushBuffer() { - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender(BUFFERING_APPENDER_PLUGIN_NAME); - if (bufferingAppender != null) { - bufferingAppender.flushBuffer(); - } + getBufferingAppenders().forEach(BufferingAppender::flushBuffer); } /** @@ -70,11 +66,15 @@ public void flushBuffer() { */ @Override public void clearBuffer() { + getBufferingAppenders().forEach(BufferingAppender::clearBuffer); + } + + private Collection getBufferingAppenders() { + // Search all buffering appenders to avoid relying on the appender name given by the user LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - BufferingAppender bufferingAppender = (BufferingAppender) ctx.getConfiguration() - .getAppender(BUFFERING_APPENDER_PLUGIN_NAME); - if (bufferingAppender != null) { - bufferingAppender.clearBuffer(); - } + return ctx.getConfiguration().getAppenders().values().stream() + .filter(BufferingAppender.class::isInstance) + .map(BufferingAppender.class::cast) + .collect(Collectors.toList()); } } diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index 53e012d13..f96d4f19f 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -5,6 +5,20 @@ import static org.slf4j.event.Level.ERROR; import static org.slf4j.event.Level.WARN; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -47,4 +61,45 @@ void resetLogLevel() { assertThat(rootLevel).isEqualTo(ERROR); assertThat(logLevel).isEqualTo(ERROR); } + + @Test + void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException { + // Given - configuration with multiple BufferingAppenders with different names + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + + ConfigurationFactory factory = new XmlConfigurationFactory(); + ConfigurationSource source = new ConfigurationSource( + getClass().getResourceAsStream("/log4j2-multiple-buffering.xml")); + Configuration config = factory.getConfiguration(null, source); + + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + ctx.setConfiguration(config); + ctx.updateLoggers(); + + org.apache.logging.log4j.Logger logger = LogManager.getLogger("test.multiple.appenders"); + + // When - log messages and flush buffers + logger.debug("Test message 1"); + logger.debug("Test message 2"); + + Log4jLoggingManager manager = new Log4jLoggingManager(); + manager.flushBuffer(); + + // Then - both appenders should have flushed their buffers + File logFile = new File("target/logfile.json"); + assertThat(logFile).exists(); + } + + @AfterEach + void cleanUp() throws IOException { + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there + } + } } diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2-multiple-buffering.xml b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2-multiple-buffering.xml new file mode 100644 index 000000000..53995e4f5 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2-multiple-buffering.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + From 447633fc16239b83b8e651e6debe2727eca074fc Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 12:15:25 +0200 Subject: [PATCH 08/45] Add assertions in unit test to test multiple appenders. --- .../lambda/powertools/logging/log4j/BufferingAppender.java | 4 ++-- .../logging/log4j/internal/Log4jLoggingManagerTest.java | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index 500c11a4d..def32f338 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -58,7 +58,7 @@ *

Configuration Example:

*
{@code
  * 
  *   
@@ -80,7 +80,7 @@
  *   
  • On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}
  • * * - * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushLogBuffer() + * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushBuffer() */ @Plugin(name = BUFFERING_APPENDER_PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class BufferingAppender extends AbstractAppender implements BufferManager { diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index f96d4f19f..1a9d1b88e 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -1,6 +1,7 @@ package software.amazon.lambda.powertools.logging.log4j.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; import static org.slf4j.event.Level.DEBUG; import static org.slf4j.event.Level.ERROR; import static org.slf4j.event.Level.WARN; @@ -92,6 +93,9 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException // Then - both appenders should have flushed their buffers File logFile = new File("target/logfile.json"); assertThat(logFile).exists(); + assertThat(contentOf(logFile)) + .contains("Test message 1") + .contains("Test message 2"); } @AfterEach From e158b76840415d149cfbaeaf22d938e2fb3b0bc3 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 12:22:57 +0200 Subject: [PATCH 09/45] Make assertion stronger to assert that each message appears twice because we configured two appenders as proxies. --- .../logging/log4j/internal/Log4jLoggingManagerTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index 1a9d1b88e..bcc32e08f 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -93,9 +93,10 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException // Then - both appenders should have flushed their buffers File logFile = new File("target/logfile.json"); assertThat(logFile).exists(); - assertThat(contentOf(logFile)) - .contains("Test message 1") - .contains("Test message 2"); + String content = contentOf(logFile); + // Each message should appear twice (once from each BufferingAppender) + assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts + assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts } @AfterEach From 0ff03b21a5c71ccdaa464dfba14e269ce0337b02 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 13:28:02 +0200 Subject: [PATCH 10/45] Add logback implementation. Decouple KeyBuffer and BufferingAppender to avoid circular dependency when flushing buffer on error. --- .../sam/pom.xml | 13 +- .../sam/src/main/java/helloworld/App.java | 3 +- .../src/main/java/helloworld/AppStream.java | 41 ++-- .../sam/src/main/resources/logback.xml | 16 ++ .../logging/log4j/BufferingAppender.java | 20 +- .../logging/log4j/BufferingAppenderTest.java | 17 ++ .../powertools-logging-logback/pom.xml | 5 + .../logging/logback/BufferingAppender.java | 196 ++++++++++++++++++ .../internal/LogbackLoggingManager.java | 51 ++++- .../logging/LogbackLoggingManagerTest.java | 56 +++++ .../logback/BufferingAppenderTest.java | 150 ++++++++++++++ .../test/resources/logback-buffering-test.xml | 21 ++ .../resources/logback-multiple-buffering.xml | 28 +++ .../logging/internal/KeyBuffer.java | 16 +- .../logging/internal/KeyBufferTest.java | 73 +++++-- 15 files changed, 645 insertions(+), 61 deletions(-) create mode 100644 examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/resources/logback-buffering-test.xml create mode 100644 powertools-logging/powertools-logging-logback/src/test/resources/logback-multiple-buffering.xml diff --git a/examples/powertools-examples-core-utilities/sam/pom.xml b/examples/powertools-examples-core-utilities/sam/pom.xml index 3df44f441..93bf882c4 100644 --- a/examples/powertools-examples-core-utilities/sam/pom.xml +++ b/examples/powertools-examples-core-utilities/sam/pom.xml @@ -22,7 +22,7 @@ software.amazon.lambda - powertools-logging-log4j + powertools-logging-logback ${project.version} @@ -99,19 +99,10 @@ false - - - - - - org.apache.logging.log4j - log4j-transform-maven-shade-plugin-extensions - 0.2.0 - - + diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index daf59ecf7..ef2f93f92 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -58,7 +58,8 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv log.info("INFO 1"); // Manually flush buffer to show buffered debug logs - PowertoolsLogging.flushBuffer(); + // PowertoolsLogging.flushBuffer(); + log.error("Some error happened"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java index 8bc57b201..d3ebbef5d 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java @@ -14,41 +14,45 @@ package helloworld; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.databind.ObjectMapper; - +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.nio.charset.StandardCharsets; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; + import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import java.io.InputStreamReader; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; - public class AppStream implements RequestStreamHandler { private static final ObjectMapper mapper = new ObjectMapper(); - private final static Logger log = LogManager.getLogger(AppStream.class); + private static final Logger log = LoggerFactory.getLogger(AppStream.class); @Override @Logging(logEvent = true) @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically - // Note that you still need to return a proper JSON for API Gateway to handle - // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body + // or serialize response body yourself, instead of allowing that to happen automatically + // Note that you still need to return a proper JSON for API Gateway to handle + // See Lambda Response format for examples: + // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html public void handleRequest(InputStream input, OutputStream output, Context context) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); - PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)))) { + PrintWriter writer = new PrintWriter( + new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)))) { - log.info("Received: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapper.readTree(reader))); + log.info( + "Received: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapper.readTree(reader))); writer.write("{\"body\": \"" + System.currentTimeMillis() + "\"} "); } catch (IOException e) { @@ -56,4 +60,3 @@ public void handleRequest(InputStream input, OutputStream output, Context contex } } } - diff --git a/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml new file mode 100644 index 000000000..68fce98c8 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + + + DEBUG + 8 + + + + + + + diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index def32f338..d0a3e3a64 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -34,6 +34,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.message.SimpleMessage; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.logging.internal.BufferManager; @@ -91,6 +92,7 @@ public class BufferingAppender extends AbstractAppender implements BufferManager private final boolean flushOnErrorLog; private final KeyBuffer buffer; + @SuppressWarnings("java:S107") // Constructor has too many parameters, which is OK for a Log4j2 plugin protected BufferingAppender(String name, Filter filter, Layout layout, AppenderRef[] appenderRefs, Configuration configuration, Level bufferAtVerbosity, int maxBytes, boolean flushOnErrorLog) { @@ -99,7 +101,8 @@ protected BufferingAppender(String name, Filter filter, Layout(maxBytes, event -> event.getMessage().getFormattedMessage().length()); + this.buffer = new KeyBuffer<>(maxBytes, event -> event.getMessage().getFormattedMessage().length(), + this::logOverflowWarning); } @Override @@ -159,6 +162,7 @@ private void flushBuffer(String traceId) { } @PluginFactory + @SuppressWarnings("java:S107") // Method has too many parameters, which is OK for a Log4j2 plugin factory public static BufferingAppender createAppender( @PluginAttribute("name") String name, @PluginElement("Filter") Filter filter, @@ -182,4 +186,18 @@ public static BufferingAppender createAppender( return new BufferingAppender(name, filter, layout, appenderRefs, configuration, level, maxBytes, flushOnErrorLog); } + + private void logOverflowWarning() { + // Create a properly formatted warning event and send directly to child appenders. Used to avoid circular + // dependency between KeyBuffer and BufferingAppender. + SimpleMessage message = new SimpleMessage( + "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer."); + LogEvent warningEvent = Log4jLogEvent.newBuilder() + .setLoggerName(BufferingAppender.class.getName()) + .setLevel(Level.WARN) + .setMessage(message) + .setTimeMillis(System.currentTimeMillis()) + .build(); + callAppenders(warningEvent); + } } diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java index 9ede1fe84..2c3fc8d02 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java @@ -110,6 +110,23 @@ void shouldClearBufferManually() { .doesNotContain("Buffered message"); } + @Test + void shouldLogOverflowWarningWhenBufferOverflows() { + // When - fill buffer beyond capacity to trigger overflow + for (int i = 0; i < 100; i++) { + logger.debug("Debug message " + i); + } + + // When - flush buffer to trigger overflow warning + BufferingAppender appender = getBufferingAppender(); + appender.flushBuffer(); + + // Then - overflow warning should be logged + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Some logs are not displayed because they were evicted from the buffer"); + } + private BufferingAppender getBufferingAppender() { return (BufferingAppender) ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)) .getConfiguration().getAppender(Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME); diff --git a/powertools-logging/powertools-logging-logback/pom.xml b/powertools-logging/powertools-logging-logback/pom.xml index 433a3774a..e2c16019f 100644 --- a/powertools-logging/powertools-logging-logback/pom.xml +++ b/powertools-logging/powertools-logging-logback/pom.xml @@ -85,6 +85,11 @@ jsonassert test + + org.junit-pioneer + junit-pioneer + test + diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java new file mode 100644 index 000000000..e8147ccec --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java @@ -0,0 +1,196 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback; + +import java.util.Deque; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.spi.AppenderAttachable; +import ch.qos.logback.core.spi.AppenderAttachableImpl; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.BufferManager; +import software.amazon.lambda.powertools.logging.internal.KeyBuffer; + +/** + * A Logback appender that buffers log events by AWS X-Ray trace ID for optimized Lambda logging. + * + *

    This appender is designed specifically for AWS Lambda functions to reduce log ingestion + * by buffering lower-level logs and only outputting them when errors occur, preserving + * full context for troubleshooting while minimizing routine log volume. + * + *

    Key Features:

    + *
      + *
    • Trace-based buffering: Groups logs by AWS X-Ray trace ID
    • + *
    • Selective output: Only buffers logs at or below configured verbosity level
    • + *
    • Auto-flush on errors: Automatically outputs buffered logs when ERROR events occur
    • + *
    • Memory management: Prevents memory leaks with configurable buffer size limits
    • + *
    • Overflow protection: Warns when logs are discarded due to buffer limits
    • + *
    + * + *

    Configuration Example:

    + *
    {@code
    + * 
    + *   DEBUG
    + *   20480
    + *   true
    + *   
    + * 
    + * }
    + * + *

    Configuration Parameters:

    + *
      + *
    • bufferAtVerbosity: Log level to buffer (default: DEBUG). Logs at this level and below are buffered
    • + *
    • maxBytes: Maximum buffer size in bytes per trace ID (default: 20480)
    • + *
    • flushOnErrorLog: Whether to flush buffer on ERROR logs (default: true)
    • + *
    + * + *

    Behavior:

    + *
      + *
    • During Lambda INIT phase (no trace ID): logs are output directly
    • + *
    • During Lambda execution (with trace ID): logs are buffered or output based on level
    • + *
    • When buffer overflows: oldest logs are discarded and a warning is logged
    • + *
    • On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}
    • + *
    + * + * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushBuffer() + */ +public class BufferingAppender extends AppenderBase + implements AppenderAttachable, BufferManager { + + private static final int DEFAULT_BUFFER_SIZE = 20480; + + private final AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); + private Level bufferAtVerbosity = Level.DEBUG; + private boolean flushOnErrorLog = true; + private int maxBytes = DEFAULT_BUFFER_SIZE; + private KeyBuffer buffer; + + @Override + public void start() { + // Initialize lazily to ensure configuration properties are set first. + if (buffer == null) { + buffer = new KeyBuffer<>(maxBytes, event -> event.getFormattedMessage().length(), this::logOverflowWarning); + } + super.start(); + } + + @Override + protected void append(ILoggingEvent event) { + LambdaHandlerProcessor.getXrayTraceId().ifPresentOrElse( + traceId -> { + if (shouldBuffer(event.getLevel())) { + buffer.add(traceId, event); + } else { + aai.appendLoopOnAppenders(event); + } + + // Flush buffer on error logs if configured + if (flushOnErrorLog && event.getLevel().isGreaterOrEqual(Level.ERROR)) { + flushBuffer(traceId); + } + }, + () -> aai.appendLoopOnAppenders(event) // No trace ID (INIT phase), log directly + ); + } + + private boolean shouldBuffer(Level level) { + return level.levelInt <= bufferAtVerbosity.levelInt; + } + + public void clearBuffer() { + LambdaHandlerProcessor.getXrayTraceId().ifPresent(buffer::clear); + } + + public void flushBuffer() { + LambdaHandlerProcessor.getXrayTraceId().ifPresent(this::flushBuffer); + } + + private void flushBuffer(String traceId) { + Deque events = buffer.removeAll(traceId); + if (events != null) { + events.forEach(aai::appendLoopOnAppenders); + } + } + + // Configuration setters. These will be inspected as JavaBean properties by Logback + // when configuring the appender via XML or programmatically. They run before start(). + public void setBufferAtVerbosity(String level) { + this.bufferAtVerbosity = Level.toLevel(level, Level.DEBUG); + } + + public void setMaxBytes(int maxBytes) { + this.maxBytes = maxBytes; + } + + public void setFlushOnErrorLog(boolean flushOnErrorLog) { + this.flushOnErrorLog = flushOnErrorLog; + } + + // AppenderAttachable implementation. We simply delegate to the internal logback AppenderAttachableImpl. This is + // needed to be able to attach other appenders to this appender so that customers can wrap existing appenders with + // this buffering appender. + @Override + public void addAppender(Appender newAppender) { + aai.addAppender(newAppender); + } + + @Override + public java.util.Iterator> iteratorForAppenders() { + return aai.iteratorForAppenders(); + } + + @Override + public Appender getAppender(String name) { + return aai.getAppender(name); + } + + @Override + public boolean isAttached(Appender appender) { + return aai.isAttached(appender); + } + + @Override + public void detachAndStopAllAppenders() { + aai.detachAndStopAllAppenders(); + } + + @Override + public boolean detachAppender(Appender appender) { + return aai.detachAppender(appender); + } + + @Override + public boolean detachAppender(String name) { + return aai.detachAppender(name); + } + + private void logOverflowWarning() { + // Create a properly formatted warning event and send directly to child appenders. Used to avoid circular + // dependency between KeyBuffer and BufferingAppender. + Logger logbackLogger = (Logger) org.slf4j.LoggerFactory + .getLogger(BufferingAppender.class); + LoggingEvent warningEvent = new LoggingEvent( + BufferingAppender.class.getName(), logbackLogger, Level.WARN, + "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.", + null, null); + warningEvent.setTimeStamp(System.currentTimeMillis()); + aai.appendLoopOnAppenders(warningEvent); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java index 86982b444..906ebdad5 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java @@ -14,18 +14,29 @@ package software.amazon.lambda.powertools.logging.logback.internal; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; + import org.slf4j.ILoggerFactory; import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import software.amazon.lambda.powertools.logging.internal.BufferManager; import software.amazon.lambda.powertools.logging.internal.LoggingManager; +import software.amazon.lambda.powertools.logging.logback.BufferingAppender; /** - * LoggingManager for Logback (see {@link LoggingManager}). + * LoggingManager for Logback that provides log level management and buffer operations. + * Implements both {@link LoggingManager} and {@link BufferManager} interfaces. */ -public class LogbackLoggingManager implements LoggingManager { +public class LogbackLoggingManager implements LoggingManager, BufferManager { private final LoggerContext loggerContext; @@ -56,4 +67,34 @@ public void setLogLevel(org.slf4j.event.Level logLevel) { public org.slf4j.event.Level getLogLevel(org.slf4j.Logger logger) { return org.slf4j.event.Level.valueOf(loggerContext.getLogger(logger.getName()).getEffectiveLevel().toString()); } + + /** + * @inheritDoc + */ + @Override + public void flushBuffer() { + getBufferingAppenders().forEach(BufferingAppender::flushBuffer); + } + + /** + * @inheritDoc + */ + @Override + public void clearBuffer() { + getBufferingAppenders().forEach(BufferingAppender::clearBuffer); + } + + private Collection getBufferingAppenders() { + // Search all buffering appenders to avoid relying on the appender name given by the user + return loggerContext.getLoggerList().stream() + .flatMap(logger -> { + Iterator> iterator = logger.iteratorForAppenders(); + List> appenders = new ArrayList<>(); + iterator.forEachRemaining(appenders::add); + return appenders.stream(); + }) + .filter(BufferingAppender.class::isInstance) + .map(BufferingAppender.class::cast) + .collect(Collectors.toList()); + } } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java index 214057917..35908d1f5 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -15,15 +15,28 @@ package software.amazon.lambda.powertools.logging; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; import static org.slf4j.event.Level.DEBUG; import static org.slf4j.event.Level.ERROR; import static org.slf4j.event.Level.WARN; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; import software.amazon.lambda.powertools.logging.logback.internal.LogbackLoggingManager; class LogbackLoggingManagerTest { @@ -51,4 +64,47 @@ void resetLogLevel() { Level logLevel = manager.getLogLevel(LOG); assertThat(logLevel).isEqualTo(ERROR); } + + @Test + void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException, JoranException { + // Given - configuration with multiple BufferingAppenders with different names + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + context.reset(); + + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + configurator.doConfigure(getClass().getResourceAsStream("/logback-multiple-buffering.xml")); + + Logger logger = LoggerFactory.getLogger("test.multiple.appenders"); + + // When - log messages and flush buffers + logger.debug("Test message 1"); + logger.debug("Test message 2"); + + LogbackLoggingManager manager = new LogbackLoggingManager(); + manager.flushBuffer(); + + // Then - both appenders should have flushed their buffers + File logFile = new File("target/logfile.json"); + assertThat(logFile).exists(); + String content = contentOf(logFile); + // Each message should appear twice (once from each BufferingAppender) + assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts + assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts + } + + @AfterEach + void cleanUp() throws IOException { + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there + } + } } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java new file mode 100644 index 000000000..e4a3dc593 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java @@ -0,0 +1,150 @@ +package software.amazon.lambda.powertools.logging.logback; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; + +class BufferingAppenderTest { + + private Logger logger; + + @BeforeEach + void setUp() throws IOException, JoranException { + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + + // Configure Logback with BufferingAppender + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + context.reset(); + + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + configurator.doConfigure(getClass().getResourceAsStream("/logback-buffering-test.xml")); + + logger = LoggerFactory.getLogger(BufferingAppenderTest.class); + } + + @AfterEach + void cleanUp() throws IOException { + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there + } + } + + @Test + void shouldBufferDebugLogsAndFlushOnError() { + // When - log debug messages (should be buffered) + logger.debug("Debug message 1"); + logger.debug("Debug message 2"); + + // Then - no logs written yet + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).isEmpty(); + + // When - log error (should flush buffer) + logger.error("Error message"); + + // Then - all logs written + assertThat(contentOf(logFile)) + .contains("Debug message 1") + .contains("Debug message 2") + .contains("Error message"); + } + + @Test + @ClearEnvironmentVariable(key = "_X_AMZN_TRACE_ID") + void shouldLogDirectlyWhenNoTraceId() { + // When + logger.debug("Debug without trace"); + + // Then - log written directly + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Debug without trace"); + } + + @Test + void shouldNotBufferInfoLogs() { + // When - log info message (above buffer level) + logger.info("Info message"); + + // Then - log written directly + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Info message"); + } + + @Test + void shouldFlushBufferManually() { + // When - buffer debug logs + logger.debug("Buffered message"); + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).isEmpty(); + + // When - manual flush + BufferingAppender appender = getBufferingAppender(); + appender.flushBuffer(); + + // Then - logs written + assertThat(contentOf(logFile)).contains("Buffered message"); + } + + @Test + void shouldClearBufferManually() { + // When - buffer debug logs then clear + logger.debug("Buffered message"); + BufferingAppender appender = getBufferingAppender(); + appender.clearBuffer(); + + // When - log error (should not flush cleared buffer) + logger.error("Error after clear"); + + // Then - only error logged + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Error after clear") + .doesNotContain("Buffered message"); + } + + @Test + void shouldLogOverflowWarningWhenBufferOverflows() { + // When - fill buffer beyond capacity to trigger overflow + for (int i = 0; i < 100; i++) { + logger.debug("Debug message " + i); + } + + // When - flush buffer to trigger overflow warning + BufferingAppender appender = getBufferingAppender(); + appender.flushBuffer(); + + // Then - overflow warning should be logged + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("Some logs are not displayed because they were evicted from the buffer"); + } + + private BufferingAppender getBufferingAppender() { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + return (BufferingAppender) context.getLogger(BufferingAppenderTest.class).getAppender("TestBufferingAppender"); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/logback-buffering-test.xml b/powertools-logging/powertools-logging-logback/src/test/resources/logback-buffering-test.xml new file mode 100644 index 000000000..b9ec3917f --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/logback-buffering-test.xml @@ -0,0 +1,21 @@ + + + + target/logfile.json + + %msg%n + + + + + DEBUG + 1024 + true + + + + + + + diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/logback-multiple-buffering.xml b/powertools-logging/powertools-logging-logback/src/test/resources/logback-multiple-buffering.xml new file mode 100644 index 000000000..84c348d61 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/logback-multiple-buffering.xml @@ -0,0 +1,28 @@ + + + + target/logfile.json + + %msg%n + + + + + DEBUG + 1024 + true + + + + + DEBUG + 1024 + true + + + + + + + + diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java index c8d5419e2..03a4cade1 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java @@ -20,9 +20,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Generic buffer data structure for storing events by key with size-based eviction. * @@ -56,16 +53,22 @@ */ public class KeyBuffer { - private static final Logger logger = LoggerFactory.getLogger(KeyBuffer.class); - private final Map> keyBufferCache = new ConcurrentHashMap<>(); private final Map overflowTriggered = new ConcurrentHashMap<>(); private final int maxBytes; private final Function sizeCalculator; + private final Runnable overflowWarningLogger; + @SuppressWarnings("java:S106") // Using System.err to avoid circular dependency with logging implementation public KeyBuffer(int maxBytes, Function sizeCalculator) { + this(maxBytes, sizeCalculator, () -> System.err.println("WARN [" + KeyBuffer.class.getSimpleName() + + "] - Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.")); + } + + public KeyBuffer(int maxBytes, Function sizeCalculator, Runnable overflowWarningLogger) { this.maxBytes = maxBytes; this.sizeCalculator = sizeCalculator; + this.overflowWarningLogger = overflowWarningLogger; } public void add(K key, T event) { @@ -103,8 +106,7 @@ public void clear(K key) { private void logOverflowWarningIfNeeded(K key) { if (Boolean.TRUE.equals(overflowTriggered.remove(key))) { - logger.warn( - "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer."); + overflowWarningLogger.run(); } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index 20b4522b8..f97842e91 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -15,10 +15,10 @@ package software.amazon.lambda.powertools.logging.internal; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.contentOf; -import java.io.File; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.nio.channels.FileChannel; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -183,7 +183,9 @@ void shouldReturnDefensiveCopyOnRemoveAll() { @Test void shouldLogWarningOnOverflow() { - KeyBuffer testBuffer = new KeyBuffer<>(10, String::length); + StringBuilder warningCapture = new StringBuilder(); + KeyBuffer testBuffer = new KeyBuffer<>(10, String::length, + () -> warningCapture.append("Some logs are not displayed because they were evicted from the buffer")); // Cause overflow testBuffer.add("key1", "1234567890"); // 10 bytes @@ -192,14 +194,15 @@ void shouldLogWarningOnOverflow() { // Trigger warning by removing testBuffer.removeAll("key1"); - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) + assertThat(warningCapture.toString()) .contains("Some logs are not displayed because they were evicted from the buffer"); } @Test void shouldLogWarningOnLargeEventRejection() { - KeyBuffer testBuffer = new KeyBuffer<>(10, String::length); + StringBuilder warningCapture = new StringBuilder(); + KeyBuffer testBuffer = new KeyBuffer<>(10, String::length, + () -> warningCapture.append("Some logs are not displayed because they were evicted from the buffer")); // Add large event that gets rejected testBuffer.add("key1", "12345678901"); // 11 bytes > 10 max @@ -207,20 +210,20 @@ void shouldLogWarningOnLargeEventRejection() { // Trigger warning by removing testBuffer.removeAll("key1"); - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) + assertThat(warningCapture.toString()) .contains("Some logs are not displayed because they were evicted from the buffer"); } @Test void shouldNotLogWarningWhenNoOverflow() { - KeyBuffer testBuffer = new KeyBuffer<>(20, String::length); + StringBuilder warningCapture = new StringBuilder(); + KeyBuffer testBuffer = new KeyBuffer<>(20, String::length, + () -> warningCapture.append("Some logs are not displayed because they were evicted from the buffer")); testBuffer.add("key1", "small"); testBuffer.removeAll("key1"); - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)) + assertThat(warningCapture.toString()) .doesNotContain("Some logs are not displayed because they were evicted from the buffer"); } @@ -251,9 +254,7 @@ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { for (int i = 0; i < threadCount; i++) { String key = "key" + i; Deque events = buffer.removeAll(key); - assertThat(events).isNotNull(); - // Some events might be evicted due to size limits, but should have some - assertThat(events).isNotEmpty(); + assertThat(events).isNotNull().isNotEmpty(); } executor.shutdown(); @@ -283,9 +284,7 @@ void shouldBeThreadSafeForSameKey() throws InterruptedException { assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); Deque events = buffer.removeAll("sharedKey"); - assertThat(events).isNotNull(); - // Due to size limits, not all events will be present, but buffer should be consistent - assertThat(events).isNotEmpty(); + assertThat(events).isNotNull().isNotEmpty(); executor.shutdown(); } @@ -306,4 +305,44 @@ void shouldHandleZeroSizeEvents() { Deque events = zeroBuffer.removeAll("key1"); assertThat(events).containsExactly("event1", "event2"); } + + @Test + void shouldUseCustomWarningLogger() { + StringBuilder customWarning = new StringBuilder(); + KeyBuffer testBuffer = new KeyBuffer<>(5, String::length, + () -> customWarning.append("CUSTOM WARNING LOGGED")); + + // Cause overflow + testBuffer.add("key1", "12345"); // 5 bytes + testBuffer.add("key1", "extra"); // causes overflow + + // Trigger warning + testBuffer.removeAll("key1"); + + assertThat(customWarning).hasToString("CUSTOM WARNING LOGGED"); + } + + @Test + void shouldUseDefaultWarningLoggerWhenNotProvided() { + // Capture System.err output + ByteArrayOutputStream errCapture = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(errCapture)); + + try { + KeyBuffer defaultBuffer = new KeyBuffer<>(5, String::length); + + // Cause overflow + defaultBuffer.add("key1", "12345"); + defaultBuffer.add("key1", "extra"); + defaultBuffer.removeAll("key1"); + + // Assert System.err received the warning + assertThat(errCapture) + .hasToString( + "WARN [KeyBuffer] - Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer.\n"); + } finally { + System.setErr(originalErr); + } + } } From eb2502e364cddd45fc4dbf0efd31d3421199fa06 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 13:35:49 +0200 Subject: [PATCH 11/45] Log error in AppStream example. --- .../sam/src/main/java/helloworld/AppStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java index d3ebbef5d..6524b8e7e 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java @@ -56,6 +56,7 @@ public void handleRequest(InputStream input, OutputStream output, Context contex writer.write("{\"body\": \"" + System.currentTimeMillis() + "\"} "); } catch (IOException e) { + log.error("Exception caught in handler", e); log.error("Something has gone wrong: ", e); } } From d9fe1e19327be3e641eb91323876498838a1e996 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 13:45:45 +0200 Subject: [PATCH 12/45] Update Logging E2E test to use log buffering. --- .../amazon/lambda/powertools/e2e/Function.java | 12 +++++++++--- .../handlers/logging/src/main/resources/log4j2.xml | 11 ++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 58492653a..94520c447 100644 --- a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -14,12 +14,15 @@ package software.amazon.lambda.powertools.e2e; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.PowertoolsLogging; public class Function implements RequestHandler { private static final Logger LOG = LoggerFactory.getLogger(Function.class); @@ -29,6 +32,9 @@ public String handleRequest(Input input, Context context) { input.getKeys().forEach(MDC::put); LOG.info(input.getMessage()); + // Flush buffer manually since we buffer at INFO level to test log buffering + PowertoolsLogging.flushBuffer(); + return "OK"; } -} \ No newline at end of file +} diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml index 8925f70b9..28e03a9e0 100644 --- a/powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml +++ b/powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml @@ -4,13 +4,14 @@ + + + + - + - - - - \ No newline at end of file + From 2dbae695becfdc5861599d2b3c62c37e14ba98cf Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 13:54:53 +0200 Subject: [PATCH 13/45] Prepare Logging E2E test to support paramaterized versions by logging backend. --- powertools-e2e-tests/pom.xml | 5 ++ .../amazon/lambda/powertools/LoggingE2ET.java | 85 +++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 4094bfb20..d206697c9 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -102,6 +102,11 @@ junit-jupiter-engine test + + org.junit.jupiter + junit-jupiter-params + test + org.assertj assertj-core diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index ad2c2564f..6cd6e5d1a 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -26,9 +26,10 @@ import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -37,20 +38,19 @@ import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class LoggingE2ET { private static final ObjectMapper objectMapper = new ObjectMapper(); - private static Infrastructure infrastructure; - private static String functionName; + private Infrastructure infrastructure; + private String functionName; - @BeforeAll - @Timeout(value = 10, unit = TimeUnit.MINUTES) - static void setup() { + private void setupInfrastructure(String pathToFunction) { infrastructure = Infrastructure.builder() - .testName(LoggingE2ET.class.getSimpleName()) + .testName(LoggingE2ET.class.getSimpleName() + "-" + pathToFunction) .tracing(true) - .pathToFunction("logging") + .pathToFunction(pathToFunction) .environmentVariables( Stream.of(new String[][] { { "POWERTOOLS_LOG_LEVEL", "INFO" }, @@ -63,37 +63,50 @@ static void setup() { } @AfterAll - static void tearDown() { + void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } - @Test - void test_logInfoWithAdditionalKeys() throws JsonProcessingException { - // GIVEN - String orderId = UUID.randomUUID().toString(); - String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId + "\"}}"; - - // WHEN - InvocationResult invocationResult1 = invokeFunction(functionName, event); - InvocationResult invocationResult2 = invokeFunction(functionName, event); - - // THEN - String[] functionLogs = invocationResult1.getLogs().getFunctionLogs(INFO); - assertThat(functionLogs).hasSize(1); - - JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); - assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); - assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); - assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); - assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); - - // second call should not be cold start - functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); - assertThat(functionLogs).hasSize(1); - jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); + @ParameterizedTest + @ValueSource(strings = { "logging" }) + @Timeout(value = 15, unit = TimeUnit.MINUTES) + void test_logInfoWithAdditionalKeys(String pathToFunction) throws JsonProcessingException { + setupInfrastructure(pathToFunction); + + try { + // GIVEN + String orderId = UUID.randomUUID().toString(); + String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId + "\"}}"; + + // WHEN + InvocationResult invocationResult1 = invokeFunction(functionName, event); + InvocationResult invocationResult2 = invokeFunction(functionName, event); + + // THEN + String[] functionLogs = invocationResult1.getLogs().getFunctionLogs(INFO); + assertThat(functionLogs).hasSize(1); + + JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); + assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); + assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); + assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); + assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); + assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); + + // second call should not be cold start + functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); + assertThat(functionLogs).hasSize(1); + jsonNode = objectMapper.readTree(functionLogs[0]); + assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); + + } finally { + // Clean up infrastructure after each parameter + if (infrastructure != null) { + infrastructure.destroy(); + infrastructure = null; + } + } } } From f3cfd5d37faf7d652611c054e0442408df84e0ec Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 14:03:00 +0200 Subject: [PATCH 14/45] Add Logging E2E tests for both logback and log4j2. --- .../{logging => logging-log4j}/pom.xml | 9 +- .../lambda/powertools/e2e/Function.java | 0 .../amazon/lambda/powertools/e2e/Input.java | 0 .../aws-lambda-java-core/reflect-config.json | 0 .../reflect-config.json | 0 .../jni-config.json | 0 .../native-image.properties | 0 .../reflect-config.json | 0 .../resource-config.json | 0 .../reflect-config.json | 0 .../reflect-config.json | 0 .../resource-config.json | 0 .../src/main/resources/log4j2.xml | 0 .../handlers/logging-logback/pom.xml | 82 +++++++++++++++++++ .../lambda/powertools/e2e/Function.java | 40 +++++++++ .../amazon/lambda/powertools/e2e/Input.java | 41 ++++++++++ .../aws-lambda-java-core/reflect-config.json | 13 +++ .../reflect-config.json | 35 ++++++++ .../jni-config.json | 11 +++ .../native-image.properties | 1 + .../reflect-config.json | 61 ++++++++++++++ .../resource-config.json | 19 +++++ .../reflect-config.json | 25 ++++++ .../reflect-config.json | 20 +++++ .../resource-config.json | 7 ++ .../src/main/resources/logback.xml | 16 ++++ powertools-e2e-tests/handlers/pom.xml | 8 +- .../amazon/lambda/powertools/LoggingE2ET.java | 2 +- 28 files changed, 381 insertions(+), 9 deletions(-) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/pom.xml (90%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/java/software/amazon/lambda/powertools/e2e/Function.java (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/java/software/amazon/lambda/powertools/e2e/Input.java (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json (100%) rename powertools-e2e-tests/handlers/{logging => logging-log4j}/src/main/resources/log4j2.xml (100%) create mode 100644 powertools-e2e-tests/handlers/logging-logback/pom.xml create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Function.java create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json create mode 100644 powertools-e2e-tests/handlers/logging-logback/src/main/resources/logback.xml diff --git a/powertools-e2e-tests/handlers/logging/pom.xml b/powertools-e2e-tests/handlers/logging-log4j/pom.xml similarity index 90% rename from powertools-e2e-tests/handlers/logging/pom.xml rename to powertools-e2e-tests/handlers/logging-log4j/pom.xml index f8b5689c8..d415ae4b3 100644 --- a/powertools-e2e-tests/handlers/logging/pom.xml +++ b/powertools-e2e-tests/handlers/logging-log4j/pom.xml @@ -8,20 +8,15 @@ 2.3.0 - e2e-test-handler-logging + e2e-test-handler-logging-log4j jar - E2E test handler – Logging + E2E test handler – Logging Log4j software.amazon.lambda powertools-logging-log4j - - org.apache.logging.log4j - log4j-layout-template-json - 2.25.1 - org.aspectj aspectjrt diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Function.java similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename to powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Function.java diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java rename to powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json diff --git a/powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/logging-log4j/src/main/resources/log4j2.xml similarity index 100% rename from powertools-e2e-tests/handlers/logging/src/main/resources/log4j2.xml rename to powertools-e2e-tests/handlers/logging-log4j/src/main/resources/log4j2.xml diff --git a/powertools-e2e-tests/handlers/logging-logback/pom.xml b/powertools-e2e-tests/handlers/logging-logback/pom.xml new file mode 100644 index 000000000..932eb0612 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 2.3.0 + + + e2e-test-handler-logging-logback + jar + E2E test handler – Logging Logback + + + + software.amazon.lambda + powertools-logging-logback + + + org.aspectj + aspectjrt + + + com.amazonaws + aws-lambda-java-events + + + com.amazonaws + aws-lambda-java-runtime-interface-client + + + com.amazonaws + aws-lambda-java-core + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + + + + + + diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..94520c447 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.PowertoolsLogging; + +public class Function implements RequestHandler { + private static final Logger LOG = LoggerFactory.getLogger(Function.class); + + @Logging + public String handleRequest(Input input, Context context) { + input.getKeys().forEach(MDC::put); + LOG.info(input.getMessage()); + + // Flush buffer manually since we buffer at INFO level to test log buffering + PowertoolsLogging.flushBuffer(); + + return "OK"; + } +} diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java new file mode 100644 index 000000000..cc449922e --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import java.util.Map; + +public class Input { + private String message; + private Map keys; + + public Input() { + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Map getKeys() { + return keys; + } + + public void setKeys(Map keys) { + this.keys = keys; + } +} diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..91be72f7a --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name": "tenantId"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..e69fa735c --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,61 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields": [{ "name": "logger" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogLevel", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogFormat", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "java.lang.Void", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "java.util.Collections$UnmodifiableMap", + "fields": [{ "name": "m" }] + }, + { + "name": "jdk.internal.module.IllegalAccessLogger", + "fields": [{ "name": "logger" }] + }, + { + "name": "sun.misc.Unsafe", + "fields": [{ "name": "theUnsafe" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields": [ + { "name": "id" }, + { "name": "invokedFunctionArn" }, + { "name": "deadlineTimeInMs" }, + { "name": "xrayTraceId" }, + { "name": "clientContext" }, + { "name": "cognitoIdentity" }, + { "name": "tenantId" }, + { "name": "content" } + ], + "allPublicMethods": true + } +] diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..1062b4249 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-x86_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-x86_64.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json new file mode 100644 index 000000000..9ddd235e2 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "software.amazon.lambda.powertools.e2e.Function", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "software.amazon.lambda.powertools.e2e.Input", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json new file mode 100644 index 000000000..a603a9398 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlogback.xml\\E" + }]}, + "bundles":[] +} diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/resources/logback.xml b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/logback.xml new file mode 100644 index 000000000..0a5e4d146 --- /dev/null +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + + + + INFO + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index 8cc580958..d9681a8af 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -28,7 +28,8 @@ batch largemessage largemessage_idempotent - logging + logging-log4j + logging-logback tracing metrics idempotency @@ -56,6 +57,11 @@ powertools-logging-log4j ${project.version} + + software.amazon.lambda + powertools-logging-logback + ${project.version} + software.amazon.lambda powertools-tracing diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index 6cd6e5d1a..5aee60e9d 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -70,7 +70,7 @@ void tearDown() { } @ParameterizedTest - @ValueSource(strings = { "logging" }) + @ValueSource(strings = { "logging-log4j", "logging-logback" }) @Timeout(value = 15, unit = TimeUnit.MINUTES) void test_logInfoWithAdditionalKeys(String pathToFunction) throws JsonProcessingException { setupInfrastructure(pathToFunction); From cc863c461407df418963b8a29de24c223117b51c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 14:09:09 +0200 Subject: [PATCH 15/45] Debug not found appender on Linux. --- .../logging/log4j/BufferingAppender.java | 4 +-- .../logging/log4j/Log4jConstants.java | 30 ------------------- .../logging/log4j/BufferingAppenderTest.java | 13 ++++++-- 3 files changed, 11 insertions(+), 36 deletions(-) delete mode 100644 powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index d0a3e3a64..ebf2aac4f 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -14,8 +14,6 @@ package software.amazon.lambda.powertools.logging.log4j; -import static software.amazon.lambda.powertools.logging.log4j.Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME; - import java.io.Serializable; import java.util.Deque; @@ -83,7 +81,7 @@ * * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushBuffer() */ -@Plugin(name = BUFFERING_APPENDER_PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +@Plugin(name = "BufferingAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class BufferingAppender extends AbstractAppender implements BufferManager { private final AppenderRef[] appenderRefs; diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java deleted file mode 100644 index 5d43eb3dc..000000000 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/Log4jConstants.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed 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. - * - */ - -package software.amazon.lambda.powertools.logging.log4j; - -/** - * Constants for Log4j2 configuration and references. - */ -public final class Log4jConstants { - - /** - * The plugin name for BufferingAppender in Log4j2 configuration. - */ - public static final String BUFFERING_APPENDER_PLUGIN_NAME = "BufferingAppender"; - - private Log4jConstants() { - // Utility class - } -} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java index 2c3fc8d02..689c75eaf 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java @@ -12,6 +12,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LoggerContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -116,7 +118,7 @@ void shouldLogOverflowWarningWhenBufferOverflows() { for (int i = 0; i < 100; i++) { logger.debug("Debug message " + i); } - + // When - flush buffer to trigger overflow warning BufferingAppender appender = getBufferingAppender(); appender.flushBuffer(); @@ -128,7 +130,12 @@ void shouldLogOverflowWarningWhenBufferOverflows() { } private BufferingAppender getBufferingAppender() { - return (BufferingAppender) ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)) - .getConfiguration().getAppender(Log4jConstants.BUFFERING_APPENDER_PLUGIN_NAME); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Appender appender = context.getConfiguration().getAppender("BufferingAppender"); + if (appender == null) { + throw new IllegalStateException("BufferingAppender not found in configuration. Available appenders: " + + context.getConfiguration().getAppenders().keySet()); + } + return (BufferingAppender) appender; } } From c6478bece05c1868b5c95cdd7e54255f2f86d76e Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 14:15:06 +0200 Subject: [PATCH 16/45] Isolate log4j context loading in unit tests. --- .../internal/Log4jLoggingManagerTest.java | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index bcc32e08f..b7fb9385e 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -72,31 +72,39 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException // may not be there in the first run } - ConfigurationFactory factory = new XmlConfigurationFactory(); - ConfigurationSource source = new ConfigurationSource( - getClass().getResourceAsStream("/log4j2-multiple-buffering.xml")); - Configuration config = factory.getConfiguration(null, source); - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - ctx.setConfiguration(config); - ctx.updateLoggers(); - - org.apache.logging.log4j.Logger logger = LogManager.getLogger("test.multiple.appenders"); - - // When - log messages and flush buffers - logger.debug("Test message 1"); - logger.debug("Test message 2"); + Configuration originalConfig = ctx.getConfiguration(); - Log4jLoggingManager manager = new Log4jLoggingManager(); - manager.flushBuffer(); - - // Then - both appenders should have flushed their buffers - File logFile = new File("target/logfile.json"); - assertThat(logFile).exists(); - String content = contentOf(logFile); - // Each message should appear twice (once from each BufferingAppender) - assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts - assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts + try { + ConfigurationFactory factory = new XmlConfigurationFactory(); + ConfigurationSource source = new ConfigurationSource( + getClass().getResourceAsStream("/log4j2-multiple-buffering.xml")); + Configuration config = factory.getConfiguration(null, source); + + ctx.setConfiguration(config); + ctx.updateLoggers(); + + org.apache.logging.log4j.Logger logger = LogManager.getLogger("test.multiple.appenders"); + + // When - log messages and flush buffers + logger.debug("Test message 1"); + logger.debug("Test message 2"); + + Log4jLoggingManager manager = new Log4jLoggingManager(); + manager.flushBuffer(); + + // Then - both appenders should have flushed their buffers + File logFile = new File("target/logfile.json"); + assertThat(logFile).exists(); + String content = contentOf(logFile); + // Each message should appear twice (once from each BufferingAppender) + assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts + assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts + } finally { + // Restore original configuration to prevent test interference + ctx.setConfiguration(originalConfig); + ctx.updateLoggers(); + } } @AfterEach From 5ed645d5be2f801bd62aa20952c9b281bb5e1dfc Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 12:34:15 +0000 Subject: [PATCH 17/45] Avoid logging config leakage in log4j unit test. --- .../log4j/internal/Log4jLoggingManagerTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index b7fb9385e..e3cc15560 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -18,9 +18,10 @@ import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.Configurator; import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,8 +32,13 @@ class Log4jLoggingManagerTest { private static final Logger LOG = LoggerFactory.getLogger(Log4jLoggingManagerTest.class); private static final Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + @BeforeEach + void setUp() { + // Force reconfiguration from XML to ensure clean state + Configurator.reconfigure(); + } + @Test - @Order(1) void getLogLevel_shouldReturnConfiguredLogLevel() { // Given log4j2.xml in resources @@ -47,7 +53,6 @@ void getLogLevel_shouldReturnConfiguredLogLevel() { } @Test - @Order(2) void resetLogLevel() { // Given log4j2.xml in resources @@ -109,6 +114,9 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException @AfterEach void cleanUp() throws IOException { + // Reset to original configuration from XML + Configurator.reconfigure(); + try { FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); } catch (NoSuchFileException e) { From 44ebc64520316c346ba1827dc90ce50f24252299 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 12:40:39 +0000 Subject: [PATCH 18/45] Avoid config leaking in logback unit tests. --- .../internal/Log4jLoggingManagerTest.java | 54 ++++++++----------- .../logging/LogbackLoggingManagerTest.java | 44 +++++++-------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java index e3cc15560..16036fe3e 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/internal/Log4jLoggingManagerTest.java @@ -78,38 +78,30 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException } LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - Configuration originalConfig = ctx.getConfiguration(); + ConfigurationFactory factory = new XmlConfigurationFactory(); + ConfigurationSource source = new ConfigurationSource( + getClass().getResourceAsStream("/log4j2-multiple-buffering.xml")); + Configuration config = factory.getConfiguration(null, source); - try { - ConfigurationFactory factory = new XmlConfigurationFactory(); - ConfigurationSource source = new ConfigurationSource( - getClass().getResourceAsStream("/log4j2-multiple-buffering.xml")); - Configuration config = factory.getConfiguration(null, source); - - ctx.setConfiguration(config); - ctx.updateLoggers(); - - org.apache.logging.log4j.Logger logger = LogManager.getLogger("test.multiple.appenders"); - - // When - log messages and flush buffers - logger.debug("Test message 1"); - logger.debug("Test message 2"); - - Log4jLoggingManager manager = new Log4jLoggingManager(); - manager.flushBuffer(); - - // Then - both appenders should have flushed their buffers - File logFile = new File("target/logfile.json"); - assertThat(logFile).exists(); - String content = contentOf(logFile); - // Each message should appear twice (once from each BufferingAppender) - assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts - assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts - } finally { - // Restore original configuration to prevent test interference - ctx.setConfiguration(originalConfig); - ctx.updateLoggers(); - } + ctx.setConfiguration(config); + ctx.updateLoggers(); + + org.apache.logging.log4j.Logger logger = LogManager.getLogger("test.multiple.appenders"); + + // When - log messages and flush buffers + logger.debug("Test message 1"); + logger.debug("Test message 2"); + + Log4jLoggingManager manager = new Log4jLoggingManager(); + manager.flushBuffer(); + + // Then - both appenders should have flushed their buffers + File logFile = new File("target/logfile.json"); + assertThat(logFile).exists(); + String content = contentOf(logFile); + // Each message should appear twice (once from each BufferingAppender) + assertThat(content.split("Test message 1", -1)).hasSize(3); // 2 occurrences = 3 parts + assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts } @AfterEach diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java index 35908d1f5..843b5d953 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -28,7 +28,7 @@ import java.nio.file.StandardOpenOption; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,8 +44,18 @@ class LogbackLoggingManagerTest { private static final Logger LOG = LoggerFactory.getLogger(LogbackLoggingManagerTest.class); private static final Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + @BeforeEach + void setUp() throws JoranException, IOException { + resetLogbackConfig("/logback-test.xml"); + + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + @Test - @Order(1) void getLogLevel_shouldReturnConfiguredLogLevel() { LogbackLoggingManager manager = new LogbackLoggingManager(); Level logLevel = manager.getLogLevel(LOG); @@ -56,7 +66,6 @@ void getLogLevel_shouldReturnConfiguredLogLevel() { } @Test - @Order(2) void resetLogLevel() { LogbackLoggingManager manager = new LogbackLoggingManager(); manager.setLogLevel(ERROR); @@ -68,18 +77,7 @@ void resetLogLevel() { @Test void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException, JoranException { // Given - configuration with multiple BufferingAppenders with different names - try { - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - } catch (NoSuchFileException e) { - // may not be there in the first run - } - - LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); - context.reset(); - - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(context); - configurator.doConfigure(getClass().getResourceAsStream("/logback-multiple-buffering.xml")); + resetLogbackConfig("/logback-multiple-buffering.xml"); Logger logger = LoggerFactory.getLogger("test.multiple.appenders"); @@ -100,11 +98,15 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException } @AfterEach - void cleanUp() throws IOException { - try { - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - } catch (NoSuchFileException e) { - // may not be there - } + void cleanUp() throws JoranException { + resetLogbackConfig("/logback-test.xml"); + } + + private void resetLogbackConfig(String configFileName) throws JoranException { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + context.reset(); + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + configurator.doConfigure(getClass().getResourceAsStream(configFileName)); } } From 47f403ba09ef9d7fbdfa7db24c87aebecd2081a8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 12:49:36 +0000 Subject: [PATCH 19/45] Update Graal metadata for powertools-logging. --- .../powertools-logging/jni-config.json | 16 - .../powertools-logging/reflect-config.json | 415 +----------------- .../powertools-logging/resource-config.json | 6 - 3 files changed, 14 insertions(+), 423 deletions(-) diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json index 2c4de0562..c8b081385 100644 --- a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json @@ -3,22 +3,6 @@ "name":"java.lang.Boolean", "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] }, -{ - "name":"java.lang.String", - "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] -}, -{ - "name":"org.apache.maven.surefire.booter.ForkedBooter", - "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] -}, -{ - "name":"sun.instrument.InstrumentationImpl", - "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] -}, { "name":"sun.management.VMManagementImpl", "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json index 7347b8400..4c66ebd97 100644 --- a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json @@ -23,21 +23,7 @@ "methods":[{"name":"","parameterTypes":[] }] }, { - "name":"com.amazonaws.services.lambda.runtime.Context", - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] -}, -{ - "name":"com.amazonaws.services.lambda.runtime.RequestHandler", - "allDeclaredClasses":true, - "queryAllPublicMethods":true -}, -{ - "name":"com.amazonaws.services.lambda.runtime.RequestStreamHandler", - "allDeclaredClasses":true, - "queryAllPublicMethods":true + "name":"com.amazonaws.services.lambda.runtime.Context" }, { "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", @@ -129,6 +115,7 @@ "name":"com.amazonaws.services.lambda.runtime.tests.EventArgumentsProvider", "queryAllDeclaredMethods":true, "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, "methods":[{"name":"","parameterTypes":[] }] }, { @@ -139,9 +126,6 @@ "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "methods":[{"name":"","parameterTypes":[] }] }, -{ - "name":"com.sun.tools.attach.VirtualMachine" -}, { "name":"double", "queryAllDeclaredMethods":true @@ -159,137 +143,43 @@ "name":"java.io.Serializable", "allDeclaredClasses":true, "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true + "queryAllPublicMethods":true }, { "name":"java.lang.Boolean" }, -{ - "name":"java.lang.Class", - "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] -}, -{ - "name":"java.lang.ClassLoader", - "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] -}, { "name":"java.lang.Cloneable", "queryAllDeclaredMethods":true }, { - "name":"java.lang.Comparable", - "allDeclaredClasses":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.Enum", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.Module", - "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] -}, -{ - "name":"java.lang.Object", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] + "name":"java.lang.Object" }, { "name":"java.lang.ProcessEnvironment", "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] }, { - "name":"java.lang.ProcessHandle", - "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] -}, -{ - "name":"java.lang.Runtime", - "methods":[{"name":"version","parameterTypes":[] }] + "name":"java.lang.String" }, { - "name":"java.lang.Runtime$Version", - "methods":[{"name":"feature","parameterTypes":[] }] -}, -{ - "name":"java.lang.StackWalker" -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getSecurityManager","parameterTypes":[] }] -}, -{ - "name":"java.lang.annotation.Retention", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true + "name":"java.util.AbstractMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true }, { - "name":"java.lang.annotation.Target", + "name":"java.util.Collections$SingletonMap", + "allDeclaredFields":true, "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, -{ - "name":"java.lang.constant.Constable", - "allDeclaredClasses":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.invoke.MethodHandle", - "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] -}, -{ - "name":"java.lang.invoke.MethodHandles", - "methods":[{"name":"lookup","parameterTypes":[] }] -}, -{ - "name":"java.lang.invoke.MethodHandles$Lookup", - "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] -}, -{ - "name":"java.lang.invoke.MethodType", - "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] -}, -{ - "name":"java.lang.reflect.AccessibleObject", - "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] -}, -{ - "name":"java.lang.reflect.AnnotatedArrayType", - "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.AnnotatedType", - "methods":[{"name":"getType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Executable", - "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Method", - "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Parameter", - "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] -}, -{ - "name":"java.security.AccessController", - "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] -}, { "name":"java.util.Collections$UnmodifiableMap", "fields":[{"name":"m"}] }, { - "name":"java.util.Map" -}, -{ - "name":"java.util.concurrent.ForkJoinTask", - "fields":[{"name":"aux"}, {"name":"status"}] + "name":"java.util.Map", + "queryAllDeclaredMethods":true }, { "name":"java.util.concurrent.atomic.AtomicBoolean", @@ -303,22 +193,10 @@ "name":"java.util.function.Consumer", "queryAllPublicMethods":true }, -{ - "name":"jdk.internal.misc.Unsafe" -}, -{ - "name":"kotlin.jvm.JvmInline" -}, { "name":"org.apiguardian.api.API", "queryAllPublicMethods":true }, -{ - "name":"org.aspectj.runtime.internal.AroundClosure", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, { "name":"org.joda.time.DateTime" }, @@ -349,42 +227,6 @@ "allDeclaredClasses":true, "queryAllPublicMethods":true }, -{ - "name":"org.slf4j.test.OutputChoice", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"org.slf4j.test.OutputChoice$OutputChoiceType", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"org.slf4j.test.TestLogger", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"org.slf4j.test.TestLoggerConfiguration", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"org.slf4j.test.TestLoggerFactory", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"org.slf4j.test.TestServiceProvider", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, { "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", "fields":[{"name":"IS_COLD_START"}, {"name":"SERVICE_NAME"}] @@ -396,232 +238,11 @@ "queryAllDeclaredMethods":true, "queryAllPublicMethods":true, "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"arrayArgument","parameterTypes":[] }, {"name":"jsonArgument","parameterTypes":[] }, {"name":"keyValueArgument","parameterTypes":[] }, {"name":"mapArgument","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.util.Map","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabled", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabledForStream", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"anotherMethod","parameterTypes":[] }, {"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled$AjcClosure3", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] + "methods":[{"name":"","parameterTypes":[] }, {"name":"arrayArgument","parameterTypes":[] }, {"name":"emptyMapArgument","parameterTypes":[] }, {"name":"jsonArgument","parameterTypes":[] }, {"name":"keyValueArgument","parameterTypes":[] }, {"name":"mapArgument","parameterTypes":[] }, {"name":"reservedKeywordArgumentIgnored","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }] }, { - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId$AjcClosure1", + "name":"software.amazon.lambda.powertools.logging.internal.BufferManager", "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, "queryAllPublicMethods":true }, { @@ -647,23 +268,15 @@ { "name":"software.amazon.lambda.powertools.logging.model.Basket", "allDeclaredFields":true, - "allDeclaredClasses":true, "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, "queryAllDeclaredConstructors":true, "methods":[{"name":"getProducts","parameterTypes":[] }] }, { "name":"software.amazon.lambda.powertools.logging.model.Product", "allDeclaredFields":true, - "allDeclaredClasses":true, "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, "queryAllDeclaredConstructors":true, "methods":[{"name":"getId","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPrice","parameterTypes":[] }] -}, -{ - "name":"sun.reflect.ReflectionFactory", - "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] } ] diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json index ca77675e0..832be3d72 100644 --- a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json @@ -2,8 +2,6 @@ "resources":{ "includes":[{ "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" - }, { - "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" }, { "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" }, { @@ -14,10 +12,6 @@ "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" }, { "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" - }, { - "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/Europe/Berlin\\E" - }, { - "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/ZoneInfoMap\\E" }]}, "bundles":[] } From 6f4e4c3161eb6e92a372d403c259c43bf9d4ed65 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 14:16:03 +0000 Subject: [PATCH 20/45] Update Graal metadata for powertools-logging-log4j. --- .../powertools-logging-log4j/jni-config.json | 16 -- .../reflect-config.json | 258 ++++-------------- .../resource-config.json | 56 ---- 3 files changed, 60 insertions(+), 270 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json index 2c4de0562..c8b081385 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json @@ -3,22 +3,6 @@ "name":"java.lang.Boolean", "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] }, -{ - "name":"java.lang.String", - "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] -}, -{ - "name":"org.apache.maven.surefire.booter.ForkedBooter", - "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] -}, -{ - "name":"sun.instrument.InstrumentationImpl", - "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] -}, { "name":"sun.management.VMManagementImpl", "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json index 9b2afe183..adbd9e0c1 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json @@ -1,4 +1,10 @@ [ +{ + "name":"[Ljava.lang.Object;" +}, +{ + "name":"[Ljava.lang.String;" +}, { "name":"[Lorg.apache.logging.log4j.core.Appender;" }, @@ -15,16 +21,7 @@ "name":"[Lorg.apache.logging.log4j.layout.template.json.JsonTemplateLayout$EventTemplateAdditionalField;" }, { - "name":"com.amazonaws.services.lambda.runtime.Context", - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] -}, -{ - "name":"com.amazonaws.services.lambda.runtime.RequestHandler", - "allDeclaredClasses":true, - "queryAllPublicMethods":true + "name":"com.amazonaws.services.lambda.runtime.Context" }, { "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$MessageAttribute", @@ -60,155 +57,88 @@ "name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", "methods":[{"name":"","parameterTypes":[] }] }, -{ - "name":"com.sun.tools.attach.VirtualMachine" -}, { "name":"jakarta.servlet.Servlet" }, { "name":"java.io.Serializable", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.Class", - "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] -}, -{ - "name":"java.lang.ClassLoader", - "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] + "queryAllDeclaredMethods":true }, { "name":"java.lang.Cloneable", "queryAllDeclaredMethods":true }, { - "name":"java.lang.Comparable", - "allDeclaredClasses":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.Enum", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.Module", - "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] + "name":"java.lang.Iterable", + "queryAllDeclaredMethods":true }, { "name":"java.lang.Object", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] + "allDeclaredFields":true }, { "name":"java.lang.ProcessEnvironment", "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] }, -{ - "name":"java.lang.ProcessHandle", - "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] -}, -{ - "name":"java.lang.Runtime", - "methods":[{"name":"version","parameterTypes":[] }] -}, -{ - "name":"java.lang.Runtime$Version", - "methods":[{"name":"feature","parameterTypes":[] }] -}, -{ - "name":"java.lang.StackWalker" -}, { "name":"java.lang.String" }, -{ - "name":"java.lang.System", - "methods":[{"name":"getSecurityManager","parameterTypes":[] }] -}, { "name":"java.lang.Thread", "fields":[{"name":"threadLocalRandomProbe"}] }, { - "name":"java.lang.annotation.Retention", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.annotation.Target", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.constant.Constable", - "allDeclaredClasses":true, - "queryAllPublicMethods":true -}, -{ - "name":"java.lang.invoke.MethodHandle", - "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] -}, -{ - "name":"java.lang.invoke.MethodHandles", - "methods":[{"name":"lookup","parameterTypes":[] }] -}, -{ - "name":"java.lang.invoke.MethodHandles$Lookup", - "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] -}, -{ - "name":"java.lang.invoke.MethodType", - "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] + "name":"java.sql.Date" }, { - "name":"java.lang.reflect.AccessibleObject", - "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] + "name":"java.sql.Time" }, { - "name":"java.lang.reflect.AnnotatedArrayType", - "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] + "name":"java.util.AbstractCollection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true }, { - "name":"java.lang.reflect.AnnotatedType", - "methods":[{"name":"getType","parameterTypes":[] }] + "name":"java.util.AbstractList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true }, { - "name":"java.lang.reflect.Executable", - "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] + "name":"java.util.AbstractMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true }, { - "name":"java.lang.reflect.Method", - "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] + "name":"java.util.Arrays$ArrayList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true }, { - "name":"java.lang.reflect.Parameter", - "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] + "name":"java.util.Collection", + "queryAllDeclaredMethods":true }, { - "name":"java.security.AccessController", - "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] + "name":"java.util.Collections$SingletonMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true }, { - "name":"java.sql.Date" + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] }, { - "name":"java.sql.Time" + "name":"java.util.List", + "queryAllDeclaredMethods":true }, { - "name":"java.util.Collections$UnmodifiableMap", - "fields":[{"name":"m"}] + "name":"java.util.Map", + "queryAllDeclaredMethods":true }, { - "name":"java.util.concurrent.ForkJoinTask", - "fields":[{"name":"aux"}, {"name":"status"}] + "name":"java.util.RandomAccess", + "queryAllDeclaredMethods":true }, { "name":"java.util.concurrent.atomic.AtomicBoolean", @@ -230,10 +160,7 @@ "name":"javax.servlet.Servlet" }, { - "name":"jdk.internal.misc.Unsafe" -}, -{ - "name":"kotlin.jvm.JvmInline" + "name":"kotlin.Metadata" }, { "name":"org.apache.logging.log4j.core.appender.AbstractAppender$Builder", @@ -493,6 +420,12 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"createLoggers","parameterTypes":["org.apache.logging.log4j.core.config.LoggerConfig[]"] }] }, +{ + "name":"org.apache.logging.log4j.core.config.MonitorResource" +}, +{ + "name":"org.apache.logging.log4j.core.config.MonitorResources" +}, { "name":"org.apache.logging.log4j.core.config.PropertiesPlugin" }, @@ -743,7 +676,13 @@ "name":"org.apache.logging.log4j.core.layout.MessageLayout" }, { - "name":"org.apache.logging.log4j.core.layout.PatternLayout" + "name":"org.apache.logging.log4j.core.layout.PatternLayout", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.layout.PatternLayout$Builder", + "allDeclaredFields":true }, { "name":"org.apache.logging.log4j.core.layout.PatternMatch" @@ -878,9 +817,7 @@ "name":"org.apache.logging.log4j.core.pattern.ClassNamePatternConverter" }, { - "name":"org.apache.logging.log4j.core.pattern.DatePatternConverter", - "queryAllDeclaredMethods":true, - "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] + "name":"org.apache.logging.log4j.core.pattern.DatePatternConverter" }, { "name":"org.apache.logging.log4j.core.pattern.EncodingPatternConverter" @@ -913,9 +850,7 @@ "name":"org.apache.logging.log4j.core.pattern.IntegerPatternConverter" }, { - "name":"org.apache.logging.log4j.core.pattern.LevelPatternConverter", - "queryAllDeclaredMethods":true, - "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] + "name":"org.apache.logging.log4j.core.pattern.LevelPatternConverter" }, { "name":"org.apache.logging.log4j.core.pattern.LineLocationPatternConverter" @@ -929,9 +864,7 @@ "name":"org.apache.logging.log4j.core.pattern.LoggerFqcnPatternConverter" }, { - "name":"org.apache.logging.log4j.core.pattern.LoggerPatternConverter", - "queryAllDeclaredMethods":true, - "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] + "name":"org.apache.logging.log4j.core.pattern.LoggerPatternConverter" }, { "name":"org.apache.logging.log4j.core.pattern.MapPatternConverter" @@ -990,9 +923,7 @@ "name":"org.apache.logging.log4j.core.pattern.ThreadIdPatternConverter" }, { - "name":"org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter", - "queryAllDeclaredMethods":true, - "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] + "name":"org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter" }, { "name":"org.apache.logging.log4j.core.pattern.ThreadPriorityPatternConverter" @@ -1105,29 +1036,6 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"getInstance","parameterTypes":[] }] }, -{ - "name":"org.apache.logging.log4j.layout.template.json.resolver.PowerToolsResolverFactoryTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogInEcsFormat","parameterTypes":[] }, {"name":"shouldLogInJsonFormat","parameterTypes":[] }] -}, -{ - "name":"org.apache.logging.log4j.layout.template.json.resolver.PowertoolsResolverArgumentsTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingKeyValue","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingRawJson","parameterTypes":[] }] -}, -{ - "name":"org.apache.logging.log4j.layout.template.json.resolver.PowertoolsResolverFactory", - "queryAllDeclaredMethods":true, - "methods":[{"name":"getInstance","parameterTypes":[] }] -}, { "name":"org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory", "queryAllDeclaredMethods":true, @@ -1161,12 +1069,6 @@ "name":"org.apiguardian.api.API", "queryAllPublicMethods":true }, -{ - "name":"org.aspectj.runtime.internal.AroundClosure", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, { "name":"org.jctools.queues.MpmcArrayQueue" }, @@ -1178,48 +1080,8 @@ "fields":[{"name":"IS_COLD_START"}] }, { - "name":"software.amazon.lambda.powertools.logging.internal.Log4jLoggingManagerTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"getLogLevel_shouldReturnConfiguredLogLevel","parameterTypes":[] }, {"name":"resetLogLevel","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments", - "allDeclaredClasses":true, + "name":"software.amazon.lambda.powertools.logging.log4j.BufferingAppender", "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments$ArgumentFormat", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled$AjcClosure1", - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true -}, -{ - "name":"sun.reflect.ReflectionFactory", - "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] + "methods":[{"name":"createAppender","parameterTypes":["java.lang.String","org.apache.logging.log4j.core.Filter","org.apache.logging.log4j.core.Layout","org.apache.logging.log4j.core.config.AppenderRef[]","org.apache.logging.log4j.core.config.Configuration","java.lang.String","int","boolean"] }] } ] diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json index aca0e0356..cf017fdeb 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json @@ -12,8 +12,6 @@ "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" }, { "pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E" - }, { - "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" }, { "pattern":"\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E" }, { @@ -36,60 +34,6 @@ "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" }, { "pattern":"\\QStackTraceElementLayout.json\\E" - }, { - "pattern":"\\Qlog4j2-test.jsn\\E" - }, { - "pattern":"\\Qlog4j2-test.json\\E" - }, { - "pattern":"\\Qlog4j2-test.properties\\E" - }, { - "pattern":"\\Qlog4j2-test.xml\\E" - }, { - "pattern":"\\Qlog4j2-test.yaml\\E" - }, { - "pattern":"\\Qlog4j2-test.yml\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.jsn\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.json\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.properties\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.xml\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.yaml\\E" - }, { - "pattern":"\\Qlog4j2-test18b4aac2.yml\\E" - }, { - "pattern":"\\Qlog4j2.StatusLogger.properties\\E" - }, { - "pattern":"\\Qlog4j2.component.properties\\E" - }, { - "pattern":"\\Qlog4j2.jsn\\E" - }, { - "pattern":"\\Qlog4j2.json\\E" - }, { - "pattern":"\\Qlog4j2.properties\\E" - }, { - "pattern":"\\Qlog4j2.system.properties\\E" - }, { - "pattern":"\\Qlog4j2.xml\\E" - }, { - "pattern":"\\Qlog4j2.yaml\\E" - }, { - "pattern":"\\Qlog4j2.yml\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.jsn\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.json\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.properties\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.xml\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.yaml\\E" - }, { - "pattern":"\\Qlog4j218b4aac2.yml\\E" }]}, "bundles":[] } From 6933e1d6b5d5414d1efb3102b0628c396fd806d8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 14:18:58 +0000 Subject: [PATCH 21/45] Update Graal metadata for powertools-logging-logback. --- .../jni-config.json | 12 --- .../reflect-config.json | 85 +++++++------------ .../resource-config.json | 4 - 3 files changed, 33 insertions(+), 68 deletions(-) diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json index 753dafdea..c8b081385 100644 --- a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json @@ -3,18 +3,6 @@ "name":"java.lang.Boolean", "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] }, -{ - "name":"java.lang.String", - "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] -}, -{ - "name":"org.apache.maven.surefire.booter.ForkedBooter", - "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] -}, { "name":"sun.management.VMManagementImpl", "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json index 683933a77..dfc50427f 100644 --- a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json @@ -1,4 +1,15 @@ [ +{ + "name":"[Ljava.lang.Object;" +}, +{ + "name":"[Ljava.lang.String;" +}, +{ + "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"ch.qos.logback.classic.joran.SerializedModelConfigurator", "methods":[{"name":"","parameterTypes":[] }] @@ -20,6 +31,18 @@ "name":"ch.qos.logback.core.encoder.Encoder", "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] }, +{ + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] +}, +{ + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.spi.ContextAware", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, { "name":"com.amazonaws.services.lambda.runtime.Context" }, @@ -69,6 +92,10 @@ { "name":"java.lang.Object" }, +{ + "name":"java.lang.ProcessEnvironment", + "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] +}, { "name":"java.lang.String" }, @@ -103,6 +130,10 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, +{ + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] +}, { "name":"java.util.List", "queryAllDeclaredMethods":true @@ -135,59 +166,9 @@ "fields":[{"name":"IS_COLD_START"}] }, { - "name":"software.amazon.lambda.powertools.logging.LogbackLoggingManagerTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"getLogLevel_shouldReturnConfiguredLogLevel","parameterTypes":[] }, {"name":"resetLogLevel","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.LambdaEcsEncoderTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogException","parameterTypes":[] }, {"name":"shouldLogInEcsFormat","parameterTypes":[] }, {"name":"shouldNotLogCloudInfo","parameterTypes":[] }, {"name":"shouldNotLogFunctionInfo","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, + "name":"software.amazon.lambda.powertools.logging.logback.BufferingAppender", "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingKeyValue","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingRawJson","parameterTypes":[] }, {"name":"shouldLogEventAsStringForStreamHandler","parameterTypes":[] }, {"name":"shouldLogEventForHandlerWhenEnvVariableSetToTrue","parameterTypes":[] }, {"name":"shouldLogEventForHandlerWithLogEventAnnotation","parameterTypes":[] }, {"name":"shouldLogException","parameterTypes":[] }, {"name":"shouldLogInJsonFormat","parameterTypes":[] }, {"name":"shouldLogResponseForHandlerWhenEnvVariableSetToTrue","parameterTypes":[] }, {"name":"shouldLogResponseForHandlerWithLogResponseAnnotation","parameterTypes":[] }, {"name":"shouldLogResponseForStreamHandler","parameterTypes":[] }, {"name":"shouldLogStructuredArgumentsAsNewEntries","parameterTypes":[] }, {"name":"shouldLogThreadInfo","parameterTypes":[] }, {"name":"shouldLogTimestampDifferently","parameterTypes":[] }, {"name":"shouldNotLogEventForHandlerWhenEnvVariableSetToFalse","parameterTypes":[] }, {"name":"shouldNotLogPowertoolsInfo","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments", - "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEvent", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventDisabled", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventForStream", - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponse", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponseForStream", - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBufferAtVerbosity","parameterTypes":["java.lang.String"] }, {"name":"setFlushOnErrorLog","parameterTypes":["boolean"] }, {"name":"setMaxBytes","parameterTypes":["int"] }] }, { "name":"software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder", diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json index 2fc3c56bd..33d1d61c4 100644 --- a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json @@ -4,16 +4,12 @@ "pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" }, { "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" - }, { - "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" }, { "pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" }, { "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" }, { "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" - }, { - "pattern":"\\Qlogback.scmo\\E" }]}, "bundles":[] } From 15efff4269a0f53c242a25bb5d5cf6408805b5ec Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 1 Sep 2025 18:40:02 +0200 Subject: [PATCH 22/45] Address Sonar findings. --- .../logging/log4j/BufferingAppenderTest.java | 2 +- .../logging/LogbackLoggingManagerTest.java | 4 +- .../logback/BufferingAppenderTest.java | 2 +- .../lambda/powertools/logging/Logging.java | 2 +- .../logging/internal/LambdaLoggingAspect.java | 102 ++++++++++-------- .../internal/LoggingManagerRegistryTest.java | 12 ++- 6 files changed, 70 insertions(+), 54 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java index 689c75eaf..434d3983b 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppenderTest.java @@ -116,7 +116,7 @@ void shouldClearBufferManually() { void shouldLogOverflowWarningWhenBufferOverflows() { // When - fill buffer beyond capacity to trigger overflow for (int i = 0; i < 100; i++) { - logger.debug("Debug message " + i); + logger.debug("Debug message {}", i); } // When - flush buffer to trigger overflow warning diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java index 843b5d953..a880beace 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -47,7 +47,7 @@ class LogbackLoggingManagerTest { @BeforeEach void setUp() throws JoranException, IOException { resetLogbackConfig("/logback-test.xml"); - + try { FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); } catch (NoSuchFileException e) { @@ -75,7 +75,7 @@ void resetLogLevel() { } @Test - void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws IOException, JoranException { + void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws JoranException { // Given - configuration with multiple BufferingAppenders with different names resetLogbackConfig("/logback-multiple-buffering.xml"); diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java index e4a3dc593..62d2e3056 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/logback/BufferingAppenderTest.java @@ -130,7 +130,7 @@ void shouldClearBufferManually() { void shouldLogOverflowWarningWhenBufferOverflows() { // When - fill buffer beyond capacity to trigger overflow for (int i = 0; i < 100; i++) { - logger.debug("Debug message " + i); + logger.debug("Debug message {}", i); } // When - flush buffer to trigger overflow warning diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java index a0685a256..79d1a95fd 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java @@ -72,7 +72,7 @@ /** * Set to true if you want to log the response sent by the Lambda function handler.
    - * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_RESPONE' environment variable + * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_RESPONSE' environment variable */ boolean logResponse() default false; 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 b9e01da92..d7d956963 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 @@ -130,12 +130,9 @@ public Object around(ProceedingJoinPoint pjp, boolean isOnRequestStreamHandler = placedOnStreamHandler(pjp); setLogLevelBasedOnSamplingRate(pjp, logging); - addLambdaContextToLoggingContext(pjp); - getXrayTraceId().ifPresent(xRayTraceId -> MDC.put(FUNCTION_TRACE_ID.getName(), xRayTraceId)); - // Log Event Object[] proceedArgs = logEvent(pjp, logging, isOnRequestHandler, isOnRequestStreamHandler); if (!logging.correlationIdPath().isEmpty()) { @@ -143,58 +140,28 @@ public Object around(ProceedingJoinPoint pjp, isOnRequestStreamHandler); } - // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: - // 1. backup a reference to the OutputStream provided by Lambda - // 2. create a temporary OutputStream and pass it to the handler method - // 3. retrieve this temporary stream to log it (if enabled) - // 4. write it back to the OutputStream provided by Lambda OutputStream backupOutputStream = null; - if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE) && isOnRequestStreamHandler) { - backupOutputStream = (OutputStream) proceedArgs[1]; - proceedArgs[1] = new ByteArrayOutputStream(); + if (isOnRequestStreamHandler) { + // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: + // 1. backup a reference to the OutputStream provided by Lambda + // 2. create a temporary OutputStream and pass it to the handler method + // 3. retrieve this temporary stream to log it (if enabled) + // 4. write it back to the OutputStream provided by Lambda + backupOutputStream = prepareOutputStreamForLogging(logging, proceedArgs); } Object lambdaFunctionResponse; - try { - // Call Function Handler lambdaFunctionResponse = pjp.proceed(proceedArgs); } catch (Throwable t) { - if (LOGGING_MANAGER instanceof BufferManager) { - if (logging.flushBufferOnUncaughtError()) { - ((BufferManager) LOGGING_MANAGER).flushBuffer(); - } else { - // Clear buffer before error logging to prevent unintended flush. If flushOnErrorLog is enabled on - // the appender the next line would otherwise cause an unintended flush by the appender directly. - ((BufferManager) LOGGING_MANAGER).clearBuffer(); - } - } - if (logging.logError() || POWERTOOLS_LOG_ERROR) { - // logging the exception with additional context - logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t); - } + handleException(pjp, logging, t); throw t; } finally { - if (logging.clearState()) { - MDC.clear(); - } - // Clear buffer after each handler invocation - if (LOGGING_MANAGER instanceof BufferManager) { - ((BufferManager) LOGGING_MANAGER).clearBuffer(); - } - coldStartDone(); + performCleanup(logging); } - // Log Response - if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { - if (isOnRequestHandler) { - logRequestHandlerResponse(pjp, lambdaFunctionResponse); - } else if (isOnRequestStreamHandler && backupOutputStream != null) { - byte[] bytes = ((ByteArrayOutputStream) proceedArgs[1]).toByteArray(); - logRequestStreamHandlerResponse(pjp, bytes); - backupOutputStream.write(bytes); - } - } + logResponse(pjp, logging, lambdaFunctionResponse, isOnRequestHandler, isOnRequestStreamHandler, + backupOutputStream, proceedArgs); return lambdaFunctionResponse; } @@ -355,6 +322,53 @@ private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws } } + private OutputStream prepareOutputStreamForLogging(Logging logging, + Object[] proceedArgs) { + if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { + OutputStream backupOutputStream = (OutputStream) proceedArgs[1]; + proceedArgs[1] = new ByteArrayOutputStream(); + return backupOutputStream; + } + return null; + } + + private void handleException(ProceedingJoinPoint pjp, Logging logging, Throwable t) { + if (LOGGING_MANAGER instanceof BufferManager) { + if (logging.flushBufferOnUncaughtError()) { + ((BufferManager) LOGGING_MANAGER).flushBuffer(); + } else { + ((BufferManager) LOGGING_MANAGER).clearBuffer(); + } + } + if (logging.logError() || POWERTOOLS_LOG_ERROR) { + logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t); + } + } + + private void performCleanup(Logging logging) { + if (logging.clearState()) { + MDC.clear(); + } + if (LOGGING_MANAGER instanceof BufferManager) { + ((BufferManager) LOGGING_MANAGER).clearBuffer(); + } + coldStartDone(); + } + + private void logResponse(ProceedingJoinPoint pjp, Logging logging, Object lambdaFunctionResponse, + boolean isOnRequestHandler, boolean isOnRequestStreamHandler, + OutputStream backupOutputStream, Object[] proceedArgs) throws IOException { + if (logging.logResponse() || POWERTOOLS_LOG_RESPONSE) { + if (isOnRequestHandler) { + logRequestHandlerResponse(pjp, lambdaFunctionResponse); + } else if (isOnRequestStreamHandler && backupOutputStream != null) { + byte[] bytes = ((ByteArrayOutputStream) proceedArgs[1]).toByteArray(); + logRequestStreamHandlerResponse(pjp, bytes); + backupOutputStream.write(bytes); + } + } + } + private Logger logger(final ProceedingJoinPoint pjp) { return LoggerFactory.getLogger(pjp.getSignature().getDeclaringType()); } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java index 4807870b9..6d293e68d 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -91,8 +91,9 @@ void testSingleLoggingManager_shouldReturnWithoutWarning() throws UnsupportedEnc // THEN String output = outputStream.toString("UTF-8"); assertThat(output).isEmpty(); - assertThat(loggingManager).isSameAs(testManager); - assertThat(loggingManager).isInstanceOf(BufferManager.class); + assertThat(loggingManager) + .isSameAs(testManager) + .isInstanceOf(BufferManager.class); } @Test @@ -102,9 +103,10 @@ void testGetLoggingManager_shouldReturnSameInstance() { LoggingManager second = LoggingManagerRegistry.getLoggingManager(); // THEN - assertThat(first).isSameAs(second); - assertThat(first).isNotNull(); - assertThat(first).isInstanceOf(BufferManager.class); + assertThat(first) + .isSameAs(second) + .isNotNull() + .isInstanceOf(BufferManager.class); } @Test From 95a11b1018f6218286bc3542ffb8cdf72bf25269 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 11:34:22 +0200 Subject: [PATCH 23/45] Address pmd findings. --- .github/pmd-ruleset.xml | 74 ++++++++++--------- .../amazon/lambda/powertools/e2e/Input.java | 3 - .../amazon/lambda/powertools/e2e/Input.java | 3 - .../amazon/lambda/powertools/e2e/Input.java | 3 - .../amazon/lambda/powertools/e2e/Input.java | 3 - .../logging/internal/LambdaLoggingAspect.java | 4 +- 6 files changed, 43 insertions(+), 47 deletions(-) diff --git a/.github/pmd-ruleset.xml b/.github/pmd-ruleset.xml index b93fa19b8..5deb71e47 100644 --- a/.github/pmd-ruleset.xml +++ b/.github/pmd-ruleset.xml @@ -1,13 +1,14 @@ + xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> Rules to check Powertools for Lambda - + @@ -516,10 +522,10 @@ + language="java" + since="5.0" + message="replace o.getClass().equals(MyClass.class) with o instanceof MyClass" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> replace o.getClass().equals(MyClass.class) with o instanceof MyClass. Make sure MyClass doesn't have descendants 1 @@ -536,10 +542,10 @@ + language="java" + since="5.0" + message="replace MyClass.class.equals(o.getClass()) with o instanceof MyClass" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> replace MyClass.class.equals(o.getClass()) with o instanceof MyClass. Make sure MyClass doesn't have descendants 3 @@ -556,10 +562,10 @@ + language="java" + message="Don't call super.visit() when using the rulechain" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Calling super.visit breaks the rulechain, by starting a full visitor run from the passed node downwards. Add all needed nodes to the rulechain instead. 1 @@ -583,10 +589,10 @@ + language="java" + message="Always call super.visit() when not using rulechain" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Just returning without calling super stops visiting of nested nodes like inner classes. 3 @@ -606,10 +612,10 @@ + language="java" + message="Reuse InvocationMatcher" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Share the invocation matcher and not create a new one every time 1 @@ -625,10 +631,10 @@ + language="java" + since="7.0.0" + message="Use slf4j: LoggerFactory.getLogger(MyClass.class)" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Use slf4j: LoggerFactory.getLogger(MyClass.class) 1 @@ -641,4 +647,4 @@ - \ No newline at end of file + diff --git a/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index cc449922e..66fd49ddc 100644 --- a/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/logging-log4j/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -20,9 +20,6 @@ public class Input { private String message; private Map keys; - public Input() { - } - public String getMessage() { return message; } diff --git a/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index cc449922e..66fd49ddc 100644 --- a/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/logging-logback/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -20,9 +20,6 @@ public class Input { private String message; private Map keys; - public Input() { - } - public String getMessage() { return message; } diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index 1328ded77..054c2867a 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -23,9 +23,6 @@ public class Input { private String highResolution; - public Input() { - } - public Map getMetrics() { return metrics; } diff --git a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index 92078d0b3..ed89f4498 100644 --- a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -17,9 +17,6 @@ public class Input { private String message; - public Input() { - } - public String getMessage() { return message; } 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 d7d956963..6add93931 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 @@ -43,6 +43,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.Arrays; +import java.util.Locale; import java.util.Random; import org.aspectj.lang.ProceedingJoinPoint; @@ -99,7 +100,7 @@ static void setLogLevel() { private static Level getLevelFromString(String level) { if (Arrays.stream(Level.values()).anyMatch(slf4jLevel -> slf4jLevel.name().equalsIgnoreCase(level))) { - return Level.valueOf(level.toUpperCase()); + return Level.valueOf(level.toUpperCase(Locale.ROOT)); } else { // FATAL does not exist in slf4j if ("FATAL".equalsIgnoreCase(level)) { @@ -117,6 +118,7 @@ private static void setLogLevels(Level logLevel) { @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(logging)") public void callAt(Logging logging) { + // Pointcut method - body intentionally empty } /** From 82e0d17788c6927f63f7d39a04fbda416eeed794 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 11:47:39 +0200 Subject: [PATCH 24/45] Set allowCommentedBlocks=true in pmd rules. --- .github/pmd-ruleset.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pmd-ruleset.xml b/.github/pmd-ruleset.xml index 5deb71e47..ed64a03cb 100644 --- a/.github/pmd-ruleset.xml +++ b/.github/pmd-ruleset.xml @@ -354,7 +354,7 @@ 1 - + From 3bfbbe889698cb51f3853575d702400bc5c6dfc8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 12:09:59 +0200 Subject: [PATCH 25/45] Fix thread-safety issue in double-checked singleton instance creation. --- .../logging/internal/LambdaLoggingAspect.java | 6 ++-- .../internal/LoggingManagerRegistry.java | 11 ++++--- .../logging/internal/KeyBufferTest.java | 32 +++++++++++-------- .../internal/LoggingManagerRegistryTest.java | 9 ++++-- 4 files changed, 33 insertions(+), 25 deletions(-) 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 6add93931..72e55b8a5 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 @@ -312,8 +312,8 @@ private void setCorrelationIdFromNode(String correlationIdPath, JsonNode jsonNod private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { - OutputStreamWriter writer = new OutputStreamWriter(out, UTF_8); + InputStreamReader reader = new InputStreamReader(inputStream, UTF_8); + OutputStreamWriter writer = new OutputStreamWriter(out, UTF_8)) { int n; char[] buffer = new char[4096]; while (-1 != (n = reader.read(buffer))) { @@ -326,7 +326,7 @@ private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws private OutputStream prepareOutputStreamForLogging(Logging logging, Object[] proceedArgs) { - if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { + if (logging.logResponse() || POWERTOOLS_LOG_RESPONSE) { OutputStream backupOutputStream = (OutputStream) proceedArgs[1]; proceedArgs[1] = new ByteArrayOutputStream(); return backupOutputStream; diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java index 5bcc6d382..dfede8f43 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; -import java.util.concurrent.atomic.AtomicReference; /** * Thread-safe singleton registry for LoggingManager instances. @@ -28,7 +27,9 @@ */ public final class LoggingManagerRegistry { - private static final AtomicReference instance = new AtomicReference<>(); + // Used with double-checked locking within getLoggingManger() + @SuppressWarnings("java:S3077") + private static volatile LoggingManager instance; private LoggingManagerRegistry() { // Utility class @@ -40,13 +41,13 @@ private LoggingManagerRegistry() { * @return the LoggingManager instance */ public static LoggingManager getLoggingManager() { - LoggingManager manager = instance.get(); + LoggingManager manager = instance; if (manager == null) { synchronized (LoggingManagerRegistry.class) { - manager = instance.get(); + manager = instance; if (manager == null) { manager = loadLoggingManager(); - instance.set(manager); + instance = manager; } } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index f97842e91..9b5407c78 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -248,16 +248,18 @@ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { }); } - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - - // Verify each key has its events - for (int i = 0; i < threadCount; i++) { - String key = "key" + i; - Deque events = buffer.removeAll(key); - assertThat(events).isNotNull().isNotEmpty(); + try { + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Verify each key has its events + for (int i = 0; i < threadCount; i++) { + String key = "key" + i; + Deque events = buffer.removeAll(key); + assertThat(events).isNotNull().isNotEmpty(); + } + } finally { + executor.shutdown(); } - - executor.shutdown(); } @Test @@ -281,12 +283,14 @@ void shouldBeThreadSafeForSameKey() throws InterruptedException { }); } - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - - Deque events = buffer.removeAll("sharedKey"); - assertThat(events).isNotNull().isNotEmpty(); + try { + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - executor.shutdown(); + Deque events = buffer.removeAll("sharedKey"); + assertThat(events).isNotNull().isNotEmpty(); + } finally { + executor.shutdown(); + } } @Test diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java index 6d293e68d..b4bc25cad 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -131,8 +131,11 @@ void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException { } // THEN - latch.await(5, TimeUnit.SECONDS); - executor.shutdown(); - assertThat(sharedInstance.get()).isNotNull(); + try { + latch.await(5, TimeUnit.SECONDS); + assertThat(sharedInstance.get()).isNotNull(); + } finally { + executor.shutdown(); + } } } From 7652b931418a6e617e2e3e53fc4c3b3a52a83a1b Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 12:18:08 +0200 Subject: [PATCH 26/45] Increase scope of thread-safety tests in KeyBuffer. --- .../logging/internal/KeyBufferTest.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index 9b5407c78..0d882eca4 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -231,31 +231,34 @@ void shouldNotLogWarningWhenNoOverflow() { void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { int threadCount = 10; int eventsPerThread = 100; + KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - // Each thread works with different key - for (int i = 0; i < threadCount; i++) { - final String key = "key" + i; - executor.submit(() -> { - try { - for (int j = 0; j < eventsPerThread; j++) { - buffer.add(key, "event" + j); + try { + CountDownLatch latch = new CountDownLatch(threadCount); + + // Each thread works with different key + for (int i = 0; i < threadCount; i++) { + final String key = "key" + i; + executor.submit(() -> { + try { + for (int j = 0; j < eventsPerThread; j++) { + largeBuffer.add(key, "event" + j); + } + } finally { + latch.countDown(); } - } finally { - latch.countDown(); - } - }); - } + }); + } - try { assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify each key has its events for (int i = 0; i < threadCount; i++) { String key = "key" + i; - Deque events = buffer.removeAll(key); - assertThat(events).isNotNull().isNotEmpty(); + Deque events = largeBuffer.removeAll(key); + assertThat(events) + .isNotNull() + .hasSize(eventsPerThread); } } finally { executor.shutdown(); @@ -266,28 +269,31 @@ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { void shouldBeThreadSafeForSameKey() throws InterruptedException { int threadCount = 5; int eventsPerThread = 20; + KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - // All threads work with same key - for (int i = 0; i < threadCount; i++) { - final int threadId = i; - executor.submit(() -> { - try { - for (int j = 0; j < eventsPerThread; j++) { - buffer.add("sharedKey", "t" + threadId + "e" + j); + try { + CountDownLatch latch = new CountDownLatch(threadCount); + + // All threads work with same key + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + for (int j = 0; j < eventsPerThread; j++) { + largeBuffer.add("sharedKey", "t" + threadId + "e" + j); + } + } finally { + latch.countDown(); } - } finally { - latch.countDown(); - } - }); - } + }); + } - try { assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - Deque events = buffer.removeAll("sharedKey"); - assertThat(events).isNotNull().isNotEmpty(); + Deque events = largeBuffer.removeAll("sharedKey"); + assertThat(events) + .isNotNull() + .hasSize(threadCount * eventsPerThread); } finally { executor.shutdown(); } From d6d514d01145e6721488c600a646d3035c041cfc Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 12:22:49 +0200 Subject: [PATCH 27/45] Try to satisfy pmd executor service closing. --- .../logging/internal/KeyBufferTest.java | 13 +++--- .../internal/LoggingManagerRegistryTest.java | 42 +++++++++---------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index 0d882eca4..7351e11c0 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.PrintStream; import java.nio.channels.FileChannel; @@ -228,12 +229,12 @@ void shouldNotLogWarningWhenNoOverflow() { } @Test - void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { + void shouldBeThreadSafeForDifferentKeys() throws InterruptedException, IOException { int threadCount = 10; int eventsPerThread = 100; KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - try { + try (Closeable close = executor::shutdown) { CountDownLatch latch = new CountDownLatch(threadCount); // Each thread works with different key @@ -260,18 +261,16 @@ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException { .isNotNull() .hasSize(eventsPerThread); } - } finally { - executor.shutdown(); } } @Test - void shouldBeThreadSafeForSameKey() throws InterruptedException { + void shouldBeThreadSafeForSameKey() throws InterruptedException, IOException { int threadCount = 5; int eventsPerThread = 20; KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - try { + try (Closeable close = executor::shutdown) { CountDownLatch latch = new CountDownLatch(threadCount); // All threads work with same key @@ -294,8 +293,6 @@ void shouldBeThreadSafeForSameKey() throws InterruptedException { assertThat(events) .isNotNull() .hasSize(threadCount * eventsPerThread); - } finally { - executor.shutdown(); } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java index b4bc25cad..8dce562f6 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; @@ -110,32 +112,30 @@ void testGetLoggingManager_shouldReturnSameInstance() { } @Test - void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException { + void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException, IOException { // GIVEN int threadCount = 10; ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - AtomicReference sharedInstance = new AtomicReference<>(); - - // WHEN - for (int i = 0; i < threadCount; i++) { - executor.submit(() -> { - try { - LoggingManager instance = LoggingManagerRegistry.getLoggingManager(); - sharedInstance.compareAndSet(null, instance); - assertThat(instance).isSameAs(sharedInstance.get()); - } finally { - latch.countDown(); - } - }); - } - - // THEN - try { + try (Closeable close = executor::shutdown) { + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicReference sharedInstance = new AtomicReference<>(); + + // WHEN + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + LoggingManager instance = LoggingManagerRegistry.getLoggingManager(); + sharedInstance.compareAndSet(null, instance); + assertThat(instance).isSameAs(sharedInstance.get()); + } finally { + latch.countDown(); + } + }); + } + + // THEN latch.await(5, TimeUnit.SECONDS); assertThat(sharedInstance.get()).isNotNull(); - } finally { - executor.shutdown(); } } } From 496a2b1928158c530a8c5cf42b9d55e5f7f282be Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 12:24:54 +0200 Subject: [PATCH 28/45] Try to satisfy pmd executor service closing. --- .../lambda/powertools/logging/internal/KeyBufferTest.java | 4 ++-- .../logging/internal/LoggingManagerRegistryTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index 7351e11c0..b51f341c1 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -234,7 +234,7 @@ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException, IOExcepti int eventsPerThread = 100; KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - try (Closeable close = executor::shutdown) { + try (Closeable ignored = executor::shutdown) { CountDownLatch latch = new CountDownLatch(threadCount); // Each thread works with different key @@ -270,7 +270,7 @@ void shouldBeThreadSafeForSameKey() throws InterruptedException, IOException { int eventsPerThread = 20; KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length); ExecutorService executor = Executors.newFixedThreadPool(threadCount); - try (Closeable close = executor::shutdown) { + try (Closeable ignored = executor::shutdown) { CountDownLatch latch = new CountDownLatch(threadCount); // All threads work with same key diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java index 8dce562f6..0134970f2 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -116,7 +116,7 @@ void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException, IOE // GIVEN int threadCount = 10; ExecutorService executor = Executors.newFixedThreadPool(threadCount); - try (Closeable close = executor::shutdown) { + try (Closeable ignored = executor::shutdown) { CountDownLatch latch = new CountDownLatch(threadCount); AtomicReference sharedInstance = new AtomicReference<>(); From 54acdc201dcbb43ef4a5f85a3bbf068ef0602f8d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:31:34 +0200 Subject: [PATCH 29/45] Add PMD suppressions with reason where appropriate. --- .../powertools/logging/internal/LambdaLoggingAspect.java | 3 ++- .../lambda/powertools/logging/internal/KeyBufferTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 72e55b8a5..591283996 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 @@ -142,6 +142,7 @@ public Object around(ProceedingJoinPoint pjp, isOnRequestStreamHandler); } + @SuppressWarnings("PMD.CloseResource") // Lambda-owned stream, not ours to close OutputStream backupOutputStream = null; if (isOnRequestStreamHandler) { // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: @@ -155,7 +156,7 @@ public Object around(ProceedingJoinPoint pjp, Object lambdaFunctionResponse; try { lambdaFunctionResponse = pjp.proceed(proceedArgs); - } catch (Throwable t) { + } catch (Throwable t) { // NOPMD - AspectJ proceed() throws Throwable handleException(pjp, logging, t); throw t; } finally { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index b51f341c1..86d82e468 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -334,9 +334,9 @@ void shouldUseDefaultWarningLoggerWhenNotProvided() { // Capture System.err output ByteArrayOutputStream errCapture = new ByteArrayOutputStream(); PrintStream originalErr = System.err; - System.setErr(new PrintStream(errCapture)); + try (PrintStream newErr = new PrintStream(errCapture)) { + System.setErr(newErr); - try { KeyBuffer defaultBuffer = new KeyBuffer<>(5, String::length); // Cause overflow From 3a3ccffa76f9b79c086ed95539b6a16c329168f0 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:35:05 +0200 Subject: [PATCH 30/45] Add PMD suppressions with reason where appropriate. --- .../powertools/logging/internal/LoggingManagerRegistry.java | 2 +- .../lambda/powertools/logging/internal/KeyBufferTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java index dfede8f43..c0a77ccb0 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java @@ -28,7 +28,7 @@ public final class LoggingManagerRegistry { // Used with double-checked locking within getLoggingManger() - @SuppressWarnings("java:S3077") + @SuppressWarnings({ "java:S3077", "PMD.AvoidUsingVolatile" }) private static volatile LoggingManager instance; private LoggingManagerRegistry() { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java index 86d82e468..15a54fa5c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java @@ -333,6 +333,7 @@ void shouldUseCustomWarningLogger() { void shouldUseDefaultWarningLoggerWhenNotProvided() { // Capture System.err output ByteArrayOutputStream errCapture = new ByteArrayOutputStream(); + @SuppressWarnings("PMD.CloseResource") // System.err is not ours to close PrintStream originalErr = System.err; try (PrintStream newErr = new PrintStream(errCapture)) { System.setErr(newErr); From 4d6d22e412dc40d433a24d1adaabc7daeeb28396 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:39:45 +0200 Subject: [PATCH 31/45] Restore original example. --- .../sam/pom.xml | 13 +++- .../sam/src/main/java/helloworld/App.java | 71 +++++++++++++------ .../src/main/java/helloworld/AppStream.java | 1 - .../sam/src/main/resources/log4j2.xml | 2 +- .../sam/src/main/resources/logback.xml | 16 ----- 5 files changed, 62 insertions(+), 41 deletions(-) delete mode 100644 examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml diff --git a/examples/powertools-examples-core-utilities/sam/pom.xml b/examples/powertools-examples-core-utilities/sam/pom.xml index 93bf882c4..3df44f441 100644 --- a/examples/powertools-examples-core-utilities/sam/pom.xml +++ b/examples/powertools-examples-core-utilities/sam/pom.xml @@ -22,7 +22,7 @@
    software.amazon.lambda - powertools-logging-logback + powertools-logging-log4j ${project.version} @@ -99,10 +99,19 @@ false + + + - + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.2.0 + + diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index ef2f93f92..2844e50fe 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -14,8 +14,16 @@ package helloworld; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; +import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,53 +35,74 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.PowertoolsLogging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.tracing.CaptureMode; +import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.tracing.TracingUtils; /** * Handler for requests to Lambda function. */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); - public App() { - // Flush immediately because no trace ID is set yet - log.debug("Constructor DEBUG - should not be buffered (no trace ID)"); - log.info("Constructor INFO - should not be buffered (no trace ID)"); - } - - @Logging(logEvent = false) + @Logging(logEvent = true, samplingRate = 0.7) + @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - // Manually set trace ID for testing in SAM local - System.setProperty("com.amazonaws.xray.traceHeader", - "Root=1-63441c4a-abcdef012345678912345678;Parent=0123456789abcdef;Sampled=1"); - Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - log.debug("DEBUG 1"); - MDC.put("test", "willBeLogged"); - log.debug("DEBUG 2"); - log.info("INFO 1"); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension("AnotherService", "CustomService"); + dimensionSet.addDimension("AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - // Manually flush buffer to show buffered debug logs - // PowertoolsLogging.flushBuffer(); - log.error("Some error happened"); + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); + + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", "Test"); + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + log.info("", entry("ip", pageContents)); + TracingUtils.putAnnotation("Test", "New"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + TracingUtils.withSubsegment("loggingResponse", subsegment -> { + String sampled = "log something out"; + log.info(sampled); + log.info(output); + }); + log.info("After output"); return response .withStatusCode(200) .withBody(output); - } catch (RuntimeException e) { + } catch (RuntimeException | IOException e) { return response .withBody("{}") .withStatusCode(500); } } + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + putMetadata("getPageContents", address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } } diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java index 6524b8e7e..d3ebbef5d 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java @@ -56,7 +56,6 @@ public void handleRequest(InputStream input, OutputStream output, Context contex writer.write("{\"body\": \"" + System.currentTimeMillis() + "\"} "); } catch (IOException e) { - log.error("Exception caught in handler", e); log.error("Something has gone wrong: ", e); } } diff --git a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml index f60db0fb5..e140022e4 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml @@ -4,7 +4,7 @@ - + diff --git a/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml deleted file mode 100644 index 68fce98c8..000000000 --- a/examples/powertools-examples-core-utilities/sam/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - DEBUG - 8 - - - - - - - From 01c6bce9c56b9a759676d743dae99ea1eba32d71 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:44:13 +0200 Subject: [PATCH 32/45] Enable log sampling again. --- .../powertools-examples-core-utilities/sam/template.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/powertools-examples-core-utilities/sam/template.yaml b/examples/powertools-examples-core-utilities/sam/template.yaml index a35e8bd32..6b1814dce 100644 --- a/examples/powertools-examples-core-utilities/sam/template.yaml +++ b/examples/powertools-examples-core-utilities/sam/template.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > CoreUtilities @@ -13,9 +13,9 @@ Globals: Environment: Variables: # Powertools for AWS Lambda (Java) env vars: https://docs.powertools.aws.dev/lambda/java/#environment-variables - POWERTOOLS_LOG_LEVEL: DEBUG - # POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: false + POWERTOOLS_LOG_LEVEL: DEBUG # We use log buffering to buffer DEBUG logs (see log4j2.xml) + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true POWERTOOLS_METRICS_NAMESPACE: Coreutilities Resources: From 10c51c749d1fe675bf40804b45f20d8b65cabc17 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:47:09 +0200 Subject: [PATCH 33/45] Restore formatting in pmd ruleset. --- .github/pmd-ruleset.xml | 69 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/.github/pmd-ruleset.xml b/.github/pmd-ruleset.xml index ed64a03cb..1bc5f3020 100644 --- a/.github/pmd-ruleset.xml +++ b/.github/pmd-ruleset.xml @@ -1,14 +1,13 @@ + xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> Rules to check Powertools for Lambda - + + language="java" + since="5.0" + message="replace o.getClass().equals(MyClass.class) with o instanceof MyClass" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> replace o.getClass().equals(MyClass.class) with o instanceof MyClass. Make sure MyClass doesn't have descendants 1 @@ -542,10 +539,10 @@ + language="java" + since="5.0" + message="replace MyClass.class.equals(o.getClass()) with o instanceof MyClass" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> replace MyClass.class.equals(o.getClass()) with o instanceof MyClass. Make sure MyClass doesn't have descendants 3 @@ -562,10 +559,10 @@ + language="java" + message="Don't call super.visit() when using the rulechain" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Calling super.visit breaks the rulechain, by starting a full visitor run from the passed node downwards. Add all needed nodes to the rulechain instead. 1 @@ -589,10 +586,10 @@ + language="java" + message="Always call super.visit() when not using rulechain" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Just returning without calling super stops visiting of nested nodes like inner classes. 3 @@ -612,10 +609,10 @@ + language="java" + message="Reuse InvocationMatcher" + typeResolution="true" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Share the invocation matcher and not create a new one every time 1 @@ -631,10 +628,10 @@ + language="java" + since="7.0.0" + message="Use slf4j: LoggerFactory.getLogger(MyClass.class)" + class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> Use slf4j: LoggerFactory.getLogger(MyClass.class) 1 From 8a87ea546cc1d74871b75252ae7824538c63e333 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 13:56:38 +0200 Subject: [PATCH 34/45] Update Javadoc of BufferingAppenders. --- .../lambda/powertools/logging/log4j/BufferingAppender.java | 2 +- .../powertools/logging/logback/BufferingAppender.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java index ebf2aac4f..fcf4a2040 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4j/BufferingAppender.java @@ -76,7 +76,7 @@ *
  • During Lambda INIT phase (no trace ID): logs are output directly
  • *
  • During Lambda execution (with trace ID): logs are buffered or output based on level
  • *
  • When buffer overflows: oldest logs are discarded and a warning is logged
  • - *
  • On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}
  • + *
  • On Lambda completion: buffer is auto-cleared when used with {@code @Logging} annotation
  • * * * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushBuffer() diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java index e8147ccec..8f323ff46 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/BufferingAppender.java @@ -39,7 +39,7 @@ *
      *
    • Trace-based buffering: Groups logs by AWS X-Ray trace ID
    • *
    • Selective output: Only buffers logs at or below configured verbosity level
    • - *
    • Auto-flush on errors: Automatically outputs buffered logs when ERROR events occur
    • + *
    • Auto-flush on errors: Automatically outputs buffered logs when ERROR/FATAL events occur
    • *
    • Memory management: Prevents memory leaks with configurable buffer size limits
    • *
    • Overflow protection: Warns when logs are discarded due to buffer limits
    • *
    @@ -58,7 +58,7 @@ *
      *
    • bufferAtVerbosity: Log level to buffer (default: DEBUG). Logs at this level and below are buffered
    • *
    • maxBytes: Maximum buffer size in bytes per trace ID (default: 20480)
    • - *
    • flushOnErrorLog: Whether to flush buffer on ERROR logs (default: true)
    • + *
    • flushOnErrorLog: Whether to flush buffer on ERROR/FATAL logs (default: true)
    • *
    * *

    Behavior:

    @@ -66,7 +66,7 @@ *
  • During Lambda INIT phase (no trace ID): logs are output directly
  • *
  • During Lambda execution (with trace ID): logs are buffered or output based on level
  • *
  • When buffer overflows: oldest logs are discarded and a warning is logged
  • - *
  • On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}
  • + *
  • On Lambda completion: buffer is auto-cleared when used with {@code @Logging} annotation
  • * * * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushBuffer() From 88a26a0e5db06fc75a15ea242880e9c67b19c829 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:03:19 +0200 Subject: [PATCH 35/45] Remove unncessary cleanup. --- .../lambda/powertools/logging/LogbackLoggingManagerTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java index a880beace..95e22d0c6 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -97,11 +97,6 @@ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws JoranExcept assertThat(content.split("Test message 2", -1)).hasSize(3); // 2 occurrences = 3 parts } - @AfterEach - void cleanUp() throws JoranException { - resetLogbackConfig("/logback-test.xml"); - } - private void resetLogbackConfig(String configFileName) throws JoranException { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); context.reset(); From 76b1ab5754d74615d55a6881097149bb1a821c91 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:04:35 +0200 Subject: [PATCH 36/45] Remove duplicated infrastructure clearnup in LoggingE2E. --- .../amazon/lambda/powertools/LoggingE2ET.java | 61 ++++++++----------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index 5aee60e9d..f5d2cea84 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -25,7 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; @@ -62,7 +62,7 @@ private void setupInfrastructure(String pathToFunction) { functionName = outputs.get(FUNCTION_NAME_OUTPUT); } - @AfterAll + @AfterEach void tearDown() { if (infrastructure != null) { infrastructure.destroy(); @@ -75,38 +75,29 @@ void tearDown() { void test_logInfoWithAdditionalKeys(String pathToFunction) throws JsonProcessingException { setupInfrastructure(pathToFunction); - try { - // GIVEN - String orderId = UUID.randomUUID().toString(); - String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId + "\"}}"; - - // WHEN - InvocationResult invocationResult1 = invokeFunction(functionName, event); - InvocationResult invocationResult2 = invokeFunction(functionName, event); - - // THEN - String[] functionLogs = invocationResult1.getLogs().getFunctionLogs(INFO); - assertThat(functionLogs).hasSize(1); - - JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); - assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); - assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); - assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); - assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); - - // second call should not be cold start - functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); - assertThat(functionLogs).hasSize(1); - jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); - - } finally { - // Clean up infrastructure after each parameter - if (infrastructure != null) { - infrastructure.destroy(); - infrastructure = null; - } - } + // GIVEN + String orderId = UUID.randomUUID().toString(); + String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId + "\"}}"; + + // WHEN + InvocationResult invocationResult1 = invokeFunction(functionName, event); + InvocationResult invocationResult2 = invokeFunction(functionName, event); + + // THEN + String[] functionLogs = invocationResult1.getLogs().getFunctionLogs(INFO); + assertThat(functionLogs).hasSize(1); + + JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); + assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); + assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); + assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); + assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); + assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); + + // second call should not be cold start + functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); + assertThat(functionLogs).hasSize(1); + jsonNode = objectMapper.readTree(functionLogs[0]); + assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); } } From a44c0ac86a53334970477d26a51a774fe6c8def1 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:06:45 +0200 Subject: [PATCH 37/45] Make javadoc for Keybuffer more concise. --- .../logging/internal/KeyBuffer.java | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java index 03a4cade1..bc3548496 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java @@ -21,35 +21,13 @@ import java.util.function.Function; /** - * Generic buffer data structure for storing events by key with size-based eviction. + * Thread-safe buffer that stores events by key with size-based eviction. * - *

    This buffer maintains separate event queues for each key, with configurable size limits - * to prevent memory exhaustion. When buffers exceed their size limit, older events are - * automatically evicted to make room for newer ones. + *

    Maintains separate queues per key. When buffer size exceeds maxBytes, + * oldest events are evicted FIFO. Events larger than maxBytes are rejected. * - *

    Key Features:

    - *
      - *
    • Per-key buffering: Each key maintains its own independent buffer
    • - *
    • Size-based eviction: Oldest events are removed when buffer size exceeds limit
    • - *
    • Overflow protection: Events larger than buffer size are rejected entirely
    • - *
    • Thread-safe: Supports concurrent access across different keys
    • - *
    • Overflow tracking: Logs warnings when events are evicted or rejected
    • - *
    - * - *

    Eviction Behavior:

    - *
      - *
    • Buffer overflow: When adding an event would exceed maxBytes, oldest events are evicted first
    • - *
    • Large events: Events larger than maxBytes are rejected without evicting existing events
    • - *
    • FIFO eviction: Events are removed in first-in-first-out order during overflow
    • - *
    • Overflow warnings: Automatic logging when events are evicted or rejected
    • - *
    - * - *

    Thread Safety:

    - *

    This class is thread-safe for concurrent operations. Different keys can be accessed - * simultaneously, and operations on the same key are synchronized to prevent data corruption. - * - * @param the type of key used for buffering (e.g., String for trace IDs) - * @param the type of events to buffer (must be compatible with the size calculator) + * @param key type for buffering + * @param event type to buffer */ public class KeyBuffer { From 7b9d8d2ac2c9fe5aa5ae6afae7af7096d1539f0f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:07:48 +0200 Subject: [PATCH 38/45] Add comment for large event rejection. --- .../amazon/lambda/powertools/logging/internal/KeyBuffer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java index bc3548496..3510a544d 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java @@ -51,6 +51,7 @@ public KeyBuffer(int maxBytes, Function sizeCalculator, Runnable ove public void add(K key, T event) { int eventSize = sizeCalculator.apply(event); + // Immediately reject events larger than the whole buffer-size to avoid evicting all elements. if (eventSize > maxBytes) { overflowTriggered.put(key, true); return; From 61231f4e253c6fdd3994aeedc649b393b3dec05d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:11:04 +0200 Subject: [PATCH 39/45] Fix grammer in error message. --- .../powertools/logging/internal/LoggingManagerRegistry.java | 2 +- .../powertools/logging/internal/LoggingManagerRegistryTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java index c0a77ccb0..463981903 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java @@ -88,7 +88,7 @@ static LoggingManager selectLoggingManager(List loggingManagerLi printStream.println("WARN. Found LoggingManager: [" + manager + "]"); } printStream.println( - "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies"); + "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback in your dependencies"); printStream.println("WARN. Using the first LoggingManager found on the classpath: [" + loggingManagerList.get(0) + "]"); } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java index 0134970f2..6bed0d9c1 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java @@ -51,7 +51,7 @@ void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws Unsupporte assertThat(output) .contains("WARN. Multiple LoggingManagers were found on the classpath") .contains( - "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies") + "WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback in your dependencies") .contains("WARN. Using the first LoggingManager found on the classpath: [" + list.get(0) + "]"); } From c6d55d680836769b6755bfe550abac49a1d6988f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 14:15:12 +0200 Subject: [PATCH 40/45] Remove redundant tests in PowertoolsLoggingTest. --- .../powertools/logging/PowertoolsLoggingTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java index d63cae3bb..ea3a2f3f6 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java @@ -15,7 +15,6 @@ package software.amazon.lambda.powertools.logging; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,18 +33,6 @@ void setUp() { testManager.resetBufferState(); } - @Test - void testFlushBuffer_shouldNotThrowException() { - // WHEN/THEN - assertThatCode(PowertoolsLogging::flushBuffer).doesNotThrowAnyException(); - } - - @Test - void testClearBuffer_shouldNotThrowException() { - // WHEN/THEN - assertThatCode(PowertoolsLogging::clearBuffer).doesNotThrowAnyException(); - } - @Test void testFlushBuffer_shouldCallBufferManager() { // WHEN From d439ba49fa336bd7c4ce615dffc6f94acdc63cde Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 17:20:41 +0200 Subject: [PATCH 41/45] Add developer documentation. --- docs/core/logging.md | 378 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 374 insertions(+), 4 deletions(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 2d9e57dda..308a4f73f 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -12,6 +12,7 @@ Logging provides an opinionated logger with output structured as JSON. * Optionally logs Lambda request * Optionally logs Lambda response * Optionally supports log sampling by including a configurable percentage of DEBUG logs in logging output +* Optionally supports buffering lower level logs and flushing them on error or manually * Allows additional keys to be appended to the structured log at any point in time * GraalVM support @@ -311,9 +312,8 @@ We prioritise log level settings in this order: If you set `POWERTOOLS_LOG_LEVEL` lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. -> **NOTE** -> -> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. +!!! note + With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. ## Basic Usage @@ -787,7 +787,377 @@ with `logError` param or via `POWERTOOLS_LOGGER_LOG_ERROR` env var. } ``` -# Advanced +## Advanced + +### Buffering logs + +Log buffering enables you to buffer logs for a specific request or invocation. Enable log buffering by configuring the `BufferingAppender` in your logging configuration. You can buffer logs at the `WARNING`, `INFO` or `DEBUG` level, and flush them automatically on error or manually as needed. + +!!! tip "This is useful when you want to reduce the number of log messages emitted while still having detailed logs when needed, such as when troubleshooting issues." + +=== "log4j2.xml" + + ```xml hl_lines="7-12 16 19" + + + + + + + + + + + + + + + + + + + + ``` + +=== "logback.xml" + + ```xml hl_lines="6-11 13 16" + + + + + + + 20480 + DEBUG + true + + + + + + + + + + ``` + +=== "PaymentFunction.java" + + ```java hl_lines="8 12" + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import software.amazon.lambda.powertools.logging.Logging; + // ... other imports + + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.debug("a debug log"); // this is buffered + LOGGER.info("an info log"); // this is not buffered + + // do stuff + + // Buffer is automatically cleared at the end of the method by @Logging annotation + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + } + ``` + +#### Configuring the buffer + +When configuring log buffering, you have options to fine-tune how logs are captured, stored, and emitted. You can configure the following parameters in the `BufferingAppender` configuration: + +| Parameter | Description | Configuration | +| --------------------- | ----------------------------------------------- | ---------------------------- | +| `maxBytes` | Maximum size of the log buffer in bytes | `int` (default: 20480 bytes) | +| `bufferAtVerbosity` | Minimum log level to buffer | `DEBUG` (default), `INFO`, `WARNING` | +| `flushOnErrorLog` | Automatically flush buffer when `ERROR` or `FATAL` level logs are emitted | `true` (default), `false` | + +!!! warning "Logger Level Configuration" + To use log buffering effectively, you must set your logger levels to the most verbose level (`DEBUG`) so that all logs can be captured by the `BufferingAppender`. The `BufferingAppender` then decides which logs to buffer based on the `bufferAtVerbosity` setting. + + - Set your logger levels to `DEBUG` in your log4j2.xml or logback.xml configuration + - Set `POWERTOOLS_LOG_LEVEL=DEBUG` if using the environment variable (see [Log level](#log-level) section for more details) + + This ensures that all log levels (`DEBUG`, `INFO`, `WARN`) are forwarded to the `BufferingAppender` for it to selectively buffer or emit directly. + +=== "log4j2.xml - Buffer at WARNING level" + + ```xml hl_lines="9 14-15 18" + + + + + + + + + + + + + + + + + + + + + ``` + +=== "PaymentFunction.java - Buffer at WARNING level" + + ```java hl_lines="7-9 13" + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.warn("a warning log"); // this is buffered + LOGGER.info("an info log"); // this is buffered + LOGGER.debug("a debug log"); // this is buffered + + // do stuff + + // Buffer is automatically cleared at the end of the method by @Logging annotation + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + } + ``` + +=== "log4j2.xml - Disable flush on error" + + ```xml hl_lines="9" + + + + + + + + + + + + + + + + + + + + ``` + +=== "PaymentFunction.java - Manual flush required" + + ```java hl_lines="1 16 19-20" + import software.amazon.lambda.powertools.logging.PowertoolsLogging; + + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.debug("a debug log"); // this is buffered + + // do stuff + + try { + throw new RuntimeException("Something went wrong"); + } catch (RuntimeException error) { + LOGGER.error("An error occurred", error); // Logs won't be flushed here + } + + // Manually flush buffered logs + PowertoolsLogging.flushBuffer(); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + } + ``` + +!!! note "Disabling `flushOnErrorLog` will not flush the buffer when logging an error. This is useful when you want to control when the buffer is flushed by calling the flush method manually." + +#### Manual buffer control + +You can manually control the log buffer using the `PowertoolsLogging` utility class, which provides a backend-independent API that works with both Log4j2 and Logback: + +=== "Manual flush" + + ```java hl_lines="1 12-13" + import software.amazon.lambda.powertools.logging.PowertoolsLogging; + + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.debug("Processing payment"); // this is buffered + LOGGER.info("Payment validation complete"); // this is buffered + + // Manually flush all buffered logs + PowertoolsLogging.flushBuffer(); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + } + ``` + +=== "Manual clear" + + ```java hl_lines="1 12-13" + import software.amazon.lambda.powertools.logging.PowertoolsLogging; + + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.debug("Processing payment"); // this is buffered + LOGGER.info("Payment validation complete"); // this is buffered + + // Manually clear buffered logs without outputting them + PowertoolsLogging.clearBuffer(); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + } + ``` + +**Available methods:** + +- `#!java PowertoolsLogging.flushBuffer()` - Outputs all buffered logs and clears the buffer +- `#!java PowertoolsLogging.clearBuffer()` - Discards all buffered logs without outputting them + +#### Flushing on exceptions + +Use the `@Logging` annotation to automatically flush buffered logs when an uncaught exception is raised in your Lambda function. This is enabled by default (`flushBufferOnUncaughtError = true`), but you can explicitly configure it if needed. + +=== "PaymentFunction.java" + + ```java hl_lines="5 11" + public class PaymentFunction implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging(flushBufferOnUncaughtError = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + LOGGER.debug("a debug log"); // this is buffered + + // do stuff + + throw new RuntimeException("Something went wrong"); // Logs will be flushed here + } + } + ``` + +#### Buffering workflows + +##### Manual flush + +

    +```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: logger.debug("First debug log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.info("Info log") + Logger->>CloudWatch: Directly log info message + Lambda->>Logger: logger.debug("Second debug log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: Manual flush call + Logger->>CloudWatch: Emit buffered logs to stdout + Lambda->>Client: Return execution result +``` +Flushing buffer manually +
    + +##### Flushing when logging an error + +
    +```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: logger.debug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.debug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: logger.debug("Third log") + Logger-->>Logger: Buffer third debug log + Lambda->>Lambda: Exception occurs + Lambda->>Logger: logger.error("Error details") + Logger->>CloudWatch: Emit error log + Logger->>CloudWatch: Emit buffered debug logs + Lambda->>Client: Raise exception +``` +Flushing buffer when an error happens +
    + +##### Flushing on exception + +This works when using the `@Logging` annotation which automatically clears the buffer at the end of method execution. + +
    +```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Using @Logging annotation + Logger-->>Lambda: Logger context injected + Lambda->>Logger: logger.debug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.debug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Lambda: Uncaught Exception + Lambda->>CloudWatch: Automatically emit buffered debug logs + Lambda->>Client: Raise uncaught exception +``` +Flushing buffer when an uncaught exception happens +
    + +#### Buffering FAQs + +1. **Does the buffer persist across Lambda invocations?** No, each Lambda invocation has its own buffer. The buffer is initialized when the Lambda function is invoked and is cleared after the function execution completes or when flushed manually. +2. **Are my logs buffered during cold starts (INIT phase)?** No, we never buffer logs during cold starts. This is because we want to ensure that logs emitted during this phase are always available for debugging and monitoring purposes. The buffer is only used during the execution of the Lambda function. +3. **How can I prevent log buffering from consuming excessive memory?** You can limit the size of the buffer by setting the `maxBytes` option in the `BufferingAppender` configuration. This will ensure that the buffer does not grow indefinitely and consume excessive memory. +4. **What happens if the log buffer reaches its maximum size?** Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. +5. **How is the log size of a log line calculated?** The log size is calculated based on the size of the log line in bytes. This includes the size of the log message, any exception (if present), the log line location, additional keys, and the timestamp. +6. **What timestamp is used when I flush the logs?** The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. +7. **What happens if I try to add a log line that is bigger than max buffer size?** The log will be emitted directly to standard output and not buffered. When this happens, we emit a warning to indicate that the log line was too big to be buffered. +8. **What happens if Lambda times out without flushing the buffer?** Logs that are still in the buffer will be lost. +9. **How does the `BufferingAppender` work with different appenders?** The `BufferingAppender` is designed to wrap arbitrary appenders, providing maximum flexibility. You can wrap console appenders, file appenders, or any custom appenders with buffering functionality. ## Sampling debug logs From 2d20f75e1e8fdba4c50ecc49ce1b0f33024d0a5c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 2 Sep 2025 17:22:28 +0200 Subject: [PATCH 42/45] Fix sonar finding. --- .../lambda/powertools/logging/LogbackLoggingManagerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java index 95e22d0c6..60f158739 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -27,7 +27,6 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; From c147b01197703e51c80841075eab954db56ccb37 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 3 Sep 2025 10:52:52 +0200 Subject: [PATCH 43/45] Clarify log buffering log level configuration with upper and lower bound example. --- docs/core/logging.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 308a4f73f..e974094bc 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -881,12 +881,12 @@ When configuring log buffering, you have options to fine-tune how logs are captu | `flushOnErrorLog` | Automatically flush buffer when `ERROR` or `FATAL` level logs are emitted | `true` (default), `false` | !!! warning "Logger Level Configuration" - To use log buffering effectively, you must set your logger levels to the most verbose level (`DEBUG`) so that all logs can be captured by the `BufferingAppender`. The `BufferingAppender` then decides which logs to buffer based on the `bufferAtVerbosity` setting. - + To use log buffering effectively, you must set your logger levels to the same level as `bufferAtVerbosity` or more verbose for the logging framework to capture and forward logs to the `BufferingAppender`. For example, if you want to buffer `DEBUG` level logs and emit `INFO`+ level logs directly, you must: + - Set your logger levels to `DEBUG` in your log4j2.xml or logback.xml configuration - Set `POWERTOOLS_LOG_LEVEL=DEBUG` if using the environment variable (see [Log level](#log-level) section for more details) - - This ensures that all log levels (`DEBUG`, `INFO`, `WARN`) are forwarded to the `BufferingAppender` for it to selectively buffer or emit directly. + + If you want to sample `INFO` and `WARNING` logs but not `DEBUG` logs, set your log level to `INFO` and `bufferAtVerbosity` to `WARNING`. This allows you to define the lower and upper bounds for buffering. All logs with a more severe level than `bufferAtVerbosity` will be emitted directly. === "log4j2.xml - Buffer at WARNING level" From 1640f6f6830271c05e0b1171dfc26173cbb9cca2 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 3 Sep 2025 15:49:26 +0200 Subject: [PATCH 44/45] Update docs/core/logging.md Co-authored-by: Stefano Vozza --- docs/core/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index e974094bc..0274dde8b 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -1151,7 +1151,7 @@ sequenceDiagram 1. **Does the buffer persist across Lambda invocations?** No, each Lambda invocation has its own buffer. The buffer is initialized when the Lambda function is invoked and is cleared after the function execution completes or when flushed manually. 2. **Are my logs buffered during cold starts (INIT phase)?** No, we never buffer logs during cold starts. This is because we want to ensure that logs emitted during this phase are always available for debugging and monitoring purposes. The buffer is only used during the execution of the Lambda function. -3. **How can I prevent log buffering from consuming excessive memory?** You can limit the size of the buffer by setting the `maxBytes` option in the `BufferingAppender` configuration. This will ensure that the buffer does not grow indefinitely and consume excessive memory. +3. **How can I prevent log buffering from consuming excessive memory?** You can limit the size of the buffer by setting the `maxBytes` option in the `BufferingAppender` configuration. This will ensure that the buffer does not grow indefinitely. 4. **What happens if the log buffer reaches its maximum size?** Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. 5. **How is the log size of a log line calculated?** The log size is calculated based on the size of the log line in bytes. This includes the size of the log message, any exception (if present), the log line location, additional keys, and the timestamp. 6. **What timestamp is used when I flush the logs?** The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. From e60edcbec5ac60221db5b5c9dcb7588d757e36cc Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 3 Sep 2025 15:49:54 +0200 Subject: [PATCH 45/45] Update docs/core/logging.md Co-authored-by: Stefano Vozza --- docs/core/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 0274dde8b..be3bd7e5c 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -1154,7 +1154,7 @@ sequenceDiagram 3. **How can I prevent log buffering from consuming excessive memory?** You can limit the size of the buffer by setting the `maxBytes` option in the `BufferingAppender` configuration. This will ensure that the buffer does not grow indefinitely. 4. **What happens if the log buffer reaches its maximum size?** Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. 5. **How is the log size of a log line calculated?** The log size is calculated based on the size of the log line in bytes. This includes the size of the log message, any exception (if present), the log line location, additional keys, and the timestamp. -6. **What timestamp is used when I flush the logs?** The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. +6. **What timestamp is used when I flush the logs?** The timestamp is the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. 7. **What happens if I try to add a log line that is bigger than max buffer size?** The log will be emitted directly to standard output and not buffered. When this happens, we emit a warning to indicate that the log line was too big to be buffered. 8. **What happens if Lambda times out without flushing the buffer?** Logs that are still in the buffer will be lost. 9. **How does the `BufferingAppender` work with different appenders?** The `BufferingAppender` is designed to wrap arbitrary appenders, providing maximum flexibility. You can wrap console appenders, file appenders, or any custom appenders with buffering functionality.