Skip to content

Commit 2a23f72

Browse files
Support log sampling to DEBUG level based on config (#52)
1 parent fb19b00 commit 2a23f72

File tree

8 files changed

+196
-40
lines changed

8 files changed

+196
-40
lines changed

docs/content/core/logging.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Key | Type | Example | Description
6363
**level** | String | "INFO" | Logging level
6464
**coldStart** | Boolean | true| ColdStart value.
6565
**service** | String | "payment" | Service name defined. "service_undefined" will be used if unknown
66+
**samplingRate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case
6667
**message** | String | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string
6768
**functionName**| String | "example-powertools-HelloWorldFunction-1P1Z6B39FLU73"
6869
**functionVersion**| String | "12"
@@ -161,4 +162,48 @@ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatew
161162
...
162163
}
163164
}
165+
```
166+
167+
## Sampling debug logs
168+
169+
You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or
170+
via `samplingRate` attribute on annotation.
171+
172+
Configuration on environment variable is given precedence over sampling rate configuration on annotation, provided it's in valid value range.
173+
174+
```java:title=App.java
175+
package helloworld;
176+
177+
import org.apache.logging.log4j.LogManager;
178+
import org.apache.logging.log4j.Logger;
179+
import software.amazon.lambda.logging.PowertoolsLogger;
180+
import software.amazon.lambda.logging.PowertoolsLogging;
181+
...
182+
183+
/**
184+
* Handler for requests to Lambda function.
185+
*/
186+
public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
187+
188+
Logger log = LogManager.getLogger();
189+
190+
@PowertoolsLogging(samplingRate = 0.5)
191+
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
192+
...
193+
}
194+
}
195+
```
196+
197+
You can also override sampling by setting `POWERTOOLS_LOGGER_SAMPLE_RATE` env var - Here is an example using AWS Serverless Application Model (SAM)
198+
199+
```yaml:title=template.yaml
200+
Resources:
201+
HelloWorldFunction:
202+
Type: AWS::Serverless::Function
203+
Properties:
204+
...
205+
Runtime: java8
206+
Environment:
207+
Variables:
208+
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 # highlight-line
164209
```

example/HelloWorldFunction/src/main/java/helloworld/App.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatew
3131

3232
Logger log = LogManager.getLogger();
3333

34-
@PowertoolsLogging(logEvent = true)
34+
@PowertoolsLogging(logEvent = true, samplingRate = 0.7)
3535
@PowertoolsTracing(captureError = false, captureResponse = false)
3636
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
3737
Map<String, String> headers = new HashMap<>();

example/template.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Resources:
3939
Tracing: Active
4040
Environment:
4141
Variables:
42-
PARAM1: VALUE
42+
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.7
4343
Events:
4444
HelloWorld:
4545
Type: Api

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
* <ul>
4545
* <li>coldStart - True if this is the first invocation of this Lambda execution environment; else False</li>
4646
* <li>service - The value of the 'POWER_TOOLS_SERVICE_NAME' environment variable or 'service_undefined'</li>
47+
* <li>samplingRate - The value of the 'POWERTOOLS_LOGGER_SAMPLE_RATE' environment variable or value of samplingRate field or 0.
48+
* Valid value is from 0.0 to 1.0. Value outside this range is silently ignored.</li>
4749
* </ul>
4850
*
4951
* <p>These keys and values will be joined with the existing Log4J log event and written as JSON.</p>
@@ -53,11 +55,16 @@
5355
* <p>By default {@code PowertoolsLogging} will not log the event which has trigger the invoke of the Lambda function.
5456
* This can be enabled using {@code @PowertoolsLogging(logEvent = true)}.</p>
5557
*
58+
* <p>By default {@code PowertoolsLogging} all debug loggs will follow log4j2 configuration unless configured via
59+
* POWERTOOLS_LOGGER_SAMPLE_RATE environment variable {@code @PowertoolsLogging(samplingRate = <0.0-1.0>)}.</p>
60+
*
5661
* <p>To append additional keys to each log entry you can use {@link PowertoolsLogger#appendKey(String, String)}</p>
5762
*/
5863
@Retention(RetentionPolicy.RUNTIME)
5964
@Target(ElementType.METHOD)
6065
public @interface PowertoolsLogging {
6166

6267
boolean logEvent() default false;
68+
69+
double samplingRate() default 0;
6370
}

powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@
1313
*/
1414
package software.amazon.lambda.powertools.logging.internal;
1515

16+
import java.io.ByteArrayInputStream;
17+
import java.io.ByteArrayOutputStream;
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.InputStreamReader;
21+
import java.io.OutputStreamWriter;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Random;
25+
1626
import com.amazonaws.services.lambda.runtime.Context;
1727
import com.fasterxml.jackson.databind.ObjectMapper;
1828
import org.apache.logging.log4j.Level;
1929
import org.apache.logging.log4j.LogManager;
2030
import org.apache.logging.log4j.Logger;
21-
import org.apache.logging.log4j.ThreadContext;
2231
import org.apache.logging.log4j.core.LoggerContext;
2332
import org.apache.logging.log4j.core.config.Configurator;
2433
import org.apache.logging.log4j.core.util.IOUtils;
@@ -28,15 +37,6 @@
2837
import org.aspectj.lang.annotation.Pointcut;
2938
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
3039

31-
import java.io.ByteArrayInputStream;
32-
import java.io.ByteArrayOutputStream;
33-
import java.io.IOException;
34-
import java.io.InputStream;
35-
import java.io.InputStreamReader;
36-
import java.io.OutputStreamWriter;
37-
import java.util.Map;
38-
import java.util.Optional;
39-
4040
import static java.util.Optional.empty;
4141
import static java.util.Optional.of;
4242
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone;
@@ -45,22 +45,26 @@
4545
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler;
4646
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnStreamHandler;
4747
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName;
48+
import static software.amazon.lambda.powertools.logging.PowertoolsLogger.appendKey;
49+
import static software.amazon.lambda.powertools.logging.PowertoolsLogger.appendKeys;
4850

4951
@Aspect
5052
public final class LambdaLoggingAspect {
51-
private static final ObjectMapper mapper = new ObjectMapper();
52-
private static String LOG_LEVEL = System.getenv("LOG_LEVEL");
53+
private static final Logger LOG = LogManager.getLogger(LambdaLoggingAspect.class);
54+
private static final ObjectMapper MAPPER = new ObjectMapper();
55+
private static final Random SAMPLER = new Random();
5356

54-
static {
55-
resetLogLevels();
56-
}
57+
private static final String LOG_LEVEL = System.getenv("LOG_LEVEL");
58+
private static final String SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE");
5759

58-
private static void resetLogLevels() {
59-
if (LOG_LEVEL != null) {
60-
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
61-
Configurator.setAllLevels(LogManager.getRootLogger().getName(), Level.getLevel(LOG_LEVEL));
62-
ctx.updateLoggers();
60+
private static Level LEVEL_AT_INITIALISATION;
61+
62+
static {
63+
if (null != LOG_LEVEL) {
64+
resetLogLevels(Level.getLevel(LOG_LEVEL));
6365
}
66+
67+
LEVEL_AT_INITIALISATION = LOG.getLevel();
6468
}
6569

6670
@SuppressWarnings({"EmptyMethod"})
@@ -73,11 +77,13 @@ public Object around(ProceedingJoinPoint pjp,
7377
PowertoolsLogging powertoolsLogging) throws Throwable {
7478
Object[] proceedArgs = pjp.getArgs();
7579

80+
setLogLevelBasedOnSamplingRate(pjp, powertoolsLogging);
81+
7682
extractContext(pjp)
7783
.ifPresent(context -> {
78-
ThreadContext.putAll(DefaultLambdaFields.values(context));
79-
ThreadContext.put("coldStart", null == isColdStart() ? "true" : "false");
80-
ThreadContext.put("service", serviceName());
84+
appendKeys(DefaultLambdaFields.values(context));
85+
appendKey("coldStart", null == isColdStart() ? "true" : "false");
86+
appendKey("service", serviceName());
8187
});
8288

8389

@@ -91,7 +97,49 @@ public Object around(ProceedingJoinPoint pjp,
9197
return proceed;
9298
}
9399

94-
private Optional<Context> extractContext(ProceedingJoinPoint pjp) {
100+
private static void resetLogLevels(Level logLevel) {
101+
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
102+
Configurator.setAllLevels(LogManager.getRootLogger().getName(), logLevel);
103+
ctx.updateLoggers();
104+
}
105+
106+
private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp,
107+
final PowertoolsLogging powertoolsLogging) {
108+
if (isHandlerMethod(pjp)) {
109+
float sample = SAMPLER.nextFloat();
110+
double samplingRate = samplingRate(powertoolsLogging);
111+
112+
if (samplingRate < 0 || samplingRate > 1) {
113+
LOG.debug("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", samplingRate);
114+
return;
115+
}
116+
117+
appendKey("samplingRate", String.valueOf(samplingRate));
118+
119+
if (samplingRate > sample) {
120+
resetLogLevels(Level.DEBUG);
121+
122+
LOG.debug("Changed log level to DEBUG based on Sampling configuration. " +
123+
"Sampling Rate: {}, Sampler Value: {}.", samplingRate, sample);
124+
} else if (LEVEL_AT_INITIALISATION != LOG.getLevel()) {
125+
resetLogLevels(LEVEL_AT_INITIALISATION);
126+
}
127+
}
128+
}
129+
130+
private double samplingRate(final PowertoolsLogging powertoolsLogging) {
131+
if (null != SAMPLING_RATE) {
132+
try {
133+
return Double.parseDouble(SAMPLING_RATE);
134+
} catch (NumberFormatException e) {
135+
LOG.debug("Skipping sampling rate on environment variable configuration because of invalid " +
136+
"value. Sampling rate: {}", SAMPLING_RATE);
137+
}
138+
}
139+
return powertoolsLogging.samplingRate();
140+
}
141+
142+
private Optional<Context> extractContext(final ProceedingJoinPoint pjp) {
95143

96144
if (isHandlerMethod(pjp)) {
97145
if (placedOnRequestHandler(pjp)) {
@@ -106,7 +154,7 @@ private Optional<Context> extractContext(ProceedingJoinPoint pjp) {
106154
return empty();
107155
}
108156

109-
private Object[] logEvent(ProceedingJoinPoint pjp) {
157+
private Object[] logEvent(final ProceedingJoinPoint pjp) {
110158
Object[] args = pjp.getArgs();
111159

112160
if (isHandlerMethod(pjp)) {
@@ -123,7 +171,7 @@ private Object[] logEvent(ProceedingJoinPoint pjp) {
123171
return args;
124172
}
125173

126-
private Object[] logFromInputStream(ProceedingJoinPoint pjp) {
174+
private Object[] logFromInputStream(final ProceedingJoinPoint pjp) {
127175
Object[] args = pjp.getArgs();
128176

129177
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -136,7 +184,7 @@ private Object[] logFromInputStream(ProceedingJoinPoint pjp) {
136184
args[0] = new ByteArrayInputStream(bytes);
137185

138186
Logger log = logger(pjp);
139-
log.info(mapper.readValue(bytes, Map.class));
187+
log.info(MAPPER.readValue(bytes, Map.class));
140188

141189
} catch (IOException e) {
142190
Logger log = logger(pjp);
@@ -146,7 +194,7 @@ private Object[] logFromInputStream(ProceedingJoinPoint pjp) {
146194
return args;
147195
}
148196

149-
private Logger logger(ProceedingJoinPoint pjp) {
197+
private Logger logger(final ProceedingJoinPoint pjp) {
150198
return LogManager.getLogger(pjp.getSignature().getDeclaringType());
151199
}
152200
}

powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import com.amazonaws.services.lambda.runtime.RequestHandler;
2727
import com.fasterxml.jackson.core.JsonProcessingException;
2828
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import org.apache.logging.log4j.Level;
2930
import org.junit.jupiter.api.BeforeEach;
3031
import org.junit.jupiter.api.Test;
3132
import org.mockito.Mock;
3233
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled;
34+
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled;
3335
import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect;
3436

3537
import static java.util.Collections.emptyMap;
@@ -41,7 +43,7 @@
4143

4244
class LambdaJsonLayoutTest {
4345

44-
private final RequestHandler<Object, Object> handler = new PowerLogToolEnabled();
46+
private RequestHandler<Object, Object> handler = new PowerLogToolEnabled();
4547

4648
@Mock
4749
private Context context;
@@ -52,8 +54,7 @@ void setUp() throws IOException, IllegalAccessException, NoSuchMethodException,
5254
setupContext();
5355
//Make sure file is cleaned up before running full stack logging regression
5456
FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close();
55-
writeStaticField(LambdaLoggingAspect.class, "LOG_LEVEL", "INFO", true);
56-
resetLogLevel();
57+
resetLogLevel(Level.INFO);
5758
}
5859

5960
@Test
@@ -74,8 +75,7 @@ void shouldLogInStructuredFormat() throws IOException {
7475

7576
@Test
7677
void shouldModifyLogLevelBasedOnEnvVariable() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException {
77-
writeStaticField(LambdaLoggingAspect.class, "LOG_LEVEL", "DEBUG", true);
78-
resetLogLevel();
78+
resetLogLevel(Level.DEBUG);
7979

8080
handler.handleRequest("test", context);
8181

@@ -92,10 +92,34 @@ void shouldModifyLogLevelBasedOnEnvVariable() throws IllegalAccessException, IOE
9292
});
9393
}
9494

95-
private void resetLogLevel() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
96-
Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels");
95+
@Test
96+
void shouldModifyLogLevelBasedOnSamplingRule() throws IOException {
97+
handler = new PowerLogToolSamplingEnabled();
98+
99+
handler.handleRequest("test", context);
100+
101+
assertThat(Files.lines(Paths.get("target/logfile.json")))
102+
.hasSize(3)
103+
.satisfies(line -> {
104+
assertThat(parseToMap(line.get(0)))
105+
.containsEntry("level", "DEBUG")
106+
.containsEntry("loggerName", LambdaLoggingAspect.class.getCanonicalName());
107+
108+
assertThat(parseToMap(line.get(1)))
109+
.containsEntry("level", "INFO")
110+
.containsEntry("message", "Test event");
111+
112+
assertThat(parseToMap(line.get(2)))
113+
.containsEntry("level", "DEBUG")
114+
.containsEntry("message", "Test debug event");
115+
});
116+
}
117+
118+
private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
119+
Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class);
97120
resetLogLevels.setAccessible(true);
98-
resetLogLevels.invoke(null);
121+
resetLogLevels.invoke(null, level);
122+
writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true);
99123
}
100124

101125
private Map<String, Object> parseToMap(String stringAsJson) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
package software.amazon.lambda.powertools.logging.handlers;
15+
16+
import com.amazonaws.services.lambda.runtime.Context;
17+
import com.amazonaws.services.lambda.runtime.RequestHandler;
18+
import org.apache.logging.log4j.LogManager;
19+
import org.apache.logging.log4j.Logger;
20+
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
21+
22+
public class PowerLogToolSamplingEnabled implements RequestHandler<Object, Object> {
23+
private final Logger LOG = LogManager.getLogger(PowerLogToolSamplingEnabled.class);
24+
25+
@Override
26+
@PowertoolsLogging(samplingRate = 1.0)
27+
public Object handleRequest(Object input, Context context) {
28+
LOG.info("Test event");
29+
LOG.debug("Test debug event");
30+
return null;
31+
}
32+
}

0 commit comments

Comments
 (0)