-
Notifications
You must be signed in to change notification settings - Fork 96
Closed
Labels
Description
Use case
Log buffering enables you to buffer logs for a specific request or invocation, and flush them automatically on error or manually as needed. 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.
This feature is already available in other Powertools language implementations:
PLEASE READ:
- Log buffering documentation of the other languages
- Final specification from original RFC: RFC: Buffer low-level logs and flush on high-level log powertools-lambda-typescript#3410 (reply in thread)
The remainder of this issue outlines a high-level overview of how this experience can look like in Java.
Solution/User Experience
Complete Example
public class MyHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponse> {
private static final Logger logger = LoggerFactory.getLogger(MyHandler.class);
public MyHandler() {
// Programmatic configuration using Builder pattern
PowertoolsLogging.setBufferConfig(BufferConfig.builder()
.maxBytes(20480) // Default is 20KB (20480 bytes)
.bufferAtVerbosity(Level.DEBUG) // Buffers DEBUG and TRACE
.flushOnErrorLog(true) // default true, auto-flushes buffer when error logs are emitted
.compress(false) // default false
.build());
// INIT logs will never be buffered due to no trace ID being present
logger.debug("This is a debug message"); // This is NOT buffered
}
@Logging
public APIGatewayProxyResponse handleRequest(APIGatewayProxyRequestEvent event, Context context) {
// Assuming current log level is INFO
logger.debug("This is a debug message"); // This is buffered
logger.info("This is an info message");
// your business logic here
logger.error("This is an error message"); // This also flushes the buffer
return new APIGatewayProxyResponse();
}
}
Annotation-based Configuration
@Logging(bufferConfig = @BufferConfig(maxBytes = 20480, flushOnErrorLog = true))
public String handleRequest(APIGatewayProxyRequestEvent event, Context context) {
Logger logger = LoggerFactory.getLogger(MyHandler.class);
// Assuming current log level is INFO
logger.debug("a debug log"); // this is buffered
logger.info("an info log"); // this is not buffered
// do stuff
return "Success";
}
Manual Buffer Control
@Logging(bufferConfig = @BufferConfig(flushOnErrorLog = false))
public String handleRequest(APIGatewayProxyRequestEvent event, Context context) {
Logger logger = LoggerFactory.getLogger(MyHandler.class);
// Assuming current log level is INFO
logger.debug("a debug log"); // this is buffered
try {
// Some operation that might fail
} catch (Exception error) {
logger.error("An error occurred", error); // Logs won't be flushed here
}
// Need to flush logs manually due to flushOnErrorLog = false
PowertoolsLogging.flushBuffer();
return "Success";
}
Flush Buffer on Unhandled Exceptions
@Logging(bufferConfig = @BufferConfig(maxBytes = 20480), flushBufferOnUncaughtError = true)
public String handleRequest(APIGatewayProxyRequestEvent event, Context context) {
Logger logger = LoggerFactory.getLogger(MyHandler.class);
// Assuming current log level is INFO
logger.debug("a debug log"); // this is buffered
logger.info("processing request"); // this is not buffered
// This unhandled exception will trigger buffer flush before re-throwing
// Only compatible with @Logging annotation
throw new RuntimeException("Something went wrong");
}
Configuration Options
Annotation Configuration
@BufferConfig(
maxBytes = 20480, // Maximum size of log buffer in bytes (default: 20480)
bufferAtVerbosity = Level.DEBUG, // Minimum log level to buffer (default: DEBUG, includes TRACE)
flushOnErrorLog = true, // Automatically flush on error (default: true)
compress = false // Enable compression (default: false)
)
Builder Pattern Configuration
BufferConfig config = BufferConfig.builder()
.maxBytes(20480) // Maximum size of log buffer in bytes (default: 20480)
.bufferAtVerbosity(Level.DEBUG) // Minimum log level to buffer (default: DEBUG, includes TRACE)
.flushOnErrorLog(true) // Automatically flush on error (default: true)
.compress(false) // Enable compression (default: false)
.build();
PowertoolsLogging.setBufferConfig(config);
Implementation Details
When to Buffer
- Use
_X_AMZN_TRACE_ID
as key to group logs belonging to an invocation and isolate buffers per Lambda execution - INIT phase logs are NEVER buffered (no trace ID present yet)
- Only buffer during Lambda invocation when trace ID is available
What to Buffer
- Default: Buffer only
DEBUG
andTRACE
levels - Configurable: Allow buffering up to specified level (e.g.,
bufferAtVerbosity=WARNING
buffers WARNING, INFO, DEBUG, TRACE) - Higher levels (ERROR, FATAL) are logged immediately unless configured otherwise
Buffer Flush Triggers
- Automatic flush on error logs (
logger.error()
,logger.exception()
,logger.critical()
) whenflushOnErrorLog=true
(default behavior when buffering is enabled) - Automatic flush on unhandled exceptions when using
@Logging
annotation withflushBufferOnUncaughtError=true
(default: false, must be explicitly enabled) - Manual flush via
PowertoolsLogging.flushBuffer()
Buffer Management
- Multi-key buffer cache using trace ID as key to isolate logs per Lambda execution
- Per-execution buffering prevents logs from different warm executions mixing
- Size-based eviction when
maxBytes
limit reached for each trace ID - Automatic cleanup when new trace ID detected (new Lambda invocation)
- Optional compression using Java GZIP (standard library) to optimize for space at the cost of CPU overhead (disabled by default)
- Configuration validation in
BufferConfig
class throws clear exceptions when invalid buffer settings are applied - Warning log emitted when buffer overflow causes log eviction:
{ "message": "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", "level": "WARN", ... }
Integration with Existing Architecture
Logback Integration
- Extend existing
LambdaJsonEncoder
to support buffering - Create
BufferingAppender
that works with current encoder structure - Integrate with existing MDC and structured argument handling
Log4j2 Integration
- Extend existing
PowertoolsResolver
to support buffering - Create custom appender that integrates with current JSON template layout
- Maintain compatibility with existing resolver architecture
AspectJ Integration
- Extend
LambdaLoggingAspect
to handle buffer configuration and lifecycle - Add buffer flush logic to exception handling in aspect
- Integrate buffer cleanup with existing state management
Technical Design
Core Classes Structure
// Buffer cache for a single key
public class KeyBufferCache {
private final Deque<LogEvent> cache = new ArrayDeque<>();
private int currentSize = 0;
private boolean hasEvicted = false;
public void add(LogEvent event) { }
public LogEvent removeOldest() { }
public List<LogEvent> get() { }
public void clear() { }
}
// Multi-key buffer management
public class LoggerBufferCache {
private final int maxSizeBytes;
private final Map<String, KeyBufferCache> cache = new ConcurrentHashMap<>();
public void add(String key, LogEvent event) { }
public List<LogEvent> get(String key) { }
public void clear(String key) { }
public boolean hasItemsEvicted(String key) { }
}
// Buffer configuration
public class BufferConfig {
private int maxBytes = 20480;
private Level bufferAtVerbosity = Level.DEBUG;
private boolean flushOnErrorLog = true;
private boolean compress = false;
}
// Integration point
public class PowertoolsLogging {
private static LoggerBufferCache bufferCache;
public static void setBufferConfig(BufferConfig config) { }
public static void flushBuffer() { }
public static void clearBuffer() { }
}
Annotation Updates
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Logging {
// ... existing fields ...
BufferConfig bufferConfig() default @BufferConfig;
boolean flushBufferOnUncaughtError() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface BufferConfig {
int maxBytes() default 20480;
Level bufferAtVerbosity() default Level.DEBUG;
boolean flushOnErrorLog() default true;
boolean compress() default false;
}
Alternative solutions
Standard Java logging frameworks (SLF4J with Logback/Log4j2) do not provide built-in conditional log buffering:
- Logback AsyncAppender: Provides asynchronous logging but no conditional flushing based on exceptions
- Log4j2 AsyncLogger: Similar limitations to Logback
- Custom Solutions: We want to integrate with standard Java logging frameworks (SLF4J with Logback/Log4j2) and will not consider a custom logger implementation.
Acknowledgment
- This feature request meets Powertools for AWS Lambda (Java) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, TypeScript, and .NET
Future readers
Please react with 👍 and your use case to help us understand customer demand.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Working on it