Skip to content

Commit 9fc5169

Browse files
feat(logger): Adding support to new env variables (aws-powertools#3348)
1 parent d6b3a42 commit 9fc5169

File tree

19 files changed

+290
-39
lines changed

19 files changed

+290
-39
lines changed

Diff for: aws_lambda_powertools/logging/logger.py

+86-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import random
88
import sys
9+
import warnings
910
from typing import (
1011
IO,
1112
TYPE_CHECKING,
@@ -24,13 +25,13 @@
2425
import jmespath
2526

2627
from aws_lambda_powertools.logging import compat
27-
28-
from ..shared import constants
29-
from ..shared.functions import (
28+
from aws_lambda_powertools.shared import constants
29+
from aws_lambda_powertools.shared.functions import (
3030
extract_event_from_common_models,
3131
resolve_env_var_choice,
3232
resolve_truthy_env_var_choice,
3333
)
34+
3435
from ..shared.types import AnyCallableT
3536
from .exceptions import InvalidLoggerSamplingRateError
3637
from .filters import SuppressFilter
@@ -76,7 +77,7 @@ class Logger:
7677
---------------------
7778
POWERTOOLS_SERVICE_NAME : str
7879
service name
79-
LOG_LEVEL: str
80+
POWERTOOLS_LOG_LEVEL: str
8081
logging level (e.g. INFO, DEBUG)
8182
POWERTOOLS_LOGGER_SAMPLE_RATE: float
8283
sampling rate ranging from 0 to 1, 1 being 100% sampling
@@ -297,7 +298,7 @@ def _init_logger(
297298
if self.child or is_logger_preconfigured:
298299
return
299300

300-
self.setLevel(self._determine_log_level(log_level))
301+
self.setLevel(log_level)
301302
self._configure_sampling()
302303
self.addHandler(self.logger_handler)
303304
self.structure_logs(formatter_options=formatter_options, **kwargs)
@@ -676,8 +677,8 @@ def get_correlation_id(self) -> Optional[str]:
676677
return self.registered_formatter.log_format.get("correlation_id")
677678
return None
678679

679-
def setLevel(self, level: Union[str, int]) -> None:
680-
return self._logger.setLevel(level)
680+
def setLevel(self, level: Union[str, int, None]) -> None:
681+
return self._logger.setLevel(self._determine_log_level(level))
681682

682683
def addHandler(self, handler: logging.Handler) -> None:
683684
return self._logger.addHandler(handler)
@@ -714,17 +715,88 @@ def handlers(self) -> List[logging.Handler]:
714715
"""
715716
return self._logger.handlers
716717

717-
@staticmethod
718-
def _determine_log_level(level: Union[str, int, None]) -> Union[str, int]:
719-
"""Returns preferred log level set by the customer in upper case"""
718+
def _get_aws_lambda_log_level(self) -> Optional[str]:
719+
"""
720+
Retrieve the log level for AWS Lambda from the Advanced Logging Controls feature.
721+
Returns:
722+
Optional[str]: The corresponding logging level.
723+
"""
724+
725+
return constants.LAMBDA_ADVANCED_LOGGING_LEVELS.get(os.getenv(constants.LAMBDA_LOG_LEVEL_ENV))
726+
727+
def _get_powertools_log_level(self, level: Union[str, int, None]) -> Optional[str]:
728+
"""Retrieve the log level for Powertools from the environment variable or level parameter.
729+
If log level is an integer, we convert to its respective string level `logging.getLevelName()`.
730+
If no log level is provided, we check env vars for the log level: POWERTOOLS_LOG_LEVEL_ENV and POWERTOOLS_LOG_LEVEL_LEGACY_ENV.
731+
Parameters:
732+
-----------
733+
level : Union[str, int, None]
734+
The specified log level as a string, integer, or None.
735+
Environment variables
736+
---------------------
737+
POWERTOOLS_LOG_LEVEL : str
738+
log level (e.g: INFO, DEBUG, WARNING, ERROR, CRITICAL)
739+
LOG_LEVEL (Legacy) : str
740+
log level (e.g: INFO, DEBUG, WARNING, ERROR, CRITICAL)
741+
Returns:
742+
--------
743+
Optional[str]:
744+
The corresponding logging level. Returns None if the log level is not explicitly specified.
745+
""" # noqa E501
746+
747+
# Extract log level from Powertools Logger env vars
748+
log_level_env = os.getenv(constants.POWERTOOLS_LOG_LEVEL_ENV) or os.getenv(
749+
constants.POWERTOOLS_LOG_LEVEL_LEGACY_ENV,
750+
)
751+
# If level is an int (logging.INFO), return its respective string ("INFO")
720752
if isinstance(level, int):
721-
return level
753+
return logging.getLevelName(level)
754+
755+
return level or log_level_env
756+
757+
def _determine_log_level(self, level: Union[str, int, None]) -> Union[str, int]:
758+
"""Determine the effective log level considering Lambda and Powertools preferences.
759+
It emits an UserWarning if Lambda ALC log level is lower than Logger log level.
760+
Parameters:
761+
-----------
762+
level: Union[str, int, None]
763+
The specified log level as a string, integer, or None.
764+
Returns:
765+
----------
766+
Union[str, int]: The effective logging level.
767+
"""
722768

723-
log_level: Optional[str] = level or os.getenv("LOG_LEVEL")
724-
if log_level is None:
769+
# This function consider the following order of precedence:
770+
# 1 - If a log level is set using AWS Lambda Advanced Logging Controls, it sets it.
771+
# 2 - If a log level is passed to the constructor, it sets it
772+
# 3 - If a log level is set via setLevel, it sets it.
773+
# 4 - If a log level is set via Powertools env variables, it sets it.
774+
# 5 - If none of the above is true, the default log level applies INFO.
775+
776+
lambda_log_level = self._get_aws_lambda_log_level()
777+
powertools_log_level = self._get_powertools_log_level(level)
778+
779+
if powertools_log_level and lambda_log_level:
780+
# If Powertools log level is set and higher than AWS Lambda Advanced Logging Controls, emit a warning
781+
if logging.getLevelName(lambda_log_level) > logging.getLevelName(powertools_log_level):
782+
warnings.warn(
783+
f"Current log level ({powertools_log_level}) does not match AWS Lambda Advanced Logging Controls "
784+
f"minimum log level ({lambda_log_level}). This can lead to data loss, consider adjusting them.",
785+
UserWarning,
786+
stacklevel=2,
787+
)
788+
789+
# AWS Lambda Advanced Logging Controls takes precedence over Powertools log level and we use this
790+
if lambda_log_level:
791+
return lambda_log_level
792+
793+
# Check if Powertools log level is None, which means it's not set
794+
# We assume INFO as the default log level
795+
if powertools_log_level is None:
725796
return logging.INFO
726797

727-
return log_level.upper()
798+
# Powertools log level is set, we use this
799+
return powertools_log_level.upper()
728800

729801

730802
def set_package_logger(

Diff for: aws_lambda_powertools/shared/constants.py

+15
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,18 @@
4242

4343
POWERTOOLS_DEV_ENV: str = "POWERTOOLS_DEV"
4444
POWERTOOLS_DEBUG_ENV: str = "POWERTOOLS_DEBUG"
45+
POWERTOOLS_LOG_LEVEL_ENV: str = "POWERTOOLS_LOG_LEVEL"
46+
POWERTOOLS_LOG_LEVEL_LEGACY_ENV: str = "LOG_LEVEL"
47+
LAMBDA_LOG_LEVEL_ENV: str = "AWS_LAMBDA_LOG_LEVEL"
48+
49+
# Mapping of Lambda log levels to Python logging levels
50+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-logging.html#configuration-logging-log-levels
51+
LAMBDA_ADVANCED_LOGGING_LEVELS = {
52+
None: None,
53+
"TRACE": "NOTSET",
54+
"DEBUG": "DEBUG",
55+
"INFO": "INFO",
56+
"WARN": "WARNING",
57+
"ERROR": "ERROR",
58+
"FATAL": "CRITICAL",
59+
}

Diff for: benchmark/template.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Globals:
1212
POWERTOOLS_SERVICE_NAME: benchmark
1313
POWERTOOLS_METRICS_NAMESPACE: LambdaPowertools
1414
POWERTOOLS_LOGGER_LOG_EVENT: "true"
15-
LOG_LEVEL: INFO
15+
POWERTOOLS_LOG_LEVEL: INFO
1616

1717
Resources:
1818
InstrumentedFunction:
@@ -45,4 +45,4 @@ Outputs:
4545
InstrumentedLogGroup:
4646
Value: !Ref InstrumentedLogGroup
4747
ReferenceLogGroup:
48-
Value: !Ref ReferenceLogGroup
48+
Value: !Ref ReferenceLogGroup

Diff for: docs/core/logger.md

+68-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ Logger provides an opinionated logger with output structured as JSON.
1919

2020
Logger requires two settings:
2121

22-
| Setting | Description | Environment variable | Constructor parameter |
23-
| ----------------- | ------------------------------------------------------------------- | ------------------------- | --------------------- |
24-
| **Logging level** | Sets how verbose Logger should be (INFO, by default) | `LOG_LEVEL` | `level` |
25-
| **Service** | Sets **service** key that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service` |
22+
| Setting | Description | Environment variable | Constructor parameter |
23+
| ----------------- | ------------------------------------------------------------------- | --------------------------------------------------- | --------------------- |
24+
| **Logging level** | Sets how verbose Logger should be (INFO, by default) | `POWERTOOLS_LOG_LEVEL` | `level` |
25+
| **Service** | Sets **service** key that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service` |
2626

2727
There are some [other environment variables](#environment-variables) which can be set to modify Logger's settings at a global scope.
2828

@@ -274,6 +274,70 @@ Logger is commonly initialized in the global scope. Due to [Lambda Execution Con
274274
--8<-- "examples/logger/src/clear_state_event_two.json"
275275
```
276276

277+
### Log levels
278+
279+
The default log level is `INFO`. It can be set using the `level` constructor option, `setLevel()` method or by using the `POWERTOOLS_LOG_LEVEL` environment variable.
280+
281+
We support the following log levels:
282+
283+
| Level | Numeric value | Standard logging
284+
| ---------- | ------------- | -----------------
285+
| `DEBUG` | 10 | `logging.DEBUG`
286+
| `INFO` | 20 | `logging.INFO`
287+
| `WARNING` | 30 | `logging.WARNING`
288+
| `ERROR` | 40 | `logging.ERROR`
289+
| `CRITICAL` | 50 | `logging.CRITICAL`
290+
291+
If you want to access the numeric value of the current log level, you can use the `log_level` property. For example, if the current log level is `INFO`, `logger.log_level` property will return `10`.
292+
293+
=== "setting_log_level_constructor.py"
294+
295+
```python hl_lines="3"
296+
--8<-- "examples/logger/src/setting_log_level_via_constructor.py"
297+
```
298+
299+
=== "setting_log_level_programmatically.py"
300+
301+
```python hl_lines="6 9 12"
302+
--8<-- "examples/logger/src/setting_log_level_programmatically.py"
303+
```
304+
305+
#### AWS Lambda Advanced Logging Controls (ALC)
306+
307+
<!-- markdownlint-disable MD013 -->
308+
With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, 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.
309+
<!-- markdownlint-enable MD013 -->
310+
311+
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.
312+
313+
```mermaid
314+
sequenceDiagram
315+
title Lambda ALC allows WARN logs only
316+
participant Lambda service
317+
participant Lambda function
318+
participant Application Logger
319+
Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN"
320+
Lambda service->>Lambda function: Invoke (event)
321+
Lambda function->>Lambda function: Calls handler
322+
Lambda function->>Application Logger: logger.warn("Something happened")
323+
Lambda function-->>Application Logger: logger.debug("Something happened")
324+
Lambda function-->>Application Logger: logger.info("Something happened")
325+
Lambda service->>Lambda service: DROP INFO and DEBUG logs
326+
Lambda service->>CloudWatch Logs: Ingest error logs
327+
```
328+
329+
**Priority of log level settings in Powertools for AWS Lambda**
330+
331+
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](...docs link) for more details.
332+
333+
We prioritise log level settings in this order:
334+
335+
1. `AWS_LAMBDA_LOG_LEVEL` environment variable
336+
2. Setting the log level in code using the `level` constructor option, or by calling the `logger.setLevel()` method
337+
3. `POWERTOOLS_LOG_LEVEL` environment variable
338+
339+
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.
340+
277341
### Logging exceptions
278342

279343
Use `logger.exception` method to log contextual information about exceptions. Logger will include `exception_name` and `exception` keys to aid troubleshooting and error enumeration.

Diff for: docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai
722722
| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](./utilities/parameters.md#adjusting-cache-ttl){target="_blank"} | `5` |
723723
| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Sets whether to decrypt or not values retrieved from AWS SSM Parameters Store | [Parameters](./utilities/parameters.md#ssmprovider){target="_blank"} | `false` |
724724
| **POWERTOOLS_DEV** | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#optimizing-for-non-production-environments) | `false` |
725-
| **LOG_LEVEL** | Sets logging level | [Logging](./core/logger.md){target="_blank"} | `INFO` |
725+
| **POWERTOOLS_LOG_LEVEL** | Sets logging level | [Logging](./core/logger.md){target="_blank"} | `INFO` |
726726

727727
### Optimizing for non-production environments
728728

Diff for: docs/tutorial/index.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ The first option could be to use the standard Python Logger, and use a specializ
392392
formatter = jsonlogger.JsonFormatter(fmt="%(asctime)s %(levelname)s %(name)s %(message)s")
393393
logHandler.setFormatter(formatter)
394394
logger.addHandler(logHandler)
395-
logger.setLevel(os.getenv("LOG_LEVEL", "INFO"))
395+
logger.setLevel(os.getenv("POWERTOOLS_LOG_LEVEL", "INFO"))
396396

397397
app = APIGatewayRestResolver()
398398

@@ -424,7 +424,7 @@ With just a few lines our logs will now output to `JSON` format. We've taken the
424424

425425
* **L7**: Creates an application logger named `APP`.
426426
* **L8-11**: Configures handler and formatter.
427-
* **L12**: Sets the logging level set in the `LOG_LEVEL` environment variable, or `INFO` as a sentinel value.
427+
* **L12**: Sets the logging level set in the `POWERTOOLS_LOG_LEVEL` environment variable, or `INFO` as a sentinel value.
428428

429429
After that, we use this logger in our application code to record the required information. We see logs structured as follows:
430430

@@ -485,7 +485,7 @@ def lambda_handler(event, context):
485485

486486
Let's break this down:
487487

488-
* **L5**: We add Powertools for AWS Lambda (Python) Logger; the boilerplate is now done for you. By default, we set `INFO` as the logging level if `LOG_LEVEL` env var isn't set.
488+
* **L5**: We add Powertools for AWS Lambda (Python) Logger; the boilerplate is now done for you. By default, we set `INFO` as the logging level if `POWERTOOLS_LOG_LEVEL` env var isn't set.
489489
* **L22**: We use `logger.inject_lambda_context` decorator to inject key information from Lambda context into every log.
490490
* **L22**: We also instruct Logger to use the incoming API Gateway Request ID as a [correlation id](../core/logger.md##set_correlation_id-method){target="_blank"} automatically.
491491
* **L22**: Since we're in dev, we also use `log_event=True` to automatically log each incoming request for debugging. This can be also set via [environment variables](./index.md#environment-variables){target="_blank"}.

Diff for: examples/batch_processing/sam/dynamodb_batch_processing.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/batch_processing/sam/kinesis_batch_processing.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/batch_processing/sam/sqs_batch_processing.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/event_handler_graphql/sam/template.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Globals:
1010
Environment:
1111
Variables:
1212
# Powertools for AWS Lambda (Python) env vars: https://docs.powertools.aws.dev/lambda/python/latest/#environment-variables
13-
LOG_LEVEL: INFO
13+
POWERTOOLS_LOG_LEVEL: INFO
1414
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
1515
POWERTOOLS_LOGGER_LOG_EVENT: true
1616
POWERTOOLS_SERVICE_NAME: example

Diff for: examples/event_handler_lambda_function_url/sam/template.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Globals:
99
Tracing: Active
1010
Environment:
1111
Variables:
12-
LOG_LEVEL: INFO
12+
POWERTOOLS_LOG_LEVEL: INFO
1313
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
1414
POWERTOOLS_LOGGER_LOG_EVENT: true
1515
POWERTOOLS_SERVICE_NAME: example

Diff for: examples/event_handler_rest/sam/micro_function_template.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Resources:
3737
MemorySize: 128 # Each Lambda Function can have it's own memory configuration
3838
Environment:
3939
Variables:
40-
LOG_LEVEL: INFO
40+
POWERTOOLS_LOG_LEVEL: INFO
4141
Tags:
4242
LambdaPowertools: python
4343

@@ -60,4 +60,4 @@ Resources:
6060
MemorySize: 128 # Each Lambda Function can have it's own memory configuration
6161
Environment:
6262
Variables:
63-
LOG_LEVEL: INFO
63+
POWERTOOLS_LOG_LEVEL: INFO

Diff for: examples/event_handler_rest/sam/template.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Globals:
1717
Tracing: Active
1818
Environment:
1919
Variables:
20-
LOG_LEVEL: INFO
20+
POWERTOOLS_LOG_LEVEL: INFO
2121
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
2222
POWERTOOLS_LOGGER_LOG_EVENT: true
2323
POWERTOOLS_SERVICE_NAME: example

Diff for: examples/logger/sam/template.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Globals:
1010
Environment:
1111
Variables:
1212
POWERTOOLS_SERVICE_NAME: payment
13-
LOG_LEVEL: INFO
13+
POWERTOOLS_LOG_LEVEL: INFO
1414
Layers:
1515
# Find the latest Layer version in the official documentation
1616
# https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools import Logger
2+
3+
logger = Logger()
4+
5+
# print default log level
6+
print(logger.log_level) # returns 20 (INFO)
7+
8+
# Setting programmatic log level
9+
logger.setLevel("DEBUG")
10+
11+
# print new log level
12+
print(logger.log_level) # returns 10 (DEBUG)

0 commit comments

Comments
 (0)