1515package software .amazon .lambda .powertools .logging .log4j ;
1616
1717import java .io .Serializable ;
18- import java .util .ArrayDeque ;
1918import java .util .Deque ;
20- import java .util .Map ;
21- import java .util .concurrent .ConcurrentHashMap ;
2219
2320import org .apache .logging .log4j .Level ;
2421import org .apache .logging .log4j .core .Appender ;
3532import org .apache .logging .log4j .core .config .plugins .PluginElement ;
3633import org .apache .logging .log4j .core .config .plugins .PluginFactory ;
3734import org .apache .logging .log4j .core .impl .Log4jLogEvent ;
38- import org .apache .logging .log4j .message .SimpleMessage ;
3935
4036import software .amazon .lambda .powertools .common .internal .LambdaHandlerProcessor ;
37+ import software .amazon .lambda .powertools .logging .internal .BufferManager ;
38+ import software .amazon .lambda .powertools .logging .internal .KeyBuffer ;
39+
40+ import static software .amazon .lambda .powertools .logging .log4j .BufferingAppenderConstants .NAME ;
4141
4242/**
43- * A minimalistic Log4j2 appender that buffers log events based on trace ID
44- * and flushes them when error logs are encountered or manually triggered.
43+ * A Log4j2 appender that buffers log events by AWS X-Ray trace ID for optimized Lambda logging.
44+ *
45+ * <p>This appender is designed specifically for AWS Lambda functions to reduce log ingestion
46+ * by buffering lower-level logs and only outputting them when errors occur, preserving
47+ * full context for troubleshooting while minimizing routine log volume.
48+ *
49+ * <h3>Key Features:</h3>
50+ * <ul>
51+ * <li><strong>Trace-based buffering:</strong> Groups logs by AWS X-Ray trace ID</li>
52+ * <li><strong>Selective output:</strong> Only buffers logs at or below configured verbosity level</li>
53+ * <li><strong>Auto-flush on errors:</strong> Automatically outputs buffered logs when ERROR/FATAL events occur</li>
54+ * <li><strong>Memory management:</strong> Prevents memory leaks with configurable buffer size limits</li>
55+ * <li><strong>Overflow protection:</strong> Warns when logs are discarded due to buffer limits</li>
56+ * </ul>
57+ *
58+ * <h3>Configuration Example:</h3>
59+ * <pre>{@code
60+ * <BufferingAppender name="BufferedAppender"
61+ * bufferAtVerbosity="INFO"
62+ * maxBytes="20480"
63+ * flushOnErrorLog="true">
64+ * <AppenderRef ref="ConsoleAppender"/>
65+ * </BufferingAppender>
66+ * }</pre>
67+ *
68+ * <h3>Configuration Parameters:</h3>
69+ * <ul>
70+ * <li><strong>bufferAtVerbosity:</strong> Log level to buffer (default: DEBUG). Logs at this level and below are buffered</li>
71+ * <li><strong>maxBytes:</strong> Maximum buffer size in bytes per trace ID (default: 20480)</li>
72+ * <li><strong>flushOnErrorLog:</strong> Whether to flush buffer on ERROR/FATAL logs (default: true)</li>
73+ * </ul>
74+ *
75+ * <h3>Behavior:</h3>
76+ * <ul>
77+ * <li>During Lambda INIT phase (no trace ID): logs are output directly</li>
78+ * <li>During Lambda execution (with trace ID): logs are buffered or output based on level</li>
79+ * <li>When buffer overflows: oldest logs are discarded and a warning is logged</li>
80+ * <li>On Lambda completion: remaining buffered logs can be flushed via {@link software.amazon.lambda.powertools.logging.PowertoolsLogging}</li>
81+ * </ul>
82+ *
83+ * @see software.amazon.lambda.powertools.logging.PowertoolsLogging#flushLogBuffer()
4584 */
46- @ Plugin (name = "BufferingAppender" , category = Core .CATEGORY_NAME , elementType = Appender .ELEMENT_TYPE )
47- public class BufferingAppender extends AbstractAppender {
85+ @ Plugin (name = NAME , category = Core .CATEGORY_NAME , elementType = Appender .ELEMENT_TYPE )
86+ public class BufferingAppender extends AbstractAppender implements BufferManager {
4887
4988 private final AppenderRef [] appenderRefs ;
5089 private final Configuration configuration ;
5190 private final Level bufferAtVerbosity ;
52- private final int maxBytes ;
5391 private final boolean flushOnErrorLog ;
54- private final Map <String , Deque <LogEvent >> bufferCache = new ConcurrentHashMap <>();
55- private final ThreadLocal <Boolean > bufferOverflowTriggered = new ThreadLocal <>();
92+ private final KeyBuffer <String , LogEvent > buffer ;
5693
5794 protected BufferingAppender (String name , Filter filter , Layout <? extends Serializable > layout ,
5895 AppenderRef [] appenderRefs , Configuration configuration , Level bufferAtVerbosity , int maxBytes ,
@@ -61,18 +98,18 @@ protected BufferingAppender(String name, Filter filter, Layout<? extends Seriali
6198 this .appenderRefs = appenderRefs ;
6299 this .configuration = configuration ;
63100 this .bufferAtVerbosity = bufferAtVerbosity ;
64- this .maxBytes = maxBytes ;
65101 this .flushOnErrorLog = flushOnErrorLog ;
102+ this .buffer = new KeyBuffer <>(maxBytes , event -> event .getMessage ().getFormattedMessage ().length ());
66103 }
67104
68105 @ Override
69106 public void append (LogEvent event ) {
70107 if (appenderRefs == null || appenderRefs .length == 0 ) {
71108 return ;
72109 }
110+
73111 LambdaHandlerProcessor .getXrayTraceId ().ifPresentOrElse (
74112 traceId -> {
75- // Check if we should buffer this log level
76113 if (shouldBuffer (event .getLevel ())) {
77114 bufferEvent (traceId , event );
78115 } else {
@@ -102,62 +139,22 @@ private boolean shouldBuffer(Level level) {
102139 }
103140
104141 private void bufferEvent (String traceId , LogEvent event ) {
105- // Create immutable copy to prevent mutation
106142 LogEvent immutableEvent = Log4jLogEvent .createMemento (event );
107-
108- // Check if single event is larger than buffer - discard if so
109- int eventSize = immutableEvent .getMessage ().getFormattedMessage ().length ();
110- if (eventSize > maxBytes ) {
111- if (Boolean .TRUE != bufferOverflowTriggered .get ()) {
112- bufferOverflowTriggered .set (true );
113- }
114- return ;
115- }
116-
117- bufferCache .computeIfAbsent (traceId , k -> new ArrayDeque <>()).add (immutableEvent );
118-
119- // Simple size check - remove oldest if over limit
120- Deque <LogEvent > buffer = bufferCache .get (traceId );
121- while (getBufferSize (buffer ) > maxBytes && !buffer .isEmpty ()) {
122- if (Boolean .TRUE != bufferOverflowTriggered .get ()) {
123- bufferOverflowTriggered .set (true );
124- }
125- buffer .removeFirst ();
126- }
127- }
128-
129- private int getBufferSize (Deque <LogEvent > buffer ) {
130- return buffer .stream ()
131- .mapToInt (event -> event .getMessage ().getFormattedMessage ().length ())
132- .sum ();
143+ buffer .add (traceId , immutableEvent );
133144 }
134145
135146 public void clearBuffer () {
136- LambdaHandlerProcessor .getXrayTraceId ().ifPresent (bufferCache :: remove );
147+ LambdaHandlerProcessor .getXrayTraceId ().ifPresent (buffer :: clear );
137148 }
138149
139150 public void flushBuffer () {
140151 LambdaHandlerProcessor .getXrayTraceId ().ifPresent (this ::flushBuffer );
141152 }
142153
143154 private void flushBuffer (String traceId ) {
144- // Emit buffer overflow warning if it occurred
145- if (Boolean .TRUE == bufferOverflowTriggered .get ()) {
146- // Create LogEvent directly since Log4j status logger may not reach target appenders
147- LogEvent warningEvent = Log4jLogEvent .newBuilder ()
148- .setLoggerName ("BufferingAppender" )
149- .setLevel (Level .WARN )
150- .setMessage (new SimpleMessage (
151- "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer." ))
152- .setTimeMillis (System .currentTimeMillis ())
153- .build ();
154- callAppenders (warningEvent );
155- bufferOverflowTriggered .remove ();
156- }
157-
158- Deque <LogEvent > buffer = bufferCache .remove (traceId );
159- if (buffer != null ) {
160- buffer .forEach (this ::callAppenders );
155+ Deque <LogEvent > events = buffer .removeAll (traceId );
156+ if (events != null ) {
157+ events .forEach (this ::callAppenders );
161158 }
162159 }
163160
0 commit comments