","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":[]
}
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..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
@@ -15,15 +15,27 @@
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 org.junit.jupiter.api.Order;
+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.BeforeEach;
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 {
@@ -31,8 +43,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);
@@ -43,7 +65,6 @@ void getLogLevel_shouldReturnConfiguredLogLevel() {
}
@Test
- @Order(2)
void resetLogLevel() {
LogbackLoggingManager manager = new LogbackLoggingManager();
manager.setLogLevel(ERROR);
@@ -51,4 +72,35 @@ void resetLogLevel() {
Level logLevel = manager.getLogLevel(LOG);
assertThat(logLevel).isEqualTo(ERROR);
}
+
+ @Test
+ void shouldDetectMultipleBufferingAppendersRegardlessOfName() throws JoranException {
+ // Given - configuration with multiple BufferingAppenders with different names
+ resetLogbackConfig("/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
+ }
+
+ 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));
+ }
}
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..62d2e3056
--- /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/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java
index 9e5e735d1..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;
@@ -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/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/KeyBuffer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java
new file mode 100644
index 000000000..3510a544d
--- /dev/null
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/KeyBuffer.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+/**
+ * Thread-safe buffer that stores events by key with size-based eviction.
+ *
+ * Maintains separate queues per key. When buffer size exceeds maxBytes,
+ * oldest events are evicted FIFO. Events larger than maxBytes are rejected.
+ *
+ * @param key type for buffering
+ * @param event type to buffer
+ */
+public class KeyBuffer {
+
+ 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) {
+ 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;
+ }
+
+ 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))) {
+ overflowWarningLogger.run();
+ }
+ }
+
+ private int getBufferSize(Deque buffer) {
+ return buffer.stream().mapToInt(sizeCalculator::apply).sum();
+ }
+}
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..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
@@ -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,10 @@
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.Locale;
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 +56,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 +74,7 @@ public final class LambdaLoggingAspect {
private static final LoggingManager LOGGING_MANAGER;
static {
- LOGGING_MANAGER = getLoggingManagerFromServiceLoader();
+ LOGGING_MANAGER = LoggingManagerRegistry.getLoggingManager();
setLogLevel();
@@ -90,7 +87,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);
}
}
@@ -102,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)) {
@@ -113,60 +111,14 @@ 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) {
+ // Pointcut method - body intentionally empty
}
/**
@@ -174,69 +126,51 @@ 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);
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()) {
- 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:
- // 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
+ @SuppressWarnings("PMD.CloseResource") // Lambda-owned stream, not ours to close
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.logError() || POWERTOOLS_LOG_ERROR) {
- // logging the exception with additional context
- logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t);
- }
+ } catch (Throwable t) { // NOPMD - AspectJ proceed() throws Throwable
+ handleException(pjp, logging, t);
throw t;
} finally {
- if (logging.clearState()) {
- MDC.clear();
- }
- 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;
}
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 +194,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 +280,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,11 +311,10 @@ 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))) {
@@ -392,6 +325,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/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..463981903
--- /dev/null
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistry.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+/**
+ * Thread-safe singleton registry for LoggingManager instances.
+ * Handles lazy loading and caching of the LoggingManager implementation.
+ */
+public final class LoggingManagerRegistry {
+
+ // Used with double-checked locking within getLoggingManger()
+ @SuppressWarnings({ "java:S3077", "PMD.AvoidUsingVolatile" })
+ private static volatile LoggingManager instance;
+
+ 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;
+ if (manager == null) {
+ synchronized (LoggingManagerRegistry.class) {
+ manager = instance;
+ if (manager == null) {
+ manager = loadLoggingManager();
+ instance = 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 in 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/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":[]
}
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..ea3a2f3f6
--- /dev/null
+++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/PowertoolsLoggingTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 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_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/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/KeyBufferTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java
new file mode 100644
index 000000000..15a54fa5c
--- /dev/null
+++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/KeyBufferTest.java
@@ -0,0 +1,356 @@
+/*
+ * 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.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+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() {
+ 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
+ testBuffer.add("key1", "extra"); // causes overflow
+
+ // Trigger warning by removing
+ testBuffer.removeAll("key1");
+
+ assertThat(warningCapture.toString())
+ .contains("Some logs are not displayed because they were evicted from the buffer");
+ }
+
+ @Test
+ void shouldLogWarningOnLargeEventRejection() {
+ 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
+
+ // Trigger warning by removing
+ testBuffer.removeAll("key1");
+
+ assertThat(warningCapture.toString())
+ .contains("Some logs are not displayed because they were evicted from the buffer");
+ }
+
+ @Test
+ void shouldNotLogWarningWhenNoOverflow() {
+ 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");
+
+ assertThat(warningCapture.toString())
+ .doesNotContain("Some logs are not displayed because they were evicted from the buffer");
+ }
+
+ @Test
+ void shouldBeThreadSafeForDifferentKeys() throws InterruptedException, IOException {
+ int threadCount = 10;
+ int eventsPerThread = 100;
+ KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length);
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ try (Closeable ignored = executor::shutdown) {
+ 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();
+ }
+ });
+ }
+
+ 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 = largeBuffer.removeAll(key);
+ assertThat(events)
+ .isNotNull()
+ .hasSize(eventsPerThread);
+ }
+ }
+ }
+
+ @Test
+ void shouldBeThreadSafeForSameKey() throws InterruptedException, IOException {
+ int threadCount = 5;
+ int eventsPerThread = 20;
+ KeyBuffer largeBuffer = new KeyBuffer<>(10000, String::length);
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ try (Closeable ignored = executor::shutdown) {
+ 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();
+ }
+ });
+ }
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+
+ Deque events = largeBuffer.removeAll("sharedKey");
+ assertThat(events)
+ .isNotNull()
+ .hasSize(threadCount * eventsPerThread);
+ }
+ }
+
+ @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");
+ }
+
+ @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();
+ @SuppressWarnings("PMD.CloseResource") // System.err is not ours to close
+ PrintStream originalErr = System.err;
+ try (PrintStream newErr = new PrintStream(errCapture)) {
+ System.setErr(newErr);
+
+ 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);
+ }
+ }
+}
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..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
@@ -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;
@@ -81,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;
@@ -111,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) {
@@ -721,47 +726,129 @@ void shouldLogCorrelationIdOnAppSyncEvent() throws IOException {
}
@Test
- void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws UnsupportedEncodingException {
- // GIVEN
- List list = new ArrayList<>();
- list.add(new TestLoggingManager());
- list.add(new DefautlLoggingManager());
+ void shouldClearBufferAfterSuccessfulHandlerExecution() {
+ // WHEN
+ requestHandler.handleRequest(new Object(), context);
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintStream stream = new PrintStream(outputStream);
+ // THEN
+ LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager();
+ if (loggingManager instanceof TestLoggingManager) {
+ assertThat(((TestLoggingManager) loggingManager).isBufferCleared()).isTrue();
+ }
+ }
+ @Test
+ void shouldClearBufferAfterSuccessfulStreamHandlerExecution() throws IOException {
// WHEN
- LambdaLoggingAspect.getLoggingManager(list, stream);
+ requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(),
+ context);
// 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) + "]");
+ 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 testNoLoggingManagers_shouldWarnAndCreateDefault() throws UnsupportedEncodingException {
+ void shouldFlushBufferOnUncaughtErrorWhenEnabled() {
// GIVEN
- List list = new ArrayList<>();
+ requestHandler = new PowertoolsLogError();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintStream stream = new PrintStream(outputStream);
+ // 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
- LoggingManager loggingManager = LambdaLoggingAspect.getLoggingManager(list, stream);
+ try {
+ handler.handleRequest("input", context);
+ } catch (Exception e) {
+ // ignore
+ }
// 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");
+ LoggingManager loggingManager = LoggingManagerRegistry.getLoggingManager();
+ if (loggingManager instanceof TestLoggingManager) {
+ assertThat(((TestLoggingManager) loggingManager).isBufferFlushed()).isFalse();
+ }
+ }
- assertThat(loggingManager).isExactlyInstanceOf(DefautlLoggingManager.class);
+ @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)
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..6bed0d9c1
--- /dev/null
+++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LoggingManagerRegistryTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.Closeable;
+import java.io.IOException;
+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 in 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)
+ .isInstanceOf(BufferManager.class);
+ }
+
+ @Test
+ void testGetLoggingManager_shouldReturnSameInstance() {
+ // WHEN
+ LoggingManager first = LoggingManagerRegistry.getLoggingManager();
+ LoggingManager second = LoggingManagerRegistry.getLoggingManager();
+
+ // THEN
+ assertThat(first)
+ .isSameAs(second)
+ .isNotNull()
+ .isInstanceOf(BufferManager.class);
+ }
+
+ @Test
+ void testGetLoggingManager_shouldBeThreadSafe() throws InterruptedException, IOException {
+ // GIVEN
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ try (Closeable ignored = 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();
+ }
+ }
+}
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;
+ }
}