Skip to content

Commit 397a2d9

Browse files
committed
Initial version of PowertoolsLogging.initializeLogging API.
1 parent b923cd0 commit 397a2d9

File tree

9 files changed

+612
-335
lines changed

9 files changed

+612
-335
lines changed

powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
3737
import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
38+
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
3839
import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled;
3940

4041
@Order(1)
@@ -45,7 +46,10 @@ class PowerToolsResolverFactoryTest {
4546
@BeforeEach
4647
void setUp() throws IllegalAccessException, IOException {
4748
MDC.clear();
49+
// Reset cold start state
4850
writeStaticField(LambdaHandlerProcessor.class, "isColdStart", null, true);
51+
writeStaticField(PowertoolsLogging.class, "hasBeenInitialized", false, true);
52+
4953
context = new TestLambdaContext();
5054
// Make sure file is cleaned up before running tests
5155
try {

powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,32 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.contentOf;
2020

21-
import ch.qos.logback.classic.Level;
22-
import ch.qos.logback.classic.Logger;
23-
import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter;
24-
import ch.qos.logback.classic.spi.LoggingEvent;
25-
import com.amazonaws.services.lambda.runtime.Context;
2621
import java.io.File;
2722
import java.io.IOException;
2823
import java.nio.channels.FileChannel;
2924
import java.nio.charset.StandardCharsets;
3025
import java.nio.file.NoSuchFileException;
3126
import java.nio.file.Paths;
3227
import java.nio.file.StandardOpenOption;
28+
3329
import org.junit.jupiter.api.AfterEach;
3430
import org.junit.jupiter.api.BeforeEach;
3531
import org.junit.jupiter.api.Order;
3632
import org.junit.jupiter.api.Test;
37-
import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
3833
import org.slf4j.LoggerFactory;
3934
import org.slf4j.MDC;
35+
36+
import com.amazonaws.services.lambda.runtime.Context;
37+
38+
import ch.qos.logback.classic.Level;
39+
import ch.qos.logback.classic.Logger;
40+
import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter;
41+
import ch.qos.logback.classic.spi.LoggingEvent;
4042
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
41-
import software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder;
43+
import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
44+
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
4245
import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled;
46+
import software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder;
4347

4448
@Order(3)
4549
class LambdaEcsEncoderTest {
@@ -51,7 +55,10 @@ class LambdaEcsEncoderTest {
5155
@BeforeEach
5256
void setUp() throws IllegalAccessException, IOException {
5357
MDC.clear();
58+
// Reset cold start state
5459
writeStaticField(LambdaHandlerProcessor.class, "isColdStart", null, true);
60+
writeStaticField(PowertoolsLogging.class, "hasBeenInitialized", false, true);
61+
5562
context = new TestLambdaContext();
5663
// Make sure file is cleaned up before running tests
5764
try {

powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.slf4j.LoggerFactory;
5353
import org.slf4j.MDC;
5454
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
55+
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
5556
import software.amazon.lambda.powertools.logging.argument.StructuredArgument;
5657
import software.amazon.lambda.powertools.logging.argument.StructuredArguments;
5758
import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments;
@@ -73,7 +74,10 @@ class LambdaJsonEncoderTest {
7374
@BeforeEach
7475
void setUp() throws IllegalAccessException, IOException {
7576
MDC.clear();
77+
// Reset cold start state
7678
writeStaticField(LambdaHandlerProcessor.class, "isColdStart", null, true);
79+
writeStaticField(PowertoolsLogging.class, "hasBeenInitialized", false, true);
80+
7781
context = new TestLambdaContext();
7882
// Make sure file is cleaned up before running tests
7983
try {

powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/PowertoolsLogging.java

Lines changed: 224 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,102 @@
1414

1515
package 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;
1742
import software.amazon.lambda.powertools.logging.internal.BufferManager;
1843
import software.amazon.lambda.powertools.logging.internal.LoggingManager;
1944
import 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
*/
2665
public 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

Comments
 (0)