Coverage for async_durable_execution/_core/logger.py: 99%
56 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 23:43 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 23:43 +0000
1"""Core logging helpers for durable execution contexts."""
3from __future__ import annotations
5import logging
6from typing import TypeVar
8from .context import OperationContext, _current_context
9from .exceptions import ValidationError
11_LoggerT = TypeVar("_LoggerT")
14class DurableContextFilter(logging.Filter):
15 """Add durable execution metadata from the active contextvar to log records."""
17 def filter(self, record: logging.LogRecord) -> bool:
18 context = _current_context.get()
19 if context is None:
20 return True
21 if not hasattr(context, "execution_state"):
22 return True
24 if _is_replaying(context):
25 return False
27 for key, value in build_context_log_extra(context).items():
28 if value is not None and not hasattr(record, key):
29 setattr(record, key, value)
30 return True
33def build_context_log_extra(context: OperationContext) -> dict[str, object]:
34 """Build structured log fields from the active execution context."""
35 extra: dict[str, object] = {}
36 execution_arn = context.durable_execution_arn
37 if execution_arn: 37 ↛ 41line 37 didn't jump to line 41 because the condition on line 37 was always true
38 # `executionArn` is used here while `durableExecutionArn` is used everywhere else because
39 # that's what the Lambda Console expects in log records.
40 extra["executionArn"] = execution_arn
41 parent_id = context.parent_id
42 if parent_id:
43 extra["parentId"] = context.parent_id
44 operation_id = context.operation_id
45 if operation_id:
46 extra["operationId"] = context.operation_id
47 operation_name = context.operation_name
48 if operation_name:
49 extra["operationName"] = context.operation_name
51 callback_id = getattr(context, "callback_id", None)
52 if callback_id:
53 extra["callbackId"] = callback_id
54 attempt = getattr(context, "attempt", None)
55 if attempt is not None:
56 extra["attempt"] = attempt
57 return extra
60def configure_durable_logger(logger: _LoggerT) -> _LoggerT:
61 """Attach DurableContextFilter to a stdlib-compatible logger and handlers."""
62 add_filter = getattr(logger, "addFilter", None)
63 filters = getattr(logger, "filters", ())
64 if not callable(add_filter):
65 return logger
67 if not any(isinstance(item, DurableContextFilter) for item in filters):
68 add_filter(DurableContextFilter())
70 for handler in getattr(logger, "handlers", ()):
71 if not any(isinstance(item, DurableContextFilter) for item in handler.filters):
72 handler.addFilter(DurableContextFilter())
73 return logger
76def _is_replaying(context: OperationContext) -> bool:
77 if context.execution_state is None:
78 raise ValidationError(
79 "The execution state is None",
80 )
81 return bool(context.is_replaying())
84__all__ = [
85 "DurableContextFilter",
86 "build_context_log_extra",
87 "configure_durable_logger",
88]