1414
1515package software .amazon .lambda .powertools .logging ;
1616
17+ import static software .amazon .lambda .powertools .common .internal .LambdaHandlerProcessor .coldStartDone ;
18+ import static software .amazon .lambda .powertools .common .internal .LambdaHandlerProcessor .getXrayTraceId ;
19+ import static software .amazon .lambda .powertools .common .internal .LambdaHandlerProcessor .isColdStart ;
20+ import static software .amazon .lambda .powertools .common .internal .LambdaHandlerProcessor .serviceName ;
21+ import static software .amazon .lambda .powertools .logging .internal .LoggingConstants .LAMBDA_LOG_LEVEL ;
22+ import static software .amazon .lambda .powertools .logging .internal .LoggingConstants .POWERTOOLS_LOG_LEVEL ;
23+ import static software .amazon .lambda .powertools .logging .internal .LoggingConstants .POWERTOOLS_SAMPLING_RATE ;
24+ import static software .amazon .lambda .powertools .logging .internal .PowertoolsLoggedFields .FUNCTION_COLD_START ;
25+ import static software .amazon .lambda .powertools .logging .internal .PowertoolsLoggedFields .FUNCTION_TRACE_ID ;
26+ import static software .amazon .lambda .powertools .logging .internal .PowertoolsLoggedFields .SAMPLING_RATE ;
27+ import static software .amazon .lambda .powertools .logging .internal .PowertoolsLoggedFields .SERVICE ;
28+
29+ import java .util .Arrays ;
30+ import java .util .Locale ;
31+ import java .util .Random ;
32+
33+ import org .slf4j .Logger ;
34+ import org .slf4j .LoggerFactory ;
35+ import org .slf4j .MDC ;
36+ import org .slf4j .event .Level ;
37+
38+ import com .amazonaws .services .lambda .runtime .Context ;
39+ import com .fasterxml .jackson .databind .JsonNode ;
40+
41+ import io .burt .jmespath .Expression ;
1742import software .amazon .lambda .powertools .logging .internal .BufferManager ;
1843import software .amazon .lambda .powertools .logging .internal .LoggingManager ;
1944import software .amazon .lambda .powertools .logging .internal .LoggingManagerRegistry ;
45+ import software .amazon .lambda .powertools .logging .internal .PowertoolsLoggedFields ;
46+ import software .amazon .lambda .powertools .utilities .JsonConfig ;
2047
2148/**
22- * PowertoolsLogging provides a backend-independent API for log buffering operations.
23- * This class abstracts away the underlying logging framework (Log4j2, Logback) and
24- * provides a unified interface for buffer management.
49+ * PowertoolsLogging provides a logging backend-agnostic API for managing Powertools logging functionality.
50+ * This class abstracts away the underlying logging framework (Log4j2, Logback) and provides a unified
51+ * interface for Lambda context extraction, correlation ID handling, sampling rate configuration,
52+ * log buffering operations, and other Lambda-specific logging features.
53+ *
54+ * <p>This class serves as a programmatic alternative to AspectJ-based {@code @Logging} annotation,
55+ * allowing developers to integrate Powertools logging capabilities without AspectJ dependencies.</p>
56+ *
57+ * Key features:
58+ * <ul>
59+ * <li>Lambda context initialization with function metadata, trace ID, and service name</li>
60+ * <li>Sampling rate configuration for DEBUG logging</li>
61+ * <li>Backend-independent log buffer management (flush/clear operations)</li>
62+ * <li>MDC state management for structured logging</li>
63+ * </ul>
2564 */
2665public final class PowertoolsLogging {
66+ private static final Logger LOG = LoggerFactory .getLogger (PowertoolsLogging .class );
67+ private static final Random SAMPLER = new Random ();
68+ private static boolean hasBeenInitialized = false ;
69+
70+ static {
71+ initializeLogLevel ();
72+ }
2773
2874 private PowertoolsLogging () {
2975 // Utility class
3076 }
3177
78+ private static void initializeLogLevel () {
79+ if (POWERTOOLS_LOG_LEVEL != null ) {
80+ Level powertoolsLevel = getLevelFromString (POWERTOOLS_LOG_LEVEL );
81+ if (LAMBDA_LOG_LEVEL != null ) {
82+ Level lambdaLevel = getLevelFromString (LAMBDA_LOG_LEVEL );
83+ if (powertoolsLevel .toInt () < lambdaLevel .toInt ()) {
84+ LOG .warn (
85+ "Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them." ,
86+ POWERTOOLS_LOG_LEVEL , LAMBDA_LOG_LEVEL );
87+ }
88+ }
89+ setLogLevel (powertoolsLevel );
90+ } else if (LAMBDA_LOG_LEVEL != null ) {
91+ setLogLevel (getLevelFromString (LAMBDA_LOG_LEVEL ));
92+ }
93+ }
94+
95+ private static Level getLevelFromString (String level ) {
96+ if (Arrays .stream (Level .values ()).anyMatch (slf4jLevel -> slf4jLevel .name ().equalsIgnoreCase (level ))) {
97+ return Level .valueOf (level .toUpperCase (Locale .ROOT ));
98+ } else {
99+ // FATAL does not exist in slf4j
100+ if ("FATAL" .equalsIgnoreCase (level )) {
101+ return Level .ERROR ;
102+ }
103+ }
104+ // default to INFO if incorrect value
105+ return Level .INFO ;
106+ }
107+
108+ private static void setLogLevel (Level logLevel ) {
109+ LoggingManager loggingManager = LoggingManagerRegistry .getLoggingManager ();
110+ loggingManager .setLogLevel (logLevel );
111+ }
112+
32113 /**
33114 * Flushes the log buffer for the current Lambda execution.
34115 * This method will flush any buffered logs to the output stream.
@@ -52,4 +133,144 @@ public static void clearBuffer() {
52133 ((BufferManager ) loggingManager ).clearBuffer ();
53134 }
54135 }
136+
137+ /**
138+ * Initializes Lambda logging context with standard Powertools fields.
139+ * This method should be called at the beginning of your Lambda handler to set up
140+ * logging context with Lambda function information, trace ID, and service name.
141+ *
142+ * @param context the Lambda context provided by AWS Lambda runtime
143+ */
144+ public static void initializeLogging (Context context ) {
145+ initializeLogging (context , 0.0 , null , null );
146+ }
147+
148+ /**
149+ * Initializes Lambda logging context with sampling rate configuration.
150+ * This method sets up logging context and optionally enables DEBUG logging
151+ * based on the provided sampling rate.
152+ *
153+ * @param context the Lambda context provided by AWS Lambda runtime
154+ * @param samplingRate sampling rate for DEBUG logging (0.0 to 1.0)
155+ */
156+ public static void initializeLogging (Context context , double samplingRate ) {
157+ initializeLogging (context , samplingRate , null , null );
158+ }
159+
160+ /**
161+ * Initializes Lambda logging context with correlation ID extraction.
162+ * This method sets up logging context and extracts correlation ID from the event
163+ * using the provided JSON path.
164+ *
165+ * @param context the Lambda context provided by AWS Lambda runtime
166+ * @param correlationIdPath JSON path to extract correlation ID from event
167+ * @param event the Lambda event object
168+ */
169+ public static void initializeLogging (Context context , String correlationIdPath , Object event ) {
170+ initializeLogging (context , 0.0 , correlationIdPath , event );
171+ }
172+
173+ /**
174+ * Initializes Lambda logging context with full configuration.
175+ * This method sets up logging context with Lambda function information,
176+ * configures sampling rate for DEBUG logging, and optionally extracts
177+ * correlation ID from the event.
178+ *
179+ * @param context the Lambda context provided by AWS Lambda runtime
180+ * @param samplingRate sampling rate for DEBUG logging (0.0 to 1.0)
181+ * @param correlationIdPath JSON path to extract correlation ID from event (can be null)
182+ * @param event the Lambda event object (required if correlationIdPath is provided)
183+ */
184+ public static void initializeLogging (Context context , double samplingRate , String correlationIdPath , Object event ) {
185+ if (hasBeenInitialized ) {
186+ coldStartDone ();
187+ }
188+ hasBeenInitialized = true ;
189+
190+ addLambdaContextToLoggingContext (context );
191+ setLogLevelBasedOnSamplingRate (samplingRate );
192+ getXrayTraceId ().ifPresent (xRayTraceId -> MDC .put (FUNCTION_TRACE_ID .getName (), xRayTraceId ));
193+
194+ if (correlationIdPath != null && !correlationIdPath .isEmpty () && event != null ) {
195+ captureCorrelationId (correlationIdPath , event );
196+ }
197+ }
198+
199+ private static void addLambdaContextToLoggingContext (Context context ) {
200+ if (context != null ) {
201+ PowertoolsLoggedFields .setValuesFromLambdaContext (context ).forEach (MDC ::put );
202+ MDC .put (FUNCTION_COLD_START .getName (), isColdStart () ? "true" : "false" );
203+ MDC .put (SERVICE .getName (), serviceName ());
204+ }
205+ }
206+
207+ private static void setLogLevelBasedOnSamplingRate (double samplingRate ) {
208+ double effectiveSamplingRate = getEffectiveSamplingRate (samplingRate );
209+
210+ if (effectiveSamplingRate < 0 || effectiveSamplingRate > 1 ) {
211+ LOG .warn ("Skipping sampling rate configuration because of invalid value. Sampling rate: {}" ,
212+ effectiveSamplingRate );
213+ return ;
214+ }
215+
216+ MDC .put (SAMPLING_RATE .getName (), String .valueOf (effectiveSamplingRate ));
217+
218+ if (effectiveSamplingRate == 0 ) {
219+ return ;
220+ }
221+
222+ float sample = SAMPLER .nextFloat ();
223+ if (effectiveSamplingRate > sample ) {
224+ LoggingManager loggingManager = LoggingManagerRegistry .getLoggingManager ();
225+ loggingManager .setLogLevel (Level .DEBUG );
226+ LOG .debug (
227+ "Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {}, Sampler Value: {}." ,
228+ effectiveSamplingRate , sample );
229+ }
230+ }
231+
232+ // The environment variable takes precedence over manually set sampling rate
233+ private static double getEffectiveSamplingRate (double samplingRate ) {
234+ String envSampleRate = POWERTOOLS_SAMPLING_RATE ;
235+ if (envSampleRate != null ) {
236+ try {
237+ return Double .parseDouble (envSampleRate );
238+ } catch (NumberFormatException e ) {
239+ LOG .warn (
240+ "Skipping sampling rate on environment variable configuration because of invalid value. Sampling rate: {}" ,
241+ envSampleRate );
242+ }
243+ }
244+
245+ return samplingRate ;
246+ }
247+
248+ private static void captureCorrelationId (String correlationIdPath , Object event ) {
249+ try {
250+ JsonNode jsonNode = JsonConfig .get ().getObjectMapper ().valueToTree (event );
251+ Expression <JsonNode > jmesExpression = JsonConfig .get ().getJmesPath ().compile (correlationIdPath );
252+ JsonNode node = jmesExpression .search (jsonNode );
253+
254+ String asText = node .asText ();
255+ if (asText != null && !asText .isEmpty ()) {
256+ MDC .put (PowertoolsLoggedFields .CORRELATION_ID .getName (), asText );
257+ } else {
258+ LOG .warn ("Unable to extract any correlation id. Is your function expecting supported event type?" );
259+ }
260+ } catch (Exception e ) {
261+ LOG .warn ("Failed to capture correlation id from event." , e );
262+ }
263+ }
264+
265+ /**
266+ * Clears MDC state and log buffer.
267+ *
268+ * @param clearMdcState whether to clear MDC state
269+ */
270+ public static void clearState (boolean clearMdcState ) {
271+ if (clearMdcState ) {
272+ MDC .clear ();
273+ }
274+ clearBuffer ();
275+ }
55276}
0 commit comments