ADR 0029: Logger Propagation Through Layers¶
Status¶
Accepted
Date¶
2025-11-25
Context¶
The library needs to support optional logging for diagnostic purposes. Users should be able to:
- Configure a logger at the
EnvResolverinstance level via constructor - Set a global default logger via
set_logger()for module-level facade functions - Override the logger on a per-call basis via method parameters
The implementation must be thread-safe. If multiple threads call methods with different logger overrides simultaneously, they should not interfere with each other's logging behavior.
Decision¶
Logger instances will be passed explicitly as parameters through all layers of the resolution stack, rather than mutating instance state.
API Layer:
def resolve_secret(self, uri: str, logger: logging.Logger | None = None) -> str:
effective_logger = logger if logger is not None else self._logger
resolver = self._get_resolver()
return resolver.resolve(uri, logger=effective_logger)
Application Layer (SecretResolver):
def resolve(self, value: str, logger: logging.Logger | None = None) -> str:
# Use logger for diagnostic messages
# Pass logger to provider.resolve() if needed
Provider Layer (SecretProvider):
def resolve(self, parsed_uri: ParsedURI, logger: logging.Logger | None = None) -> str:
# Use logger for diagnostic messages
Note: The SecretProvider protocol is part of the public API, as users can implement custom providers. This signature change is a breaking change for custom provider implementations.
Rationale¶
- Thread Safety: Passing logger as a parameter eliminates shared mutable state, making the implementation inherently thread-safe
- Explicit and Traceable: The logger's path through the system is explicit in method signatures, making it easy to understand and debug
- Simple Design: This approach is straightforward without introducing new abstractions like context objects
- Acceptable Breaking Change: While
SecretProvideris a public protocol, the library is in 0.x version where breaking changes are acceptable. Custom provider implementations will need to add theloggerparameter, but this is a straightforward migration
Implications¶
Positive Implications¶
- Thread-safe logger override behavior
- Clear, explicit control flow for logger propagation
- Easy to test and debug
- No risk of logger state leaking between concurrent calls
- Follows common Python patterns for passing context through call stacks
Concerns¶
- Breaking Change for Custom Providers: Users who have implemented custom
SecretProviderclasses will need to update theirresolve()method signature to include theloggerparameter -
Mitigation: The library is in 0.x version where breaking changes are expected. Migration is straightforward: add
logger: logging.Logger | None = Noneparameter. This will be clearly documented in release notes and migration guide. -
Parameter Passing Overhead: Logger parameter must be passed through each layer
-
Mitigation: The overhead is minimal, and the explicitness improves code clarity
-
Less Flexibility for Future Context: If we need to add more context parameters in the future (e.g., request ID, timeout), we'll have to either add more parameters or refactor to a context object, which would be another breaking change
- Mitigation: Accept this trade-off. The library is in 0.x, so future breaking changes are acceptable if needed. The current simple design is appropriate for present requirements.
Alternatives¶
Threading Local Storage¶
- Description: Use
threading.local()to store the logger override implicitly - Pros: No signature changes required; thread-safe
- Cons: Implicit state management makes debugging difficult; goes against Python best practices; harder to test
- Reason for Rejection: Implicit state is harder to reason about and debug, especially in a library context where users may have complex threading scenarios
Context Object¶
- Description: Introduce a
ResolverContextdataclass containing logger and future context information - Pros: Thread-safe; extensible for future context data; keeps parameter count low; single breaking change if more context is needed later
- Cons: Introduces new abstraction when only logger is currently needed; over-engineering for present requirements
- Reason for Rejection: While this would make future extensions easier, it's premature abstraction. The current requirement is only for logger propagation. However, note that if additional context parameters are needed in the future, this would require another breaking change.
Temporarily Mutate Instance State¶
- Description: Temporarily mutate
self._loggerwithin a try/finally block - Pros: Simple; no signature changes for internal layers or public protocol
- Cons: Not thread-safe; could cause subtle bugs in multi-threaded applications; difficult to reason about
- Reason for Rejection: Thread safety is a fundamental requirement. Secret management libraries are often used in concurrent environments (web servers, async code), so thread safety cannot be compromised.
Future Direction¶
- If additional context information is needed in the future (e.g., request IDs, retry counts, timeout values), consider introducing a context object. This would be another breaking change to the
SecretProviderprotocol, but is acceptable in 0.x versions. - The pattern established here (explicit parameter propagation) should be followed for any similar cross-cutting concerns
- Monitor for any performance issues related to parameter passing, though none are expected given the overhead is negligible
- Document the breaking change clearly in release notes, including migration examples for custom provider implementations
References¶
- Issue #40: Add logging support to envresolve
- ADR-0024: Core Design Principle of 'Fine-Grained Control' - This decision aligns with explicit configuration over implicit behavior