Skip to content

Commit

Permalink
feat: ALC (#1514)
Browse files Browse the repository at this point in the history
* handle AWS_LAMBDA_LOG configuration

* ALC documentation + code review

* update doc
  • Loading branch information
jeromevdl authored Nov 16, 2023
1 parent 30c8d33 commit 6b8fe49
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 7 deletions.
44 changes: 43 additions & 1 deletion docs/core/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ When debugging in non-production environments, you can instruct Logger to log th
}
```

### Customising fields in logs
### Customising fields in logs

- Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSSZz` and in system default timezone.
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:
Expand Down Expand Up @@ -596,6 +596,48 @@ via `samplingRate` attribute on annotation.
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5
```

## AWS Lambda Advanced Logging Controls
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.
Regardless of the output format setting in Lambda, Powertools for AWS Lambda will always output JSON formatted logging messages.

When you have this feature enabled, log messages that don’t meet the configured log level are discarded by Lambda.
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.

```mermaid
sequenceDiagram
participant Lambda service
participant Lambda function
participant Application Logger
Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN"
Lambda service->>Lambda function: Invoke (event)
Lambda function->>Lambda function: Calls handler
Lambda function->>Application Logger: logger.warn("Something happened")
Lambda function-->>Application Logger: logger.debug("Something happened")
Lambda function-->>Application Logger: logger.info("Something happened")
Lambda service->>Lambda service: DROP INFO and DEBUG logs
Lambda service->>CloudWatch Logs: Ingest error logs
```

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.

### Priority of log level settings in Powertools for AWS Lambda

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.

We prioritise log level settings in this order:

1. `AWS_LAMBDA_LOG_LEVEL` environment variable
2. `POWERTOOLS_LOG_LEVEL` environment variable

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.

### Timestamp format

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).
In this case the format will be `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.

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

Expand Down
10 changes: 10 additions & 0 deletions powertools-logging/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<environmentVariables>
<AWS_LAMBDA_LOG_FORMAT>JSON</AWS_LAMBDA_LOG_FORMAT>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey;
import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys;
import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper;
import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_LEVEL;

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

private static final String LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL");
private static final Random SAMPLER = new Random();
private static final String SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE");
private static Boolean LOG_EVENT;

private static Level LEVEL_AT_INITIALISATION;

static {
if (null != LOG_LEVEL) {
resetLogLevels(Level.getLevel(LOG_LEVEL));
if (POWERTOOLS_LOG_LEVEL != null) {
Level powertoolsLevel = Level.getLevel(POWERTOOLS_LOG_LEVEL);
if (LAMBDA_LOG_LEVEL != null) {
Level lambdaLevel = Level.getLevel(LAMBDA_LOG_LEVEL);
if (powertoolsLevel.intLevel() > lambdaLevel.intLevel()) {
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.",
POWERTOOLS_LOG_LEVEL, LAMBDA_LOG_LEVEL);
}
}
resetLogLevels(powertoolsLevel);
} else if (LAMBDA_LOG_LEVEL != null) {
resetLogLevels(Level.getLevel(LAMBDA_LOG_LEVEL));
}

LEVEL_AT_INITIALISATION = LOG.getLevel();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.logging.internal;

import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_FORMAT;
import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LOG_DATE_RFC3339_FORMAT;

import java.util.Locale;
import java.util.TimeZone;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
import org.apache.logging.log4j.layout.template.json.util.InstantFormatter;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;

/**
* Default timestamp used by log4j is not RFC3339, which is used by Lambda internally to filter logs.
* When `AWS_LAMBDA_LOG_FORMAT` is set to JSON (i.e. using Lambda logging configuration), we should use the appropriate pattern,
* otherwise logs with invalid date format are considered as INFO.
* Inspired from org.apache.logging.log4j.layout.template.json.resolver.TimestampResolver
*
* TODO: remove in v2 an replace with the good pattern in LambdaJsonLayout.json
*/
public class LambdaTimestampResolver implements EventResolver {

private final EventResolver internalResolver;

public LambdaTimestampResolver(final TemplateResolverConfig config) {
final PatternResolverContext patternResolverContext =
PatternResolverContext.fromConfig(config);
internalResolver = new PatternResolver(patternResolverContext);
}

@Override
public void resolve(LogEvent value, JsonWriter jsonWriter) {
internalResolver.resolve(value, jsonWriter);
}

static String getName() {
return "lambda-timestamp";
}

private static final class PatternResolverContext {

public static final String PATTERN = "pattern";
private final InstantFormatter formatter;

private final StringBuilder lastFormattedInstantBuffer = new StringBuilder();

private final MutableInstant lastFormattedInstant = new MutableInstant();

private PatternResolverContext(
final String pattern,
final TimeZone timeZone,
final Locale locale) {
this.formatter = InstantFormatter
.newBuilder()
.setPattern(pattern)
.setTimeZone(timeZone)
.setLocale(locale)
.build();
lastFormattedInstant.initFromEpochSecond(-1, 0);
}

private static PatternResolverContext fromConfig(
final TemplateResolverConfig config) {
final String pattern = readPattern(config);
final TimeZone timeZone = readTimeZone(config);
final Locale locale = config.getLocale(new String[]{PATTERN, "locale"});
return new PatternResolverContext(pattern, timeZone, locale);
}

private static String readPattern(final TemplateResolverConfig config) {
final String format = config.getString(new String[]{PATTERN, "format"});
return format != null
? format
: getLambdaTimestampFormatOrDefault();
}

private static String getLambdaTimestampFormatOrDefault() {
return "JSON".equals(LAMBDA_LOG_FORMAT) ? LOG_DATE_RFC3339_FORMAT :
JsonTemplateLayoutDefaults.getTimestampFormatPattern();
}

private static TimeZone readTimeZone(final TemplateResolverConfig config) {
final String timeZoneId = config.getString(new String[]{PATTERN, "timeZone"});
if (timeZoneId == null) {
return JsonTemplateLayoutDefaults.getTimeZone();
}
boolean found = false;
for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
found = true;
break;
}
}
if (!found) {
throw new IllegalArgumentException(
"invalid timestamp time zone: " + config);
}
return TimeZone.getTimeZone(timeZoneId);
}

}

