Skip to content

fix: Isolate the open logger stack per thread with a ContextVar - #19

Merged
xdaniel3 merged 2 commits into
masterfrom
tda/fix/logger-stack-contextvar
Sep 29, 2026
Merged

xdaniel3 merged 2 commits into
masterfrom
tda/fix/logger-stack-contextvar

Conversation

@xdaniel3

@xdaniel3 xdaniel3 commented Sep 17, 2026 •

Copy link
Copy Markdown

SecurityLogger inherited from threading.local, but the stack of open loggers was a plain class attribute. Class attributes are not thread-local -- only the instance dict is -- so every thread in the process pushed and popped onto one shared list.

With concurrent requests this had two effects:

  • a logger was linked to a parent belonging to another request, so the audit log tree mixed unrelated requests together,
  • close() found a foreign logger on top of the stack and raised "Log already finished". In SecuritySession.request that happens on the way out of the context manager, after the HTTP call has already succeeded, so the caller saw an exception for a request that went through and rolled back its transaction. Failed pops also left the stack growing without bound.

The stack now lives in a module level ContextVar, security.logging.common.loggers_stack, holding an immutable tuple, which isolates it per thread and per asyncio task. A tuple is used so that the default value is never mutated in place and contexts cannot share a list by reference. The threading.local base class is dropped because the stack was the only reason for it.

Breaking change: SecurityLogger.loggers is removed. Read the stack with loggers_stack.get() instead, or use get_last_logger() to find a logger by name.

SecurityLogger inherited from threading.local, but the stack of open loggers
was a plain class attribute. Class attributes are not thread-local -- only the
instance __dict__ is -- so every thread in the process pushed and popped onto
one shared list.

With concurrent requests this had two effects:

- a logger was linked to a parent belonging to another request, so the audit
  log tree mixed unrelated requests together,
- close() found a foreign logger on top of the stack and raised
  "Log already finished". In SecuritySession.request that happens on the way
  out of the context manager, after the HTTP call has already succeeded, so
  the caller saw an exception for a request that went through and rolled back
  its transaction. Failed pops also left the stack growing without bound.

The stack now lives in a module level ContextVar holding an immutable tuple,
which isolates it per thread and per asyncio task. A tuple is used so that the
default value is never mutated in place and contexts cannot share a list by
reference. SecurityLogger.loggers keeps working for reading, now returning a
tuple, and the threading.local base class is dropped because the stack was the
only reason for it.
@xdaniel3
xdaniel3 force-pushed the tda/fix/logger-stack-contextvar branch from 14a8358 to c1a6ed1 Compare September 17, 2026 13:12
@coveralls

coveralls commented Sep 17, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 36547094819

Coverage increased (+0.2%) to 92.579%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 2 uncovered changes across 1 file (79 of 81 lines covered, 97.53%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
example/apps/test_security/tests/logger_stack.py 70 68 97.14%
Total (3 files) 81 79 97.53%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 3948
Covered Lines: 3655
Line Coverage: 92.58%
Coverage Strength: 1.85 hits per line

💛 - Coveralls

Comment thread security/logging/common.py Outdated
@xdaniel3
xdaniel3 force-pushed the tda/fix/logger-stack-contextvar branch from e2671b0 to f806005 Compare September 29, 2026 09:03
…_stack ContextVar

The read-only descriptor only existed for backward compatibility. Callers
read the stack directly with loggers_stack.get() instead.
@xdaniel3
xdaniel3 force-pushed the tda/fix/logger-stack-contextvar branch from f806005 to 5555889 Compare September 29, 2026 09:07
@xdaniel3
xdaniel3 merged commit e8d4151 into master Sep 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants