Skip to content

Commit ded5622

Browse files
fix(logger): reverting logger child modification (#4363)
Reverting logging child change
1 parent d0293d0 commit ded5622

File tree

5 files changed

+37
-120
lines changed

5 files changed

+37
-120
lines changed
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# logger.powertools_handler is set with Powertools Logger handler; useful when there are many handlers
22
LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER = "powertools_handler"
3-
# logger.init attribute is set when Logger has been configured
3+
# logger.init attribute is set when Logger has been configured
44
LOGGER_ATTRIBUTE_PRECONFIGURED = "init"
55
LOGGER_ATTRIBUTE_HANDLER = "logger_handler"
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:

docs/core/logger.md

+28-26
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"

tests/functional/test_logger.py

+1-61
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from aws_lambda_powertools import Logger, Tracer, set_package_logger_handler
1919
from aws_lambda_powertools.logging import correlation_paths
20-
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError, OrphanedChildLoggerError
20+
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError
2121
from aws_lambda_powertools.logging.formatter import (
2222
BasePowertoolsFormatter,
2323
LambdaPowertoolsFormatter,
@@ -1176,63 +1176,3 @@ def test_logger_json_unicode(stdout, service_name):
11761176

11771177
assert log["message"] == non_ascii_chars
11781178
assert log[japanese_field] == japanese_string
1179-
1180-
1181-
def test_logger_registered_handler_is_custom_handler(service_name):
1182-
# GIVEN a library or environment pre-setup a logger for us using the same name (see #4277)
1183-
class ForeignHandler(logging.StreamHandler): ...
1184-
1185-
foreign_handler = ForeignHandler()
1186-
logging.getLogger(service_name).addHandler(foreign_handler)
1187-
1188-
# WHEN Logger init with a custom handler
1189-
custom_handler = logging.StreamHandler()
1190-
logger = Logger(service=service_name, logger_handler=custom_handler)
1191-
1192-
# THEN registered handler should always return what we provided
1193-
assert logger.registered_handler is not foreign_handler
1194-
assert logger.registered_handler is custom_handler
1195-
assert logger.logger_handler is custom_handler
1196-
assert logger.handlers == [foreign_handler, custom_handler]
1197-
1198-
1199-
def test_child_logger_registered_handler_is_custom_handler(service_name):
1200-
# GIVEN
1201-
class ForeignHandler(logging.StreamHandler): ...
1202-
1203-
foreign_handler = ForeignHandler()
1204-
logging.getLogger(service_name).addHandler(foreign_handler)
1205-
1206-
custom_handler = logging.StreamHandler()
1207-
custom_handler.name = "CUSTOM HANDLER"
1208-
parent = Logger(service=service_name, logger_handler=custom_handler)
1209-
1210-
# WHEN a child Logger init
1211-
child = Logger(service=service_name, child=True)
1212-
1213-
# THEN child registered handler should always return what we provided in the parent
1214-
assert child.registered_handler is not foreign_handler
1215-
assert child.registered_handler is custom_handler
1216-
assert child.registered_handler is parent.registered_handler
1217-
1218-
1219-
def test_logger_handler_is_created_despite_env_pre_setup(service_name):
1220-
# GIVEN a library or environment pre-setup a logger for us using the same name
1221-
environment_handler = logging.StreamHandler()
1222-
logging.getLogger(service_name).addHandler(environment_handler)
1223-
1224-
# WHEN Logger init without a custom handler
1225-
logger = Logger(service=service_name)
1226-
1227-
# THEN registered handler should be Powertools default handler, not env
1228-
assert logger.registered_handler is not environment_handler
1229-
1230-
1231-
def test_child_logger_append_keys_before_parent(stdout, service_name):
1232-
# GIVEN a child Logger is initialized before its/without parent
1233-
child = Logger(stream=stdout, service=service_name, child=True)
1234-
1235-
# WHEN a child Logger appends a key
1236-
# THEN it will raise an AttributeError
1237-
with pytest.raises(OrphanedChildLoggerError):
1238-
child.append_keys(customer_id="value")

0 commit comments

Comments
 (0)