Skip to content

Commit da645ba

Browse files
Merge branch 'develop' into feat/cloudformation-custom-resource
2 parents b6a8b7d + 99edf22 commit da645ba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+861
-746
lines changed

.github/actions/seal/action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ runs:
7979
shell: bash
8080

8181
- name: Upload artifacts
82-
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
82+
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
8383
with:
8484
if-no-files-found: error
8585
name: ${{ steps.export_artifact_name.outputs.artifact_name }}

.github/actions/upload-artifact/action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ runs:
6868
shell: bash
6969

7070
- name: Upload artifacts
71-
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
71+
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
7272
with:
7373
if-no-files-found: ${{ inputs.if-no-files-found }}
7474
name: ${{ inputs.name }}

.github/actions/upload-release-provenance/action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ runs:
4242

4343
- id: download-provenance
4444
name: Download newly generated provenance
45-
uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1
45+
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
4646
with:
4747
name: ${{ inputs.provenance_name }}
4848

.github/workflows/quality_check.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
- name: Complexity baseline
7474
run: make complexity-baseline
7575
- name: Upload coverage to Codecov
76-
uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17 # 4.4.0
76+
uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # 4.4.1
7777
with:
7878
file: ./coverage.xml
7979
env_vars: PYTHON

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ repos:
3434
entry: poetry run cfn-lint
3535
language: system
3636
types: [yaml]
37+
exclude: examples/homepage/install/.*?/serverless\.yml$
3738
files: examples/.*
3839
- repo: https://github.com/rhysd/actionlint
3940
rev: "fd7ba3c382e13dcc0248e425b4cbc3f1185fa3ee" # v1.6.24

CHANGELOG.md

+98-45
Large diffs are not rendered by default.

