Repository navigation
structguru v1.5.0
A resilience and security release. A retired sink's destructor could deadlock the dispatcher during removal or reconfiguration; a stalled sink could hold flush(), shutdown(), and reconfiguration open forever; and the warnings those failures emit could print the exception's text — including a record's secrets — into diagnostics that bypass redaction by design. The minor version carries two behavior changes to read before you upgrade: adding the same logging.Handler twice now raises ValueError, and sink-failure and fork-restart diagnostics no longer include the exception text. Free-threaded CPython 3.15t joins the 3.14t wheels. No breaking API changes; upgrade in place from any v1.x.
Behavior changes
-
Adding the same
logging.Handlertwice raisesValueErroruntil it has been removed. A secondadd()used to register the handler again and deliver every rendered record to it twice; the first registration's delivery and ownership are now preserved. A handler removed through anyLoggercan be added again, and a failed registration rolls its root-logger reservation back instead of leaving the handler unusable. -
Sink-failure and fork-restart diagnostics never stringify the exception, reporting only its type:
structguru callable sink raised 1 time(s), latest ConnectionError. Diagnostic warnings bypass record redaction, so an exception whose text embedded the record — or a credential — it was writing could put the unredacted value into a warning, and a__str__with side effects ran on the delivery thread and on the fork path. The error is still counted incallable_errorsandstream_errors, and the fork diagnostic still names the type.
Features
- CPython 3.15 support. The abi3 wheel already covers 3.15 unchanged, and free-threaded 3.15t wheels join the 3.14t ones for Linux x86_64, macOS arm64, and Windows x64. The 3.15 source-gate leg returns once PyYAML ships cp315 wheels — it has none for 3.15 and its sdist build crashes the CI runners today — while the free-threaded 3.15t gate already runs on 3.15.
Fixes
Lifecycle
-
Removing or replacing a callable sink releases retired Python callbacks after unlocking the dispatcher.
remove(),configure(),sync(), anddisable()dropped the dispatcher's fork snapshot — and with it the last reference to a retired callback — while holding the registry lock. A callback whose__del__re-enters the dispatcher, as a sink that callsmetrics()oradd()from its destructor does, deadlocked the process: all eight transition/cache combinations hung before the fix and complete cleanly after it, pinned by a subprocess regression test. -
flush(),shutdown(), and reconfiguration no longer block forever when the native writer's sink stops making progress — a full stdout pipe whose reader has stopped, for example. A writer stuck for 10 seconds is abandoned with a warning naming the number of undrained records, matching the stall limit callable sinks already had. -
A panic inside a native sink no longer kills the writer thread silently, which used to leave
flush()waiting forever and producers enqueueing into a queue nobody drained. The interruption is counted insink_errors, the writer closes, and blocked flushers and producers wake up.
Docs
- The documentation is restructured into tutorials, how-to guides, reference, and explanation (Diátaxis), and the site is built in CI and through
make docs. - The Sentry how-to no longer reads as a redaction guarantee: the raw exception passed to
sentry_sdk.capture_exceptionlets the SDK attach unredacted frame locals unless it is initialised withinclude_local_variables=False, and the docs now say so.
Test coverage
- 19 new Python test cases and 4 new Rust tests since v1.4.0 — 977 Python tests and 99 Rust tests in total, with line coverage at 93.8%. The dispatcher fix is pinned by a subprocess test that hangs on the previous build; the diagnostics change by a test whose exception's
__str__records being called and never is.