Skip to content

Commit 9a3a788

Browse files
jeromevdlJason Harris
authored and
Jason Harris
committed
feat: ALC (#1514)
* handle AWS_LAMBDA_LOG configuration * ALC documentation + code review * update doc
1 parent f814d4d commit 9a3a788

File tree

8 files changed

+344
-7
lines changed

8 files changed

+344
-7
lines changed

docs/core/logging.md

+43-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ When debugging in non-production environments, you can instruct Logger to log th
263263
}
264264
```
265265

266-
### Customising fields in logs
266+
### Customising fields in logs
267267

268268
- Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSSZz` and in system default timezone.
269269
If you need to customize format and timezone, you can do so by configuring `log4j2.component.properties` and configuring properties as shown in example below:
@@ -596,6 +596,48 @@ via `samplingRate` attribute on annotation.
596596
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5
597597
```
598598

599+
## AWS Lambda Advanced Logging Controls
600+
With AWS [Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced), you can control the output format of your logs as either `TEXT` or `JSON` and specify the minimum accepted log level for your application.
601+
Regardless of the output format setting in Lambda, Powertools for AWS Lambda will always output JSON formatted logging messages.
602+
603+
When you have this feature enabled, log messages that don’t meet the configured log level are discarded by Lambda.
604+
For example, if you set the minimum log level to `WARN`, you will only receive `WARN` and `ERROR` messages in your AWS CloudWatch Logs, all other log levels will be discarded by Lambda.
605+
606+
```mermaid
607+
sequenceDiagram
608+
participant Lambda service
609+
participant Lambda function
610+
participant Application Logger
611+
612+
Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN"
613+
Lambda service->>Lambda function: Invoke (event)
614+
Lambda function->>Lambda function: Calls handler
615+
Lambda function->>Application Logger: logger.warn("Something happened")
616+
Lambda function-->>Application Logger: logger.debug("Something happened")
617+
Lambda function-->>Application Logger: logger.info("Something happened")
618+
619+
Lambda service->>Lambda service: DROP INFO and DEBUG logs
620+
621+
Lambda service->>CloudWatch Logs: Ingest error logs
622+
```
623+
624+
Logger will automatically listen for the `AWS_LAMBDA_LOG_FORMAT` and `AWS_LAMBDA_LOG_LEVEL` environment variables, and change behaviour if they’re found to ensure as much compatibility as possible.
625+
626+
### Priority of log level settings in Powertools for AWS Lambda
627+
628+
When the Advanced Logging Controls feature is enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level) for more details.
629+
630+
We prioritise log level settings in this order:
631+
632+
1. `AWS_LAMBDA_LOG_LEVEL` environment variable
633+
2. `POWERTOOLS_LOG_LEVEL` environment variable
634+
635+
In the event you have set `POWERTOOLS_LOG_LEVEL` to a level lower than the ACL setting, Powertools for AWS Lambda will output a warning log message informing you that your messages will be discarded by Lambda.
636+
637+
### Timestamp format
638+
639+
When the Advanced Logging Controls feature is enabled, Powertools for AWS Lambda must comply with the timestamp format required by AWS Lambda, which is [RFC3339](https://www.rfc-editor.org/rfc/rfc3339).
640+
In this case the format will be `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.
599641

600642
## Upgrade to JsonTemplateLayout from deprecated LambdaJsonLayout configuration in log4j2.xml
601643

powertools-logging/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@
137137
<groupId>org.apache.maven.plugins</groupId>
138138
<artifactId>maven-checkstyle-plugin</artifactId>
139139
</plugin>
140+
<plugin>
141+
<groupId>org.apache.maven.plugins</groupId>
142+
<artifactId>maven-surefire-plugin</artifactId>
143+
<version>3.1.2</version>
144+
<configuration>
145+
<environmentVariables>
146+
<AWS_LAMBDA_LOG_FORMAT>JSON</AWS_LAMBDA_LOG_FORMAT>
147+
</environmentVariables>
148+
</configuration>
149+
</plugin>
140150
</plugins>
141151
</build>
142152
</project>

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey;
2929
import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys;
3030
import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper;
31+
import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_LEVEL;
3132