aws_lambda_powertools/event_handler/api_gateway.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
validation_error_definition,
4444
validation_error_response_definition,
4545
)
46-
from aws_lambda_powertools.event_handler.util import _FrozenDict
46+
from aws_lambda_powertools.event_handler.util import _FrozenDict, extract_origin_header
4747
from aws_lambda_powertools.shared.cookies import Cookie
4848
from aws_lambda_powertools.shared.functions import powertools_dev_is_set
4949
from aws_lambda_powertools.shared.json_encoder import Encoder
@@ -782,7 +782,8 @@ def __init__(
782782

783783
def _add_cors(self, event: ResponseEventT, cors: CORSConfig):
784784
"""Update headers to include the configured Access-Control headers"""
785-
self.response.headers.update(cors.to_dict(event.get_header_value("Origin")))
785+
extracted_origin_header = extract_origin_header(event.resolved_headers_field)
786+
self.response.headers.update(cors.to_dict(extracted_origin_header))
786787

787788
def _add_cache_control(self, cache_control: str):
788789
"""Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used."""
@@ -2129,7 +2130,8 @@ def _not_found(self, method: str) -> ResponseBuilder:
21292130
headers = {}
21302131
if self._cors:
21312132
logger.debug("CORS is enabled, updating headers.")
2132-
headers.update(self._cors.to_dict(self.current_event.get_header_value("Origin")))
2133+
extracted_origin_header = extract_origin_header(self.current_event.resolved_headers_field)
2134+
headers.update(self._cors.to_dict(extracted_origin_header))
21332135

21342136
if method == "OPTIONS":
21352137
logger.debug("Pre-flight request detected. Returning CORS with null response")

aws_lambda_powertools/event_handler/util.py

+29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
from typing import Any, Dict
2+
3+
from aws_lambda_powertools.utilities.data_classes.shared_functions import get_header_value
4+
5+
16
class _FrozenDict(dict):
27
"""
38
A dictionary that can be used as a key in another dictionary.
@@ -11,3 +16,27 @@ class _FrozenDict(dict):
1116

1217
def __hash__(self):
1318
return hash(frozenset(self.keys()))
19+
20+
21+
def extract_origin_header(resolver_headers: Dict[str, Any]):
22+
"""
23+
Extracts the 'origin' or 'Origin' header from the provided resolver headers.
24+
25+
The 'origin' or 'Origin' header can be either a single header or a multi-header.
26+
27+
Args:
28+
resolver_headers (Dict): A dictionary containing the headers.
29+
30+
Returns:
31+
Optional[str]: The value(s) of the origin header or None.
32+
"""
33+
resolved_header = get_header_value(
34+
headers=resolver_headers,
35+
name="origin",
36+
default_value=None,
37+
case_sensitive=False,
38+
)
39+
if isinstance(resolved_header, list):
40+
return resolved_header[0]
41+
42+
return resolved_header
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
class InvalidLoggerSamplingRateError(Exception):
22
pass
3-
4-
5-
class OrphanedChildLoggerError(Exception):
6-
pass

aws_lambda_powertools/logging/logger.py

+7-28
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,10 @@
1919
Optional,
2020
TypeVar,
2121
Union,
22-
cast,
2322
overload,
2423
)
2524

2625
from aws_lambda_powertools.logging.constants import (
27-
LOGGER_ATTRIBUTE_HANDLER,
28-
LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER,
2926
LOGGER_ATTRIBUTE_PRECONFIGURED,
3027
)
3128
from aws_lambda_powertools.shared import constants
@@ -37,7 +34,7 @@
3734
from aws_lambda_powertools.utilities import jmespath_utils
3835

3936
from ..shared.types import AnyCallableT
40-
from .exceptions import InvalidLoggerSamplingRateError, OrphanedChildLoggerError
37+
from .exceptions import InvalidLoggerSamplingRateError
4138
from .filters import SuppressFilter
4239
from .formatter import (
4340
RESERVED_FORMATTER_CUSTOM_KEYS,
@@ -239,14 +236,14 @@ def __init__(
239236
self.child = child
240237
self.logger_formatter = logger_formatter
241238
self._stream = stream or sys.stdout
239+
self.logger_handler = logger_handler or logging.StreamHandler(self._stream)
242240
self.log_uncaught_exceptions = log_uncaught_exceptions
243241

244242
self._is_deduplication_disabled = resolve_truthy_env_var_choice(
245243
env=os.getenv(constants.LOGGER_LOG_DEDUPLICATION_ENV, "false"),
246244
)
247245
self._default_log_keys = {"service": self.service, "sampling_rate": self.sampling_rate}
248246
self._logger = self._get_logger()
249-
self.logger_handler = logger_handler or self._get_handler()
250247

251248
# NOTE: This is primarily to improve UX, so IDEs can autocomplete LambdaPowertoolsFormatter options
252249
# previously, we masked all of them as kwargs thus limiting feature discovery
@@ -285,18 +282,6 @@ def _get_logger(self) -> logging.Logger:
285282

286283
return logging.getLogger(logger_name)
287284

288-
def _get_handler(self) -> logging.Handler:
289-
# is a logger handler already configured?
290-
if getattr(self, LOGGER_ATTRIBUTE_HANDLER, None):
291-
return self.logger_handler
292-
293-
# for children, use parent's handler
294-
if self.child:
295-
return getattr(self._logger.parent, LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER, None) # type: ignore[return-value] # always checked in formatting
296-
297-
# otherwise, create a new stream handler (first time init)
298-
return logging.StreamHandler(self._stream)
299-
300285
def _init_logger(
301286
self,
302287
formatter_options: Optional[Dict] = None,
@@ -335,7 +320,6 @@ def _init_logger(
335320
# std logging will return the same Logger with our attribute if name is reused
336321
logger.debug(f"Marking logger {self.service} as preconfigured")
337322
self._logger.init = True # type: ignore[attr-defined]
338-
self._logger.powertools_handler = self.logger_handler # type: ignore[attr-defined]
339323

340324
def _configure_sampling(self) -> None:
341325
"""Dynamically set log level based on sampling rate
@@ -691,20 +675,15 @@ def removeFilter(self, filter: logging._FilterType) -> None: # noqa: A002 # fil
691675
@property
692676
def registered_handler(self) -> logging.Handler:
693677
"""Convenience property to access the first logger handler"""
694-
return self._get_handler()
678+
# We ignore mypy here because self.child encodes whether or not self._logger.parent is
679+
# None, mypy can't see this from context but we can
680+
handlers = self._logger.parent.handlers if self.child else self._logger.handlers # type: ignore[union-attr]
681+
return handlers[0]
695682

696683
@property
697684
def registered_formatter(self) -> BasePowertoolsFormatter:
698685
"""Convenience property to access the first logger formatter"""
699-
handler = self.registered_handler
700-
if handler is None:
701-
raise OrphanedChildLoggerError(
702-
"Orphan child loggers cannot append nor remove keys until a parent is initialized first. "
703-
"To solve this issue, you can A) make sure a parent logger is initialized first, or B) move append/remove keys operations to a later stage." # noqa: E501
704-
"Reference: https://docs.powertools.aws.dev/lambda/python/latest/core/logger/#reusing-logger-across-your-code",
705-
)
706-
707-
return cast(BasePowertoolsFormatter, handler.formatter)
686+
return self.registered_handler.formatter # type: ignore[return-value]
708687

709688
@property
710689
def log_level(self) -> int:

aws_lambda_powertools/logging/utils.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@
99
def copy_config_to_registered_loggers(
1010
source_logger: Logger,
1111
log_level: Optional[Union[int, str]] = None,
12+
ignore_log_level=False,
1213
exclude: Optional[Set[str]] = None,
1314
include: Optional[Set[str]] = None,
1415
) -> None:
1516
"""Copies source Logger level and handler to all registered loggers for consistent formatting.
1617
1718
Parameters
1819
----------
20+
ignore_log_level
1921
source_logger : Logger
2022
Powertools for AWS Lambda (Python) Logger to copy configuration from
2123
log_level : Union[int, str], optional
2224
Logging level to set to registered loggers, by default uses source_logger logging level
25+
ignore_log_level: bool
26+
Whether to not touch log levels for discovered loggers. log_level param is disregarded when this is set.
2327
include : Optional[Set[str]], optional
2428
List of logger names to include, by default all registered loggers are included
2529
exclude : Optional[Set[str]], optional
@@ -54,7 +58,7 @@ def copy_config_to_registered_loggers(
5458

5559
registered_loggers = _find_registered_loggers(source_logger, loggers, filter_func)
5660
for logger in registered_loggers:
57-
_configure_logger(source_logger, logger, level)
61+
_configure_logger(source_logger=source_logger, logger=logger, level=level, ignore_log_level=ignore_log_level)
5862

5963

6064
def _include_registered_loggers_filter(loggers: Set[str]):
@@ -78,13 +82,21 @@ def _find_registered_loggers(
7882
return root_loggers
7983

8084

81-
def _configure_logger(source_logger: Logger, logger: logging.Logger, level: Union[int, str]) -> None:
85+
def _configure_logger(
86+
source_logger: Logger,
87+
logger: logging.Logger,
88+
level: Union[int, str],
89+
ignore_log_level: bool = False,
90+
) -> None:
91+
# customers may not want to copy the same log level from Logger to discovered loggers
92+
if not ignore_log_level:
93+
logger.setLevel(level)
94+
source_logger.debug(f"Logger {logger} reconfigured to use logging level {level}")
95+
8296
logger.handlers = []
83-
logger.setLevel(level)
8497
logger.propagate = False # ensure we don't propagate logs to existing loggers, #1073
8598
source_logger.append_keys(name="%(name)s") # include logger name, see #1267
8699

87-
source_logger.debug(f"Logger {logger} reconfigured to use logging level {level}")
88100
for source_handler in source_logger.handlers:
89101
logger.addHandler(source_handler)
90102
source_logger.debug(f"Logger {logger} reconfigured to use {source_handler}")
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Exposes version constant to avoid circular dependencies."""
22

3-
VERSION = "2.37.0"
3+
VERSION = "2.38.1"

docs/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# v9.1.18
2-
FROM squidfunk/mkdocs-material@sha256:48d1914439e935734b9b0886c9d82da37d804d8f7a42cf2aedf090b054de2928
2+
FROM squidfunk/mkdocs-material@sha256:5358893a04dc6ed0e267ef1c0c06abc5d6b00d13dd0fee703c978ef98d56fd53
33
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
44
COPY requirements.txt /tmp/
55
RUN pip install --require-hashes -r /tmp/requirements.txt

docs/core/logger.md

+32-29
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@ Notice in the CloudWatch Logs output how `payment_id` appears as expected when l
496496
```json hl_lines="12"
497497
--8<-- "examples/logger/src/logger_reuse_output.json"
498498
```
499+
???+ note "Note: About Child Loggers"
500+
Coming from standard library, you might be used to use `logging.getLogger(__name__)`. This will create a new instance of a Logger with a different name.
501+
502+
In Powertools, you can have the same effect by using `child=True` parameter: `Logger(child=True)`. This creates a new Logger instance named after `service.<module>`. All state changes will be propagated bi-directionally between Child and Parent.
503+
504+
For that reason, there could be side effects depending on the order the Child Logger is instantiated, because Child Loggers don't have a handler.
505+
506+
For example, if you instantiated a Child Logger and immediately used `logger.append_keys/remove_keys/set_correlation_id` to update logging state, this might fail if the Parent Logger wasn't instantiated.
507+
508+
In this scenario, you can either ensure any calls manipulating state are only called when a Parent Logger is instantiated (example above), or refrain from using `child=True` parameter altogether.
499509

500510
### Sampling debug logs
501511

@@ -571,56 +581,48 @@ You can use import and use them as any other Logger formatter via `logger_format
571581

572582
### Migrating from other Loggers
573583

574-
If you're migrating from other Loggers, there are few key points to be aware of: [Service parameter](#the-service-parameter), [Child Loggers](#child-loggers), [Overriding Log records](#overriding-log-records), and [Logging exceptions](#logging-exceptions).
584+
If you're migrating from other Loggers, there are few key points to be aware of: [Service parameter](#the-service-parameter), [Inheriting Loggers](#inheriting-loggers), [Overriding Log records](#overriding-log-records), and [Logging exceptions](#logging-exceptions).
575585

576586
#### The service parameter
577587

578588
Service is what defines the Logger name, including what the Lambda function is responsible for, or part of (e.g payment service).
579589

580590
For Logger, the `service` is the logging key customers can use to search log operations for one or more functions - For example, **search for all errors, or messages like X, where service is payment**.
581591

582-
#### Child Loggers
592+
#### Inheriting Loggers
583593

584-
<center>
585-
```mermaid
586-
stateDiagram-v2
587-
direction LR
588-
Parent: Logger()
589-
Child: Logger(child=True)
590-
Parent --> Child: bi-directional updates
591-
Note right of Child
592-
Both have the same service
593-
end note
594-
```
595-
</center>
594+
??? tip "Tip: Prefer [Logger Reuse feature](#reusing-logger-across-your-code) over inheritance unless strictly necessary, [see caveats.](#reusing-logger-across-your-code)"
596595

597-
For inheritance, Logger uses `child` parameter to ensure we don't compete with its parents config. We name child Loggers following Python's convention: _`{service}`.`{filename}`_.
596+
> Python Logging hierarchy happens via the dot notation: `service`, `service.child`, `service.child_2`
597+
For inheritance, Logger uses a `child=True` parameter along with `service` being the same value across Loggers.
598598

599-
Changes are bidirectional between parents and loggers. That is, appending a key in a child or parent will ensure both have them. This means, having the same `service` name is important when instantiating them.
599+
For child Loggers, we introspect the name of your module where `Logger(child=True, service="name")` is called, and we name your Logger as **{service}.{filename}**.
600600

601-
=== "logging_inheritance_good.py"
601+
???+ danger
602+
A common issue when migrating from other Loggers is that `service` might be defined in the parent Logger (no child param), and not defined in the child Logger:
603+
604+
=== "logging_inheritance_bad.py"
602605

603606
```python hl_lines="1 9"
604-
--8<-- "examples/logger/src/logging_inheritance_good.py"
607+
--8<-- "examples/logger/src/logging_inheritance_bad.py"
605608
```
606609

607610
=== "logging_inheritance_module.py"
608-
609611
```python hl_lines="1 9"
610612
--8<-- "examples/logger/src/logging_inheritance_module.py"
611613
```
612614

613-
There are two important side effects when using child loggers:
615+
In this case, Logger will register a Logger named `payment`, and a Logger named `service_undefined`. The latter isn't inheriting from the parent, and will have no handler, resulting in no message being logged to standard output.
614616

615-
1. **Service name mismatch**. Logging messages will be dropped as child loggers don't have logging handlers.
616-
* Solution: use `POWERTOOLS_SERVICE_NAME` env var. Alternatively, use the same service explicit value.
617-
2. **Changing state before a parent instantiate**. Using `logger.append_keys` or `logger.remove_keys` without a parent Logger will lead to `OrphanedChildLoggerError` exception.
618-
* Solution: always initialize parent Loggers first. Alternatively, move calls to `append_keys`/`remove_keys` from the child at a later stage.
617+
???+ tip
618+
This can be fixed by either ensuring both has the `service` value as `payment`, or simply use the environment variable `POWERTOOLS_SERVICE_NAME` to ensure service value will be the same across all Loggers when not explicitly set.
619619

620-
=== "logging_inheritance_bad.py"
620+
Do this instead:
621+
622+
=== "logging_inheritance_good.py"
621623

622624
```python hl_lines="1 9"
623-
--8<-- "examples/logger/src/logging_inheritance_bad.py"
625+
--8<-- "examples/logger/src/logging_inheritance_good.py"
624626
```
625627

626628
=== "logging_inheritance_module.py"
@@ -811,10 +813,11 @@ for the given name and level to the logging module. By default, this logs all bo
811813

812814
You can copy the Logger setup to all or sub-sets of registered external loggers. Use the `copy_config_to_registered_logger` method to do this.
813815

814-
???+ tip
815-
To help differentiate between loggers, we include the standard logger `name` attribute for all loggers we copied configuration to.
816+
!!! tip "We include the logger `name` attribute for all loggers we copied configuration to help you differentiate them."
817+
818+
By default all registered loggers will be modified. You can change this behavior by providing `include` and `exclude` attributes.
816819

817-
By default all registered loggers will be modified. You can change this behavior by providing `include` and `exclude` attributes. You can also provide optional `log_level` attribute external loggers will be configured with.
820+
You can also provide optional `log_level` attribute external top-level loggers will be configured with, by default it'll use the source logger log level. You can opt-out by using `ignore_log_level=True` parameter.
818821

819822
```python hl_lines="10" title="Cloning Logger config to all other registered standard loggers"
820823
---8<-- "examples/logger/src/cloning_logger_config.py"

0 commit comments

Comments
 (0)