Skip to content

Commit 26c8e58

Browse files
committed
feat(logger): add support for AWS_LAMBDA_LOG_LEVEL and POWERTOOLS_LOG_LEVEL (#1795)
* feat(logger): support advanced logging * docs(logger): add alc info * feat(logger): support alc * docs: fix alc docs links * tests(logger): add unit tests for the feature * docs(logger): make POWERTOOLS_LOG_LEVEL default
1 parent 94e8bd9 commit 26c8e58

17 files changed

+219
-35
lines changed

Diff for: docs/core/logger.md

+47-10
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ The library requires two settings. You can set them as environment variables, or
4646

4747
These settings will be used across all logs emitted:
4848

49-
| Setting | Description | Environment variable | Default Value | Allowed Values | Example Value | Constructor parameter |
50-
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------- | ------------------------------------------ | ------------------- | --------------------- |
51-
| **Service name** | Sets the name of service of which the Lambda function is part of, that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` |
52-
| **Logging level** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | `LOG_LEVEL` | `info` | `DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT` | `ERROR` | `logLevel` |
53-
| **Log incoming event** | Whether to log or not the incoming event when using the decorator or middleware | `POWERTOOLS_LOGGER_LOG_EVENT` | `false` | `true`, `false` | `false` | `logEvent` |
54-
| **Debug log sampling** | Probability that a Lambda invocation will print all the log items regardless of the log level setting | `POWERTOOLS_LOGGER_SAMPLE_RATE` | `0` | `0.0` to `1` | `0.5` | `sampleRateValue` |
49+
| Setting | Description | Environment variable | Default Value | Allowed Values | Example Value | Constructor parameter |
50+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------- | ------------------------------------------------------ | ------------------- | --------------------- |
51+
| **Service name** | Sets the name of service of which the Lambda function is part of, that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` |
52+
| **Logging level** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | `POWERTOOLS_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, `SILENT` | `ERROR` | `logLevel` |
53+
| **Log incoming event** | Whether to log or not the incoming event when using the decorator or middleware | `POWERTOOLS_LOGGER_LOG_EVENT` | `false` | `true`, `false` | `false` | `logEvent` |
54+
| **Debug log sampling** | Probability that a Lambda invocation will print all the log items regardless of the log level setting | `POWERTOOLS_LOGGER_SAMPLE_RATE` | `0` | `0.0` to `1` | `0.5` | `sampleRateValue` |
5555

5656
#### Example using AWS Serverless Application Model (SAM)
5757

@@ -71,7 +71,7 @@ These settings will be used across all logs emitted:
7171
Runtime: nodejs20.x
7272
Environment:
7373
Variables:
74-
LOG_LEVEL: WARN
74+
POWERTOOLS_LOG_LEVEL: WARN
7575
POWERTOOLS_SERVICE_NAME: serverlessAirline
7676
```
7777

@@ -394,9 +394,9 @@ The error will be logged with default key name `error`, but you can also pass yo
394394

395395
### Log levels
396396

397-
The default log level is `INFO` and can be set using the `logLevel` constructor option or by using the `LOG_LEVEL` environment variable.
397+
The default log level is `INFO` and can be set using the `logLevel` constructor option or by using the `POWERTOOLS_LOG_LEVEL` environment variable.
398398

399-
Logger supports the following log levels:
399+
We support the following log levels:
400400

401401
| Level | Numeric value |
402402
| ---------- | ------------- |
@@ -421,11 +421,48 @@ The `SILENT` log level provides a simple and efficient way to suppress all log m
421421

422422
This feature is useful when you want to have your code instrumented to produce logs, but due to some requirement or business decision, you prefer to not emit them.
423423

424-
By setting the log level to `SILENT`, which can be done either through the `logLevel` constructor option or by using the `LOG_LEVEL` environment variable, you can easily suppress all logs as needed.
424+
By setting the log level to `SILENT`, which can be done either through the `logLevel` constructor option or by using the `POWERTOOLS_LOG_LEVEL` environment variable, you can easily suppress all logs as needed.
425425

426426
!!! note
427427
Use the `SILENT` log level with care, as it can make it more challenging to monitor and debug your application. Therefore, we advise using this log level judiciously.
428428

429+
#### AWS Lambda Advanced Logging Controls (ALC)
430+
431+
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, we will always output JSON formatted logging messages.
432+
433+
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.
434+
435+
```mermaid
436+
sequenceDiagram
437+
title Lambda ALC allows WARN logs only
438+
participant Lambda service
439+
participant Lambda function
440+
participant Application Logger
441+
442+
Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN"
443+
Lambda service->>Lambda function: Invoke (event)
444+
Lambda function->>Lambda function: Calls handler
445+
Lambda function->>Application Logger: logger.warn("Something happened")
446+
Lambda function-->>Application Logger: logger.debug("Something happened")
447+
Lambda function-->>Application Logger: logger.info("Something happened")
448+
449+
Lambda service->>Lambda service: DROP INFO and DEBUG logs
450+
451+
Lambda service->>CloudWatch Logs: Ingest error logs
452+
```
453+
454+
**Priority of log level settings in Powertools for AWS Lambda**
455+
456+
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.
457+
458+
We prioritise log level settings in this order:
459+
460+
1. `AWS_LAMBDA_LOG_LEVEL` environment variable
461+
2. Setting the log level in code using the `logLevel` constructor option, or by calling the `logger.setLogLevel()` method
462+
3. `POWERTOOLS_LOG_LEVEL` environment variable
463+
464+
In the event you have set a log level in Powertools to a level that is lower than the ACL setting, we will output a warning log message informing you that your messages will be discarded by Lambda.
465+
429466
### Using multiple Logger instances across your code
430467

431468
The `createChild` method allows you to create a child instance of the Logger, which inherits all of the attributes from its parent. You have the option to override any of the settings and attributes from the parent logger, including [its settings](#utility-settings), any [persistent attributes](#appending-persistent-additional-log-keys-and-values), and [the log formatter](#custom-log-formatter-bring-your-own-formatter). Once a child logger is created, the logger and its parent will act as separate instances of the Logger class, and as such any change to one won't be applied to the other.

Diff for: docs/core/metrics.md

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Install the library in your project:
4747
npm install @aws-lambda-powertools/metrics
4848
```
4949

50+
!!! warning "Caution"
51+
52+
Using the Lambda [Advanced Logging Controls](...docs link) feature requires you to update your version of Powertools for AWS Lambda (TypeScript) to at least v1.15.0 to ensure metrics are reported correctly to Amazon CloudWatch Metrics.
53+
5054
### Usage
5155

5256
The `Metrics` utility must always be instantiated outside of the Lambda handler. In doing this, subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time. In addition, `Metrics` can track cold start and emit the appropriate metrics.

Diff for: docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ Core utilities such as Tracing, Logging, and Metrics will be available across al
307307
| **POWERTOOLS_LOGGER_LOG_EVENT** | Log incoming event | [Logger](core/logger.md) | `false` |
308308
| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logger](core/logger.md) | `0` |
309309
| **POWERTOOLS_DEV** | Increase JSON indentation to ease debugging when running functions locally or in a non-production environment | [Logger](core/logger.md) | `false` |
310-
| **LOG_LEVEL** | Set logging level | [Logger](core/logger.md) | `INFO` |
310+
| **POWERTOOLS_LOG_LEVEL** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | [Logger](core/logger.md) | `INFO` |
311311
| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](utilities/parameters.md) | `5` |
312312
| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Set whether to decrypt or not values retrieved from AWS Systems Manager Parameters Store | [Parameters](utilities/parameters.md) | `false` |
313313
| **POWERTOOLS_IDEMPOTENCY_DISABLED** | Disable the Idempotency logic without changing your code, useful for testing | [Idempotency](utilities/idempotency.md) | `false` |

Diff for: docs/snippets/batch/templates/sam/dynamodb.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Globals:
1010
Tracing: Active
1111
Environment:
1212
Variables:
13-
LOG_LEVEL: INFO
13+
POWERTOOLS_LOG_LEVEL: INFO
1414
POWERTOOLS_SERVICE_NAME: hello
1515

1616
Resources:

Diff for: docs/snippets/batch/templates/sam/sqs.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Globals:
1010
Tracing: Active
1111
Environment:
1212
Variables:
13-
LOG_LEVEL: INFO
13+
POWERTOOLS_LOG_LEVEL: INFO
1414
POWERTOOLS_SERVICE_NAME: hello
1515

1616
Resources:

Diff for: examples/cdk/src/example-stack.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const commonProps: Partial<NodejsFunctionProps> = {
1919
NODE_OPTIONS: '--enable-source-maps', // see https://docs.aws.amazon.com/lambda/latest/dg/typescript-exceptions.html
2020
POWERTOOLS_SERVICE_NAME: 'items-store',
2121
POWERTOOLS_METRICS_NAMESPACE: 'PowertoolsCDKExample',
22-
LOG_LEVEL: 'DEBUG',
22+
POWERTOOLS_LOG_LEVEL: 'DEBUG',
2323
},
2424
bundling: {
2525
externalModules: [

Diff for: examples/sam/template.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Resources:
6464
SAMPLE_TABLE: !Ref SampleTable
6565
POWERTOOLS_SERVICE_NAME: items-store
6666
POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample
67-
LOG_LEVEL: Debug
67+
POWERTOOLS_LOG_LEVEL: debug
6868
Events:
6969
Api:
7070
Type: Api
@@ -104,7 +104,7 @@ Resources:
104104
SAMPLE_TABLE: !Ref SampleTable
105105
POWERTOOLS_SERVICE_NAME: items-store
106106
POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample
107-
LOG_LEVEL: Debug
107+
POWERTOOLS_LOG_LEVEL: debug
108108
Events:
109109
Api:
110110
Type: Api
@@ -134,7 +134,7 @@ Resources:
134134
Variables:
135135
POWERTOOLS_SERVICE_NAME: items-store
136136
POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample
137-
LOG_LEVEL: Debug
137+
POWERTOOLS_LOG_LEVEL: debug
138138
# add ssm:getParameter permission to the function
139139
Policies:
140140
- SSMParameterWithSlashPrefixReadPolicy:
@@ -186,7 +186,7 @@ Resources:
186186
SAMPLE_TABLE: !Ref SampleTable
187187
POWERTOOLS_SERVICE_NAME: items-store
188188
POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample
189-
LOG_LEVEL: Debug
189+
POWERTOOLS_LOG_LEVEL: debug
190190
Events:
191191
Api:
192192
Type: Api

Diff for: packages/logger/src/Logger.ts

+37-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Utility } from '@aws-lambda-powertools/commons';
22
import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
33
import type { Context, Handler } from 'aws-lambda';
44
import merge from 'lodash.merge';
5+
import { format } from 'node:util';
56
import { Console } from 'node:console';
67
import { randomInt } from 'node:crypto';
78
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
@@ -486,9 +487,13 @@ class Logger extends Utility implements LoggerInterface {
486487
/**
487488
* Set the log level for this Logger instance.
488489
*
490+
* If the log level is set using AWS Lambda Advanced Logging Controls, it sets it
491+
* instead of the given log level to avoid data loss.
492+
*
489493
* @param logLevel The log level to set, i.e. `error`, `warn`, `info`, `debug`, etc.
490494
*/
491495
public setLogLevel(logLevel: LogLevel): void {
496+
if (this.awsLogLevelShortCircuit(logLevel)) return;
492497
if (this.isValidLogLevel(logLevel)) {
493498
this.logLevel = this.logLevelThresholds[logLevel];
494499
} else {
@@ -570,6 +575,30 @@ class Logger extends Utility implements LoggerInterface {
570575
merge(this.powertoolsLogData, attributes);
571576
}
572577

578+
private awsLogLevelShortCircuit(selectedLogLevel?: string): boolean {
579+
const awsLogLevel = this.getEnvVarsService().getAwsLogLevel();
580+
if (this.isValidLogLevel(awsLogLevel)) {
581+
this.logLevel = this.logLevelThresholds[awsLogLevel];
582+
583+
if (
584+
this.isValidLogLevel(selectedLogLevel) &&
585+
this.logLevel > this.logLevelThresholds[selectedLogLevel]
586+
) {
587+
this.warn(
588+
format(
589+
`Current log level (%s) does not match AWS Lambda Advanced Logging Controls minimum log level (%s). This can lead to data loss, consider adjusting them.`,
590+
selectedLogLevel,
591+
awsLogLevel
592+
)
593+
);
594+
}
595+
596+
return true;
597+
}
598+
599+
return false;
600+
}
601+
573602
/**
574603
* It processes a particular log item so that it can be printed to stdout:
575604
* - Merges ephemeral log attributes with persistent log attributes (printed for all logs) and additional info;
@@ -845,17 +874,21 @@ class Logger extends Utility implements LoggerInterface {
845874

846875
/**
847876
* Sets the initial Logger log level based on the following order:
848-
* 1. If a log level is passed to the constructor, it sets it.
849-
* 2. If a log level is set via custom config service, it sets it.
850-
* 3. If a log level is set via env variables, it sets it.
877+
* 1. If a log level is set using AWS Lambda Advanced Logging Controls, it sets it.
878+
* 2. If a log level is passed to the constructor, it sets it.
879+
* 3. If a log level is set via custom config service, it sets it.
880+
* 4. If a log level is set via env variables, it sets it.
851881
*
852-
* If none of the above is true, the default log level applies (INFO).
882+
* If none of the above is true, the default log level applies (`INFO`).
853883
*
854884
* @private
855885
* @param {LogLevel} [logLevel] - Log level passed to the constructor
856886
*/
857887
private setInitialLogLevel(logLevel?: LogLevel): void {
858888
const constructorLogLevel = logLevel?.toUpperCase();
889+
890+
if (this.awsLogLevelShortCircuit(constructorLogLevel)) return;
891+
859892
if (this.isValidLogLevel(constructorLogLevel)) {
860893
this.logLevel = this.logLevelThresholds[constructorLogLevel];
861894

Diff for: packages/logger/src/config/EnvironmentVariablesService.ts

+29-3
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,35 @@ class EnvironmentVariablesService
2020
implements ConfigServiceInterface
2121
{
2222
// Reserved environment variables
23+
private awsLogLevelVariable = 'AWS_LAMBDA_LOG_LEVEL';
2324
private awsRegionVariable = 'AWS_REGION';
2425
private currentEnvironmentVariable = 'ENVIRONMENT';
2526
private functionNameVariable = 'AWS_LAMBDA_FUNCTION_NAME';
2627
private functionVersionVariable = 'AWS_LAMBDA_FUNCTION_VERSION';
2728
private logEventVariable = 'POWERTOOLS_LOGGER_LOG_EVENT';
28-
private logLevelVariable = 'LOG_LEVEL';
29+
private logLevelVariable = 'POWERTOOLS_LOG_LEVEL';
30+
private logLevelVariableLegacy = 'LOG_LEVEL';
2931
private memoryLimitInMBVariable = 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE';
3032
private sampleRateValueVariable = 'POWERTOOLS_LOGGER_SAMPLE_RATE';
3133
private tzVariable = 'TZ';
3234

35+
/**
36+
* It returns the value of the `AWS_LAMBDA_LOG_LEVEL` environment variable.
37+
*
38+
* The `AWS_LAMBDA_LOG_LEVEL` environment variable is set by AWS Lambda when configuring
39+
* the function's log level using the Advanced Logging Controls feature. This value always
40+
* takes precedence over other means of configuring the log level.
41+
*
42+
* @note we need to map the `FATAL` log level to `CRITICAL`, see {@link https://docs.aws.amazon.com/lambda/latest/dg/configuration-logging.html#configuration-logging-log-levels AWS Lambda Log Levels}.
43+
*
44+
* @returns {string}
45+
*/
46+
public getAwsLogLevel(): string {
47+
const awsLogLevelVariable = this.get(this.awsLogLevelVariable);
48+
49+
return awsLogLevelVariable === 'FATAL' ? 'CRITICAL' : awsLogLevelVariable;
50+
}
51+
3352
/**
3453
* It returns the value of the AWS_REGION environment variable.
3554
*
@@ -89,12 +108,19 @@ class EnvironmentVariablesService
89108
}
90109

91110
/**
92-
* It returns the value of the LOG_LEVEL environment variable.
111+
* It returns the value of the `POWERTOOLS_LOG_LEVEL, or `LOG_LEVEL` (legacy) environment variables
112+
* when the first one is not set.
113+
*
114+
* @note The `LOG_LEVEL` environment variable is considered legacy and will be removed in a future release.
115+
* @note The `AWS_LAMBDA_LOG_LEVEL` environment variable always takes precedence over the ones above.
93116
*
94117
* @returns {string}
95118
*/
96119
public getLogLevel(): string {
97-
return this.get(this.logLevelVariable);
120+
const logLevelVariable = this.get(this.logLevelVariable);
121+
const logLevelVariableAlias = this.get(this.logLevelVariableLegacy);
122+
123+
return logLevelVariable !== '' ? logLevelVariable : logLevelVariableAlias;
98124
}
99125

100126
/**

0 commit comments

Comments
 (0)