Skip to content

v1.2.0

Choose a tag to compare

@kidoz kidoz released this 27 Aug 17:43

structguru v1.2.0

A correctness and documentation release. Two defects are fixed: the gRPC interceptor leaked request context out of every streaming handler, and the documented rewrite for look-behind redaction patterns re-emitted the secret it was supposed to remove. get_contextvars() joins the public context API so custom adapters can implement the snapshot/restore sequence that fix required, and the generated API reference gains real redaction documentation plus a context-propagation guide for integration authors. No breaking changes; upgrade in place from any v1.x.

Fixes

  • Streaming gRPC handlers no longer leak request context. A streaming handler returns a lazy iterator — calling it runs none of the generator body — so the interceptor's grpc_method and request_id stayed bound to the thread for the whole interval between the handler returning and gRPC beginning to consume the response, and indefinitely if the client cancelled and the iterator was never consumed. Any unrelated record logged on that thread in the meantime carried the wrong request's identity.

    result = interceptor.intercept_service(continuation, details)
    stream = result.unary_stream(request, context)  # iterator created, not yet consumed
    # before: {'grpc_method': '/svc/Method', 'request_id': 'leaky'}
    # after:  {}

    The handler's context is now snapshotted and cleared when the iterator is returned, then restored when iteration actually begins. The snapshot matters for handlers that are not generators: one that returns a list runs eagerly during the call, so context it bound there is captured and restored rather than discarded by the clear. Unary handlers are unchanged.

  • The documented look-behind rewrite no longer leaks the secret. Rust's linear-time regex engine rejects look-around, and the README, the configure() docstring, and the ValueError raised on an unsupported pattern all recommended rewriting (?<=password=)\S+ as password=(\S+) with pattern_replacement="$1[REDACTED]". That group captures the secret, so $1 re-emitted it verbatim:

    configure(
        sensitive_patterns=[r"(password=)\S+"],  # group wraps the prefix, not the secret
        pattern_replacement="$1[REDACTED]",
    )
    # before (as documented):  "login password=hunter2 ok" → "login hunter2[REDACTED] ok"
    # after:                   "login password=hunter2 ok" → "login password=[REDACTED] ok"

    Redaction behavior itself was always correct — only the guidance was wrong — so key-based redaction and patterns without a capture group were never affected. If you copied the documented pattern into a configure() call, update it: the capture group belongs around the prefix you want to keep, never around the secret.

Features

  • get_contextvars() is exported from the package root, completing the set alongside bind_contextvars, bound_contextvars, and clear_contextvars. It returns a shallow snapshot of the bound context: mutating the returned dictionary does not change the context, and nested values remain shared.

    from structguru import bind_contextvars, clear_contextvars, get_contextvars
    
    bind_contextvars(request_id="req-1")
    snapshot = get_contextvars()   # {'request_id': 'req-1'}
    clear_contextvars()
    bind_contextvars(**snapshot)   # restore it later, on the same thread or another

    Adapters that must capture context and restore it across a lazy boundary no longer have to import it from the private structguru._contextvars.

Docs

  • A new Context Propagation guide for anyone writing or reviewing an integration adapter. It documents the copy-on-write model and how asyncio tasks (inherit a copy) differ from new threads (start empty), the three rules every adapter must hold, the lazy-result hazard behind the gRPC fix with the snapshot/clear/restore pattern, and the testing rules that catch this class of bug — including closing suspended generators so their cleanup does not run at an arbitrary GC point in an unrelated test.

  • The redaction API reference is real documentation rather than a one-line stub: what key- and pattern-based redaction each cover, the capture-group rule with correct and leaking examples side by side, the fail-closed backtrack limit under allow_backtracking_patterns, and the Sentry marker guard. It also documents that sensitive_keys replaces the built-in set rather than extending it — configure(sensitive_keys=["custom"]) leaves password unredacted — and shows the [*DEFAULT_SENSITIVE_KEYS, ...] spread.

  • Integration guide corrections. The Sentry section previously implied severity alone controls captured events; by default only events carrying exc_info become Sentry events, and a plain logger.error() stays a breadcrumb unless capture_messages=True. The HTTPX section now separates HTTP error responses from transport-level failures (connection refused, DNS, timeouts), which raise before the response hook runs and are not logged — and notes that logger.catch() defaults to reraise=False, so it would swallow them. The ASGI request_id_header is documented as case-insensitive, which is what the middleware has always done.

  • README accuracy. The console-format example showed a stale 2025-… [info ] line; the renderer emits an RFC 3339 timestamp and an uppercase padded [INFO ]. The abbreviated JSON example now matches the renderer's actual key order.

  • The public logging methods are documented. debug, info, warning, error, critical and the loguru-compatible aliases had no docstrings, and since the API reference is generated entirely from them, they appeared as bare signatures. trace → DEBUG and success → INFO are now stated explicitly.

Tooling

  • Ruff enforces docstrings on the published API surface. docs/api/ is generated wholly from docstrings, so a missing one is an invisible hole in the published reference rather than a visible gap. The D ruleset now runs over src/ with the NumPy convention; tests, examples, and scripts are exempt.

  • mkdocstrings parses the NumPy docstring style the codebase actually uses. It was configured for Google style while every module writes NumPy sections, so Parameters rendered as a raw heading instead of a parameter table. Parameters now render with name, type, description, and default, and the docs build runs clean under --strict.

Test coverage

  • 4 new tests pin the fixed behavior: the gRPC pre-iteration window (context must be clean before the first next()) and eager-handler restore, plus the get_contextvars root export and its shallow-snapshot contract — 467 Python tests and 66 Rust tests in total.