From 47284098cef206d178bb9efcc73c3c139da89a3c Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Thu, 16 Jul 2026 15:21:00 +0000 Subject: [PATCH 1/2] docs: move recipe examples into included source files Non-doctest recipe snippets now live under docs/examples/recipes/ and are pulled into docs/recipes.md with literalinclude, matching the svcs pattern so example code can be checked independently. Fixes #819 Signed-off-by: Alex Chen --- CHANGELOG.md | 5 + .../recipes/fine_grained_filtering.py | 39 ++++++++ .../recipes/pass_context_to_worker_threads.py | 38 ++++++++ .../recipes/redact_exception_dict_locals.py | 27 ++++++ docs/recipes.md | 94 ++----------------- pyproject.toml | 7 ++ 6 files changed, 126 insertions(+), 84 deletions(-) create mode 100644 docs/examples/recipes/fine_grained_filtering.py create mode 100644 docs/examples/recipes/pass_context_to_worker_threads.py create mode 100644 docs/examples/recipes/redact_exception_dict_locals.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b23d71a7..272d229c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ## [Unreleased](https://github.com/hynek/structlog/compare/26.1.0...HEAD) +### Changed + +- Move non-doctest recipe examples into `docs/examples/` and include them with `literalinclude`, following the *svcs* pattern so example source can be verified independently. + [#819](https://github.com/hynek/structlog/issues/819) + ## [26.1.0](https://github.com/hynek/structlog/compare/25.5.0...26.1.0) - 2026-06-06 diff --git a/docs/examples/recipes/fine_grained_filtering.py b/docs/examples/recipes/fine_grained_filtering.py new file mode 100644 index 00000000..71231590 --- /dev/null +++ b/docs/examples/recipes/fine_grained_filtering.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +"""Fine-grained log-level filtering recipe. + +Used from docs/recipes.md via literalinclude. +""" + +import structlog + + +logger = structlog.get_logger() + + +def f(): + logger.info("f called") + + +def g(): + logger.info("g called") + + +def filter_f(_, __, event_dict): + if event_dict.get("func_name") == "f": + raise structlog.DropEvent + + return event_dict + + +structlog.configure( + processors=[ + structlog.processors.CallsiteParameterAdder( + [structlog.processors.CallsiteParameter.FUNC_NAME] + ), + filter_f, # <-- your processor! + structlog.processors.KeyValueRenderer(), + ] +) + +f() +g() diff --git a/docs/examples/recipes/pass_context_to_worker_threads.py b/docs/examples/recipes/pass_context_to_worker_threads.py new file mode 100644 index 00000000..aa07df47 --- /dev/null +++ b/docs/examples/recipes/pass_context_to_worker_threads.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +"""Pass contextvars into worker threads. + +Used from docs/recipes.md via literalinclude. + +Requires the optional *pathos* package for ThreadPool. +""" + +from functools import partial + +from pathos.threading import ThreadPool + +import structlog + +from structlog.contextvars import bind_contextvars + + +logger = structlog.get_logger(__name__) + + +def do_some_work(ctx, this_worker): + bind_contextvars(**ctx) + logger.info("WorkerDidSomeWork", worker=this_worker) + + +def structlog_with_threadpool(f): + ctx = structlog.contextvars.get_contextvars() + func = partial(f, ctx) + workers = ["1", "2", "3"] + + with ThreadPool() as pool: + return list(pool.map(func, workers)) + + +def manager(request_id: str): + bind_contextvars(request_id=request_id) + logger.info("StartingWorkers") + structlog_with_threadpool(do_some_work) diff --git a/docs/examples/recipes/redact_exception_dict_locals.py b/docs/examples/recipes/redact_exception_dict_locals.py new file mode 100644 index 00000000..8b2d3893 --- /dev/null +++ b/docs/examples/recipes/redact_exception_dict_locals.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +"""Custom filtering of dict traceback locals. + +Used from docs/recipes.md via literalinclude. +""" + +from structlog.tracebacks import ExceptionDictTransformer + + +def redact_locals(frame_locals): + return { + k: (v if k not in ("token", "password") else "") + for k, v in frame_locals.items() + } + + +def redact_frames(exc_dict): + for frame in exc_dict["frames"]: + if "locals" in frame: + frame["locals"] = redact_locals(frame["locals"]) + return exc_dict + + +class RedactingExceptionDictTransformer(ExceptionDictTransformer): + def __call__(self, exc_info): + exceptions = super().__call__(exc_info) + return [redact_frames(exc) for exc in exceptions] diff --git a/docs/recipes.md b/docs/recipes.md index 1d576252..f7bd8bbe 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -30,40 +30,12 @@ Sometimes, that can be a bit too coarse, though. You can achieve finer control by adding the {class}`~structlog.processors.CallsiteParameterAdder` processor and writing a simple processor that acts on the call site data added. -Let's assume you have the following code: - -```python -logger = structlog.get_logger() - -def f(): - logger.info("f called") - -def g(): - logger.info("g called") - -f() -g() -``` - -And you don't want to see log entries from function `f`. +Let's assume you have the following code and want to drop log entries from function `f`. You add {class}`~structlog.processors.CallsiteParameterAdder` to the processor chain and then look at the `func_name` field in the *event dict*: -```python -def filter_f(_, __, event_dict): - if event_dict.get("func_name") == "f": - raise structlog.DropEvent - - return event_dict - -structlog.configure( - processors=[ - structlog.processors.CallsiteParameterAdder( - [structlog.processors.CallsiteParameter.FUNC_NAME] - ), - filter_f, # <-- your processor! - structlog.processors.KeyValueRenderer(), - ] -) +```{literalinclude} examples/recipes/fine_grained_filtering.py +:language: python +:start-at: logger = structlog.get_logger() ``` Running this gives you: @@ -156,28 +128,9 @@ Because this is only called during exception handling, it's an ideal point of co The following example customizes the transformer to redact values in `locals` named `"token"` or `"password"`. -``` -from structlog.tracebacks import ExceptionDictTransformer - - -def redact_locals(frame_locals): - return { - k: (v if k not in ("token", "password") else "") - for k, v in frame_locals.items() - } - - -def redact_frames(exc_dict): - for frame in exc_dict["frames"]: - if "locals" in frame: - frame["locals"] = redact_locals(frame["locals"]) - return exc_dict - - -class RedactingExceptionDictTransformer(ExceptionDictTransformer): - def __call__(self, exc_info): - exceptions = super().__call__(exc_info) - return [redact_frames(exc) for exc in exceptions] +```{literalinclude} examples/recipes/redact_exception_dict_locals.py +:language: python +:start-at: from structlog.tracebacks import ExceptionDictTransformer ``` Please remember that values may appear elsewhere in the dict traceback, for example in an exception string. @@ -196,36 +149,9 @@ The context variables are retrieved and passed as the first argument to the part The pool invokes the partial function, once for each element of `workers`. Inside of `do_some_work`, the context vars are bound and a message about the great work being performed is logged -- including the `request_id` key / value pair. -``` -from functools import partial - -import structlog - -from structlog.contextvars import bind_contextvars -from pathos.threading import ThreadPool - -logger = structlog.get_logger(__name__) - - -def do_some_work(ctx, this_worker): - bind_contextvars(**ctx) - logger.info("WorkerDidSomeWork", worker=this_worker) - - -def structlog_with_threadpool(f): - ctx = structlog.contextvars.get_contextvars() - func = partial(f, ctx) - workers = ["1", "2", "3"] - - with ThreadPool() as pool: - return list(pool.map(func, workers)) - - -def manager(request_id: str): - bind_contextvars(request_id=request_id) - logger.info("StartingWorkers") - structlog_with_threadpool(do_some_work) - +```{literalinclude} examples/recipes/pass_context_to_worker_threads.py +:language: python +:start-at: from functools import partial ``` See the [issue 425](https://github.com/hynek/structlog/issues/425) for a more complete example. diff --git a/pyproject.toml b/pyproject.toml index 4296685b..ac85b90d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,13 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] +"docs/examples/*" = [ + "INP001", # examples are not packages + "D", # recipe snippets + "T201", # prints may appear in demos + "S", # example code, not production + "E402", # import order in snippets is free-form +] "tests/*" = [ "B018", # "useless" expressions can be useful in tests "BLE", # tests have different rules around exceptions From 412c12947a7a237a15854e51b6761c3cc058810d Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Thu, 16 Jul 2026 17:04:12 +0000 Subject: [PATCH 2/2] docs: ignore ty diagnostics for recipe example sources Moving recipes into docs/examples/*.py made ty check those files. Optional pathos import is intentional for the ThreadPool recipe and is not part of the typed API surface under tests/typing. Include docs/examples in the existing ty override that keeps full rules only for tests/typing. Signed-off-by: Alex Chen --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac85b90d..e7f5ad4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,7 +191,7 @@ ignore_errors = false # Analyze everything for LSP, but only complain about typing examples. [[tool.ty.overrides]] -include = ["src", "tests"] +include = ["src", "tests", "docs/examples"] exclude = ["tests/typing/"] rules.all = "ignore"