private static final class PatternResolver implements EventResolver {

private final PatternResolverContext patternResolverContext;

private PatternResolver(final PatternResolverContext patternResolverContext) {
this.patternResolverContext = patternResolverContext;
}

@Override
public synchronized void resolve(
final LogEvent logEvent,
final JsonWriter jsonWriter) {

// Format timestamp if it doesn't match the last cached one.
final boolean instantMatching = patternResolverContext.formatter.isInstantMatching(
patternResolverContext.lastFormattedInstant,
logEvent.getInstant());
if (!instantMatching) {

// Format the timestamp.
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant());
patternResolverContext.formatter.format(
patternResolverContext.lastFormattedInstant,
patternResolverContext.lastFormattedInstantBuffer);

// Write the formatted timestamp.
final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
final int startIndex = jsonWriterStringBuilder.length();
jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer);

// Cache the written value.
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
patternResolverContext.lastFormattedInstantBuffer.append(
jsonWriterStringBuilder,
startIndex,
jsonWriterStringBuilder.length());

}

// Write the cached formatted timestamp.
else {
jsonWriter.writeRawString(
patternResolverContext.lastFormattedInstantBuffer);
}

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.logging.internal;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;

@Plugin(name = "LambdaTimestampResolverFactory", category = TemplateResolverFactory.CATEGORY)
public final class LambdaTimestampResolverFactory implements EventResolverFactory {

private static final LambdaTimestampResolverFactory INSTANCE = new LambdaTimestampResolverFactory();

private LambdaTimestampResolverFactory() {
}

@PluginFactory
public static LambdaTimestampResolverFactory getInstance() {
return INSTANCE;
}

@Override
public String getName() {
return LambdaTimestampResolver.getName();
}

@Override
public TemplateResolver<LogEvent> create(EventResolverContext context,
TemplateResolverConfig config) {
return new LambdaTimestampResolver(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.logging.internal;

public class LoggingConstants {
public static final String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL");

public static final String LAMBDA_LOG_FORMAT = System.getenv("AWS_LAMBDA_LOG_FORMAT");

public static final String LOG_DATE_RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

private LoggingConstants() {
// constants
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"timestamp": {
"$resolver": "timestamp"
"$resolver": "lambda-timestamp"
},
"instant": {
"epochSecond": {
Expand Down
Loading

0 comments on commit 6b8fe49

Please sign in to comment.