3233
import com.amazonaws.services.lambda.runtime.Context;
3334
import com.fasterxml.jackson.core.JsonPointer;
@@ -63,17 +64,27 @@
6364
@DeclarePrecedence("*, software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect")
6465
public final class LambdaLoggingAspect {
6566
private static final Logger LOG = LogManager.getLogger(LambdaLoggingAspect.class);
66-
private static final Random SAMPLER = new Random();
67+
private static final String POWERTOOLS_LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL");
6768

68-
private static final String LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL");
69+
private static final Random SAMPLER = new Random();
6970
private static final String SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE");
7071
private static Boolean LOG_EVENT;
7172

7273
private static Level LEVEL_AT_INITIALISATION;
7374

7475
static {
75-
if (null != LOG_LEVEL) {
76-
resetLogLevels(Level.getLevel(LOG_LEVEL));
76+
if (POWERTOOLS_LOG_LEVEL != null) {
77+
Level powertoolsLevel = Level.getLevel(POWERTOOLS_LOG_LEVEL);
78+
if (LAMBDA_LOG_LEVEL != null) {
79+
Level lambdaLevel = Level.getLevel(LAMBDA_LOG_LEVEL);
80+
if (powertoolsLevel.intLevel() > lambdaLevel.intLevel()) {
81+
LOG.warn("Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them.",
82+
POWERTOOLS_LOG_LEVEL, LAMBDA_LOG_LEVEL);
83+
}
84+
}
85+
resetLogLevels(powertoolsLevel);
86+
} else if (LAMBDA_LOG_LEVEL != null) {
87+
resetLogLevels(Level.getLevel(LAMBDA_LOG_LEVEL));
7788
}
7889

7990
LEVEL_AT_INITIALISATION = LOG.getLevel();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2023 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+
15+
package software.amazon.lambda.powertools.logging.internal;
16+
17+
import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_FORMAT;
18+
import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LOG_DATE_RFC3339_FORMAT;
19+
20+
import java.util.Locale;
21+
import java.util.TimeZone;
22+
import org.apache.logging.log4j.core.LogEvent;
23+
import org.apache.logging.log4j.core.time.MutableInstant;
24+
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
25+
import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
26+
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
27+
import org.apache.logging.log4j.layout.template.json.util.InstantFormatter;
28+
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
29+
30+
/**
31+
* Default timestamp used by log4j is not RFC3339, which is used by Lambda internally to filter logs.
32+
* When `AWS_LAMBDA_LOG_FORMAT` is set to JSON (i.e. using Lambda logging configuration), we should use the appropriate pattern,
33+
* otherwise logs with invalid date format are considered as INFO.
34+
* Inspired from org.apache.logging.log4j.layout.template.json.resolver.TimestampResolver
35+
*
36+
* TODO: remove in v2 an replace with the good pattern in LambdaJsonLayout.json
37+
*/
38+
public class LambdaTimestampResolver implements EventResolver {
39+
40+
private final EventResolver internalResolver;
41+
42+
public LambdaTimestampResolver(final TemplateResolverConfig config) {
43+
final PatternResolverContext patternResolverContext =
44+
PatternResolverContext.fromConfig(config);
45+
internalResolver = new PatternResolver(patternResolverContext);
46+
}
47+
48+
@Override
49+
public void resolve(LogEvent value, JsonWriter jsonWriter) {
50+
internalResolver.resolve(value, jsonWriter);
51+
}
52+
53+
static String getName() {
54+
return "lambda-timestamp";
55+
}
56+
57+
private static final class PatternResolverContext {
58+
59+
public static final String PATTERN = "pattern";
60+
private final InstantFormatter formatter;
61+
62+
private final StringBuilder lastFormattedInstantBuffer = new StringBuilder();
63+
64+
private final MutableInstant lastFormattedInstant = new MutableInstant();
65+
66+
private PatternResolverContext(
67+
final String pattern,
68+
final TimeZone timeZone,
69+
final Locale locale) {
70+
this.formatter = InstantFormatter
71+
.newBuilder()
72+
.setPattern(pattern)
73+
.setTimeZone(timeZone)
74+
.setLocale(locale)
75+
.build();
76+
lastFormattedInstant.initFromEpochSecond(-1, 0);
77+
}
78+
79+
private static PatternResolverContext fromConfig(
80+
final TemplateResolverConfig config) {
81+
final String pattern = readPattern(config);
82+
final TimeZone timeZone = readTimeZone(config);
83+
final Locale locale = config.getLocale(new String[]{PATTERN, "locale"});
84+
return new PatternResolverContext(pattern, timeZone, locale);
85+
}
86+
87+
private static String readPattern(final TemplateResolverConfig config) {
88+
final String format = config.getString(new String[]{PATTERN, "format"});
89+
return format != null
90+
? format
91+
: getLambdaTimestampFormatOrDefault();
92+
}
93+
94+
private static String getLambdaTimestampFormatOrDefault() {
95+
return "JSON".equals(LAMBDA_LOG_FORMAT) ? LOG_DATE_RFC3339_FORMAT :
96+
JsonTemplateLayoutDefaults.getTimestampFormatPattern();
97+
}
98+
99+
private static TimeZone readTimeZone(final TemplateResolverConfig config) {
100+
final String timeZoneId = config.getString(new String[]{PATTERN, "timeZone"});
101+
if (timeZoneId == null) {
102+
return JsonTemplateLayoutDefaults.getTimeZone();
103+
}
104+
boolean found = false;
105+
for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
106+
if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
107+
found = true;
108+
break;
109+
}
110+
}
111+
if (!found) {
112+
throw new IllegalArgumentException(
113+
"invalid timestamp time zone: " + config);
114+
}
115+
return TimeZone.getTimeZone(timeZoneId);
116+
}
117+
118+
}
119+
120+
private static final class PatternResolver implements EventResolver {
121+
122+
private final PatternResolverContext patternResolverContext;
123+
124+
private PatternResolver(final PatternResolverContext patternResolverContext) {
125+
this.patternResolverContext = patternResolverContext;
126+
}
127+
128+
@Override
129+
public synchronized void resolve(
130+
final LogEvent logEvent,
131+
final JsonWriter jsonWriter) {
132+
133+
// Format timestamp if it doesn't match the last cached one.
134+
final boolean instantMatching = patternResolverContext.formatter.isInstantMatching(
135+
patternResolverContext.lastFormattedInstant,
136+
logEvent.getInstant());
137+
if (!instantMatching) {
138+
139+
// Format the timestamp.
140+
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
141+
patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant());
142+
patternResolverContext.formatter.format(
143+
patternResolverContext.lastFormattedInstant,
144+
patternResolverContext.lastFormattedInstantBuffer);
145+
146+
// Write the formatted timestamp.
147+
final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
148+
final int startIndex = jsonWriterStringBuilder.length();
149+
jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer);
150+
151+
// Cache the written value.
152+
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
153+
patternResolverContext.lastFormattedInstantBuffer.append(
154+
jsonWriterStringBuilder,
155+
startIndex,
156+
jsonWriterStringBuilder.length());
157+
158+
}
159+
160+
// Write the cached formatted timestamp.
161+
else {
162+
jsonWriter.writeRawString(
163+
patternResolverContext.lastFormattedInstantBuffer);
164+
}
165+
166+
}
167+
168+
}
169+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2023 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+
15+
package software.amazon.lambda.powertools.logging.internal;
16+
17+
import org.apache.logging.log4j.core.LogEvent;
18+
import org.apache.logging.log4j.core.config.plugins.Plugin;
19+
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
20+
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
21+
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
22+
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
23+
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
24+
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
25+
26+
@Plugin(name = "LambdaTimestampResolverFactory", category = TemplateResolverFactory.CATEGORY)
27+
public final class LambdaTimestampResolverFactory implements EventResolverFactory {
28+
29+
private static final LambdaTimestampResolverFactory INSTANCE = new LambdaTimestampResolverFactory();
30+
31+
private LambdaTimestampResolverFactory() {
32+
}
33+
34+
@PluginFactory
35+
public static LambdaTimestampResolverFactory getInstance() {
36+
return INSTANCE;
37+
}
38+
39+
@Override
40+
public String getName() {
41+
return LambdaTimestampResolver.getName();
42+
}
43+
44+
@Override
45+
public TemplateResolver<LogEvent> create(EventResolverContext context,
46+
TemplateResolverConfig config) {
47+
return new LambdaTimestampResolver(config);
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2023 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+
15+
package software.amazon.lambda.powertools.logging.internal;
16+
17+
public class LoggingConstants {
18+
public static final String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL");
19+
20+
public static final String LAMBDA_LOG_FORMAT = System.getenv("AWS_LAMBDA_LOG_FORMAT");
21+
22+
public static final String LOG_DATE_RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
23+
24+
private LoggingConstants() {
25+
// constants
26+
}
27+
}

powertools-logging/src/main/resources/LambdaJsonLayout.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"timestamp": {
3-
"$resolver": "timestamp"
3+
"$resolver": "lambda-timestamp"
44
},
55
"instant": {
66
"epochSecond": {

0 commit comments

Comments
 (0)