Repository navigation
v1.2.1
structguru v1.2.1
A correctness release. Four defects are fixed: stack traces attached with stack_info= were written to the output without redaction, callable-sink lifecycle calls could deadlock or return before a sink callback had finished, the Celery hook let a parent task's identity overwrite the running task's, and the Sentry processor forwarded the raw exception object on paths that bypass redaction. The Rust workspace now requires pyo3 0.29.2, and the development toolchain is upgraded, including a mkdocs-material advisory fix for the documentation site. No breaking changes; upgrade in place from any v1.x.
Fixes
-
stack_infotext is redacted in both output formats. The renderer appliedsensitive_keysandsensitive_patternsto the message and to every structured field, but thestackvalue produced bystack_info=was written verbatim to JSON and console output. Anything sensitive that a formatted frame carried left the process unredacted.configure(sensitive_patterns=[r"(token=)\w+"], pattern_replacement="$1[MASKED]") logger.info("trace", stack_info="frame\ntoken=hunter2") # before: ... "stack": "frame\ntoken=hunter2", "message": "trace"} # after: ... "stack": "frame\ntoken=[MASKED]", "message": "trace"}
Pattern redaction now runs over the stack text exactly as it does over the message, and listing
stackinsensitive_keysreplaces the whole value with[REDACTED]. Console output redacts the appended stack the same way. -
Callable-sink lifecycle operations neither deadlock nor return early. The callable dispatcher stopped a queue generation with a stop token under a re-entrant transition lock, which produced four related defects. A sink callback that called
shutdown()ordisable()while another thread was insideconfigure(),logger.remove(),stop(), orshutdown()deadlocked both threads: the outer call held the lock while waiting for the queue to drain, and the callback needed that lock to finish its record. Aflush()ordisable()racing a callback that had just disabled dispatch returned immediately, because the retired generation was no longer referenced anywhere.logger.remove(token)returned while a record captured in a generation retired by an earlierconfigure()was still being delivered to the removed sink. And closing a generation twice left a stop token behind that made the nextflush()hang.Generations are now retired rather than stop-tokened: the worker exits once its generation no longer accepts producers and the queue is empty, retired generations stay tracked until they are actually drained, all waiting happens outside the state lock, and a callback never waits on its own worker thread. Every lifecycle operation still drains every record accepted before it began.
-
A Celery task launched from inside another task keeps its own identity.
setup_celery_logging()bound the running task'stask_idandtask_namefirst and then applied thestructguru_contextheader. When the producer was itself a task, that header carried the parent'stask_idandtask_name, so every record the child logged was attributed to the parent.# parent task (task_id="parent-id") calls child.delay(); the header carries the parent's context # before: {'request_id': 'req-999', 'task_id': 'parent-id', 'task_name': 'parent-task'} # after: {'request_id': 'req-999', 'task_id': 'child-id', 'task_name': 'child-task'}
The running task's identity is now bound after the propagated context, so correlation fields such as
request_idstill flow through while the task fields always describe the task that is actually executing. -
SentryProcessornever forwards the raw exception object outsidecapture_exception. The event dictionary'sexc_infovalue — the live exception — was copied into breadcrumbdata, into thestructlog_eventextra, and, if listed intag_keys, into a tag. The Sentry SDK serializes arbitrary objects, so an exception message containing a secret reached Sentry through those paths even though the renderedexceptionfield had been redacted.exc_infois now stripped from all three; the raw exception is reserved forcapture_exception, the one path meant to receive it.
Tooling
-
pyo3 0.29.2 is the workspace minimum. The lockfile already resolved to it; the floor now says so.
-
mkdocs-material 9.7.7 closes GHSA-xvg9-69gf-fjrf, a DOM XSS in the documentation site's search suggestions via the query parameter. This affects only the built documentation site, never the package.
-
The development toolchain is upgraded — ruff 0.16, mypy 2.3, pytest-benchmark 5.3 among others — and the dependency-group floors in
pyproject.tomlnow match the locked versions. Ruff 0.16 formats Python code blocks inside Markdown, so the README and docs examples were reformatted to match.
Test coverage
- 18 new Python test cases and 1 new Rust test pin the fixed behavior: stack redaction in both formats, by key and by pattern, on the linear and backtracking engines; the dispatcher's retired-generation drain, the callback-disables-dispatch race, idempotent close, and — in a bounded subprocess — the callback-shutdown deadlock against each lifecycle operation; the Celery child-task identity; and the Sentry
exc_infopaths — 488 Python tests and 67 Rust tests in total.