structlog-config 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,9 +11,10 @@ from structlog.typing import FilteringBoundLogger
11
11
 
12
12
  from structlog_config.formatters import (
13
13
  PathPrettifier,
14
+ WheneverFormatter,
14
15
  add_fastapi_context,
16
+ beautiful_traceback_exception_formatter,
15
17
  logger_name,
16
- pretty_traceback_exception_formatter,
17
18
  simplify_activemodel_objects,
18
19
  )
19
20
 
@@ -45,8 +46,7 @@ def log_processors_for_mode(json_logger: bool) -> list[structlog.types.Processor
45
46
  )
46
47
 
47
48
  return [
48
- # add exc_info=True to a log and get a full stack trace attached to it
49
- structlog.processors.format_exc_info,
49
+ # omit `structlog.processors.format_exc_info` so we can use structured logging for exceptions
50
50
  # simple, short exception rendering in prod since sentry is in place
51
51
  # https://www.structlog.org/en/stable/exceptions.html this is a customized version of dict_tracebacks
52
52
  ExceptionRenderer(
@@ -65,8 +65,8 @@ def log_processors_for_mode(json_logger: bool) -> list[structlog.types.Processor
65
65
  return [
66
66
  structlog.dev.ConsoleRenderer(
67
67
  colors=not NO_COLOR,
68
- exception_formatter=pretty_traceback_exception_formatter
69
- if packages.pretty_traceback
68
+ exception_formatter=beautiful_traceback_exception_formatter
69
+ if packages.beautiful_traceback
70
70
  else structlog.dev.default_exception_formatter,
71
71
  )
72
72
  ]
@@ -86,6 +86,7 @@ def get_default_processors(json_logger) -> list[structlog.types.Processor]:
86
86
  if packages.activemodel and packages.typeid
87
87
  else None,
88
88
  PathPrettifier(),
89
+ WheneverFormatter() if packages.whenever else None,
89
90
  structlog.processors.TimeStamper(fmt="iso", utc=True),
90
91
  # add `stack_info=True` to a log and get a `stack` attached to the log
91
92
  structlog.processors.StackInfoRenderer(),
@@ -7,7 +7,7 @@ from urllib.parse import quote
7
7
 
8
8
  import structlog
9
9
  from fastapi import FastAPI
10
- from python_ipware import IpWare
10
+ from fastapi_ipware import FastAPIIpWare
11
11
  from starlette.middleware.base import RequestResponseEndpoint
12
12
  from starlette.requests import Request
13
13
  from starlette.responses import Response
@@ -15,9 +15,8 @@ from starlette.routing import Match, Mount
15
15
  from starlette.types import Scope
16
16
  from starlette.websockets import WebSocket
17
17
 
18
- # should name this access "access_log" or something
19
18
  log = structlog.get_logger()
20
- ipw = IpWare()
19
+ ipware = FastAPIIpWare()
21
20
 
22
21
 
23
22
  def get_route_name(app: FastAPI, scope: Scope, prefix: str = "") -> str:
@@ -59,35 +58,17 @@ def client_ip_from_request(request: Request | WebSocket) -> str | None:
59
58
  """
60
59
  Get the client IP address from the request.
61
60
 
62
- Headers are not case-sensitive.
63
-
64
- Uses ipware library to properly extract client IP from various proxy headers.
61
+ Uses fastapi-ipware library to properly extract client IP from various proxy headers.
65
62
  Fallback to direct client connection if no proxy headers found.
66
63
  """
67
- headers = request.headers
68
-
69
- # TODO this seems really inefficient, we should just rewrite the ipware into this repo :/
70
- # Convert Starlette headers to format expected by ipware (HTTP_ prefixed)
71
- # ipware expects headers in WSGI/Django-style meta format where HTTP headers
72
- # are prefixed with "HTTP_" and dashes become underscores.
73
- # See: https://github.com/un33k/python-ipware/blob/main/python_ipware/python_ipware.py#L33-L40
74
- meta_dict = {}
75
- for name, value in headers.items():
76
- # Convert header name to HTTP_ prefixed format
77
- meta_key = f"HTTP_{name.upper().replace('-', '_')}"
78
- meta_dict[meta_key] = value
79
-
80
- # Use ipware to extract IP from headers
81
- ip, trusted_route = ipw.get_client_ip(meta=meta_dict)
64
+ ip, trusted_route = ipware.get_client_ip_from_request(request)
82
65
  if ip:
83
66
  log.debug(
84
67
  "extracted client IP from headers", ip=ip, trusted_route=trusted_route
85
68
  )
86
69
  return str(ip)
87
70
 
88
- # Fallback to direct client connection
89
71
  host = request.client.host if request.client else None
90
-
91
72
  return host
92
73
 
93
74
 
@@ -101,16 +82,8 @@ def is_static_assets_request(scope: Scope) -> bool:
101
82
  Returns:
102
83
  bool: True if the request is for static assets, False otherwise.
103
84
  """
104
- return (
105
- scope["path"].endswith(".css")
106
- or scope["path"].endswith(".js")
107
- # .map files are attempted when devtools are enabled
108
- or scope["path"].endswith(".js.map")
109
- or scope["path"].endswith(".ico")
110
- or scope["path"].endswith(".png")
111
- or scope["path"].endswith(".jpg")
112
- or scope["path"].endswith(".jpeg")
113
- or scope["path"].endswith(".gif")
85
+ return scope["path"].endswith(
86
+ (".css", ".js", ".js.map", ".ico", ".png", ".jpg", ".jpeg", ".gif", ".webp")
114
87
  )
115
88
 
116
89
 
@@ -64,17 +64,17 @@ def logger_name(logger: Any, method_name: Any, event_dict: EventDict) -> EventDi
64
64
  return event_dict
65
65
 
66
66
 
67
- def pretty_traceback_exception_formatter(sio: TextIO, exc_info: ExcInfo) -> None:
67
+ def beautiful_traceback_exception_formatter(sio: TextIO, exc_info: ExcInfo) -> None:
68
68
  """
69
69
  By default, rich and then better-exceptions is used to render exceptions when a ConsoleRenderer is used.
70
70
 
71
- I prefer pretty-traceback, so I've added a custom processor to use it.
71
+ I prefer beautiful-traceback, so I've added a custom processor to use it.
72
72
 
73
73
  https://github.com/hynek/structlog/blob/66e22d261bf493ad2084009ec97c51832fdbb0b9/src/structlog/dev.py#L412
74
74
  """
75
75
 
76
76
  # only available in dev
77
- from pretty_traceback.formatting import exc_to_traceback_str
77
+ from beautiful_traceback.formatting import exc_to_traceback_str
78
78
 
79
79
  _, exc_value, traceback = exc_info
80
80
  # TODO support local_stack_only env var support
@@ -150,6 +150,26 @@ class RenameField:
150
150
  return event_dict
151
151
 
152
152
 
153
+ class WheneverFormatter:
154
+ """A processor for formatting whenever datetime objects.
155
+
156
+ Changes all whenever datetime objects (ZonedDateTime, Instant, PlainDateTime, etc.)
157
+ from their repr() format (e.g., ZonedDateTime("2025-11-02 00:00:00+00:00[UTC]"))
158
+ to their string format (e.g., 2025-11-02 00:00:00+00:00[UTC]).
159
+
160
+ This provides cleaner log output without the class wrapper.
161
+ """
162
+
163
+ def __call__(self, _, __, event_dict):
164
+ for key, value in event_dict.items():
165
+ # Check if the value has the _pywhenever module attribute
166
+ # This is a reliable way to detect whenever types without importing them
167
+ if hasattr(value, "__module__") and value.__module__.startswith("whenever"):
168
+ event_dict[key] = str(value)
169
+
170
+ return event_dict
171
+
172
+
153
173
  def add_fastapi_context(
154
174
  logger: logging.Logger,
155
175
  method_name: str,
@@ -23,11 +23,16 @@ except ImportError:
23
23
  typeid = None
24
24
 
25
25
  try:
26
- import pretty_traceback
26
+ import beautiful_traceback
27
27
  except ImportError:
28
- pretty_traceback = None
28
+ beautiful_traceback = None
29
29
 
30
30
  try:
31
31
  import starlette_context
32
32
  except ImportError:
33
33
  starlette_context = None
34
+
35
+ try:
36
+ import whenever
37
+ except ImportError:
38
+ whenever = None
@@ -0,0 +1,222 @@
1
+ """
2
+ Pytest plugin for capturing and displaying logs only on test failures.
3
+
4
+ This plugin integrates with structlog-config's file logging to capture logs per-test
5
+ and display them only when tests fail, keeping output clean for passing tests.
6
+
7
+ Usage:
8
+ 1. Install the plugin (automatically registered via entry point):
9
+ pip install structlog-config[fastapi]
10
+
11
+ 2. Enable in pytest.ini or pyproject.toml:
12
+ [tool.pytest.ini_options]
13
+ addopts = ["--capture-logs-on-fail"]
14
+
15
+ Or enable for a single test run:
16
+ pytest --capture-logs-on-fail
17
+
18
+ 3. Optional: Persist all logs to a directory:
19
+ pytest --capture-logs-dir=/path/to/logs
20
+
21
+ How it works:
22
+ - Sets PYTHON_LOG_PATH to a unique temp file for each test
23
+ - Logs are written to /tmp/<project-name>-pytest-logs-*/test_name.log
24
+ - On test failure, prints captured logs to stdout
25
+ - Cleans up temp files after each test (unless --capture-logs-dir is set)
26
+ - Automatically disabled if PYTHON_LOG_PATH is already set
27
+
28
+ Example output on failure:
29
+ --- Captured logs for failed test: tests/test_foo.py::test_bar ---
30
+ 2025-10-31 23:30:00 [info] Starting test
31
+ 2025-10-31 23:30:01 [error] Something went wrong
32
+ """
33
+
34
+ import logging
35
+ import os
36
+ import re
37
+ import shutil
38
+ import tempfile
39
+ from pathlib import Path
40
+ from typing import Generator
41
+
42
+ import pytest
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ PLUGIN_KEY = pytest.StashKey[dict]()
47
+ SESSION_TMPDIR_KEY = "session_tmpdir"
48
+
49
+
50
+ def sanitize_filename(name: str) -> str:
51
+ """Replace non-filename-safe characters with underscores.
52
+
53
+ Args:
54
+ name: The filename to sanitize (typically a pytest nodeid).
55
+
56
+ Returns:
57
+ A filesystem-safe filename string.
58
+ """
59
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", name)
60
+
61
+
62
+ def pytest_addoption(parser: pytest.Parser) -> None:
63
+ """Register the --capture-logs-on-fail command line option.
64
+
65
+ Args:
66
+ parser: The pytest parser to add options to.
67
+ """
68
+ parser.addoption(
69
+ "--capture-logs-on-fail",
70
+ action="store_true",
71
+ default=False,
72
+ help="Capture logs to a temp file and dump them to stdout on test failure.",
73
+ )
74
+ parser.addoption(
75
+ "--capture-logs-dir",
76
+ action="store",
77
+ default=None,
78
+ help="Directory to persist all test logs (disables automatic cleanup).",
79
+ )
80
+
81
+
82
+ @pytest.hookimpl(tryfirst=True)
83
+ def pytest_configure(config: pytest.Config) -> None:
84
+ """Configure the plugin at pytest startup.
85
+
86
+ Stores configuration state on the config object for use by fixtures and hooks.
87
+
88
+ Args:
89
+ config: The pytest config object.
90
+ """
91
+ logs_dir = config.getoption("--capture-logs-dir")
92
+ enabled = config.getoption("--capture-logs-on-fail") or logs_dir is not None
93
+
94
+ plugin_config = {
95
+ "enabled": enabled,
96
+ "logs_dir": logs_dir,
97
+ "project_name": os.path.basename(str(config.rootdir)),
98
+ }
99
+ config.stash[PLUGIN_KEY] = plugin_config
100
+
101
+
102
+ @pytest.hookimpl(tryfirst=True)
103
+ def pytest_sessionstart(session: pytest.Session) -> None:
104
+ """Create a session-level temp directory for log files.
105
+
106
+ Args:
107
+ session: The pytest session object.
108
+ """
109
+ config = session.config
110
+ plugin_config = config.stash.get(PLUGIN_KEY, {})
111
+
112
+ if not plugin_config.get("enabled"):
113
+ return
114
+
115
+ logs_dir = plugin_config.get("logs_dir")
116
+ if logs_dir:
117
+ tmpdir = Path(logs_dir)
118
+ tmpdir.mkdir(parents=True, exist_ok=True)
119
+ else:
120
+ project_name = plugin_config.get("project_name", "pytest")
121
+ tmpdir = Path(tempfile.mkdtemp(prefix=f"{project_name}-pytest-logs-"))
122
+
123
+ plugin_config[SESSION_TMPDIR_KEY] = tmpdir
124
+ config.stash[PLUGIN_KEY] = plugin_config
125
+
126
+
127
+ @pytest.hookimpl(trylast=True)
128
+ def pytest_sessionfinish(session: pytest.Session) -> None:
129
+ """Clean up session-level temp directory unless --capture-logs-dir was used.
130
+
131
+ Args:
132
+ session: The pytest session object.
133
+ """
134
+ config = session.config
135
+ plugin_config = config.stash.get(PLUGIN_KEY, {})
136
+
137
+ if not plugin_config.get("enabled"):
138
+ return
139
+
140
+ logs_dir = plugin_config.get("logs_dir")
141
+ tmpdir = plugin_config.get(SESSION_TMPDIR_KEY)
142
+
143
+ if tmpdir and not logs_dir:
144
+ shutil.rmtree(tmpdir, ignore_errors=True)
145
+
146
+
147
+ @pytest.fixture(autouse=True)
148
+ def capture_logs_on_fail(request: pytest.FixtureRequest) -> Generator[None, None, None]:
149
+ """Set up per-test log capture to a temporary file.
150
+
151
+ This fixture runs automatically for every test when --capture-logs-on-fail is enabled.
152
+ It sets PYTHON_LOG_PATH to redirect logs to a unique temp file, then cleans up after.
153
+
154
+ Args:
155
+ request: The pytest request fixture providing test context.
156
+
157
+ Yields:
158
+ Control back to the test, then handles cleanup after test completion.
159
+ """
160
+ config = request.config
161
+ plugin_config = config.stash.get(PLUGIN_KEY, {})
162
+
163
+ if not plugin_config.get("enabled"):
164
+ yield
165
+ return
166
+
167
+ if "PYTHON_LOG_PATH" in os.environ:
168
+ logger.warning(
169
+ "PYTHON_LOG_PATH is already set; pytest-capture-logs-on-fail plugin is disabled for this test."
170
+ )
171
+ yield
172
+ return
173
+
174
+ tmpdir = plugin_config.get(SESSION_TMPDIR_KEY)
175
+ if not tmpdir:
176
+ logger.warning("Session temp directory not initialized")
177
+ yield
178
+ return
179
+
180
+ test_name = sanitize_filename(request.node.nodeid)
181
+ log_file = tmpdir / f"{test_name}.log"
182
+
183
+ original_log_path = os.environ.get("PYTHON_LOG_PATH")
184
+ os.environ["PYTHON_LOG_PATH"] = str(log_file)
185
+
186
+ logger.info(f"Logs for test '{request.node.nodeid}' will be stored at: {log_file}")
187
+
188
+ yield
189
+
190
+ setattr(request.node, "_pytest_log_file", str(log_file))
191
+
192
+ if original_log_path is not None:
193
+ os.environ["PYTHON_LOG_PATH"] = original_log_path
194
+ else:
195
+ del os.environ["PYTHON_LOG_PATH"]
196
+
197
+
198
+ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo) -> None:
199
+ """Hook called after each test phase to create test reports.
200
+
201
+ On test failure, reads and prints the captured log file to stdout.
202
+ Handles failures in setup, call, and teardown phases.
203
+
204
+ Args:
205
+ item: The test item being reported on.
206
+ call: The call object containing execution info and any exception.
207
+ """
208
+ config = item.config
209
+ plugin_config = config.stash.get(PLUGIN_KEY, {})
210
+
211
+ if not plugin_config.get("enabled"):
212
+ return
213
+
214
+ if call.excinfo is not None:
215
+ log_file = getattr(item, "_pytest_log_file", None)
216
+ if log_file and os.path.exists(log_file):
217
+ with open(log_file, "r") as f:
218
+ logs = f.read()
219
+
220
+ if logs.strip():
221
+ phase = call.when
222
+ print(f"\n--- Captured logs for failed test ({phase}): {item.nodeid} ---\n{logs}\n")
@@ -11,7 +11,6 @@ from decouple import config
11
11
 
12
12
  from .constants import PYTHONASYNCIODEBUG
13
13
  from .env_config import get_custom_logger_config
14
- from .environments import is_production, is_staging
15
14
  from .levels import (
16
15
  compare_log_levels,
17
16
  get_environment_log_level_as_string,
@@ -49,14 +48,18 @@ def redirect_stdlib_loggers(json_logger: bool):
49
48
 
50
49
  default_processors = get_default_processors(json_logger=json_logger)
51
50
 
51
+ if json_logger:
52
+ # don't use ORJSON here, as the stdlib formatter chain expects a str not a bytes
53
+ final_renderer = structlog.processors.JSONRenderer(sort_keys=True)
54
+ else:
55
+ # use the default renderer, which is the last processor
56
+ final_renderer = default_processors[-1]
57
+
52
58
  formatter = ProcessorFormatter(
53
59
  processors=[
54
60
  # required to strip extra keys that the structlog stdlib bindings add in
55
61
  structlog.stdlib.ProcessorFormatter.remove_processors_meta,
56
- default_processors[-1]
57
- if not is_production() and not is_staging()
58
- # don't use ORJSON here, as the stdlib formatter chain expects a str not a bytes
59
- else structlog.processors.JSONRenderer(sort_keys=True),
62
+ final_renderer,
60
63
  ],
61
64
  # processors unique to stdlib logging
62
65
  foreign_pre_chain=[
structlog_config/trace.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """
2
+ Adds a TRACE log level to the standard logging module and structlog.
3
+
4
+ Some people believe that the standard log levels are not enough, and I'm with them.
5
+
2
6
  Adapted from:
3
7
  - https://github.com/willmcgugan/httpx/blob/973d1ed4e06577d928061092affe8f94def03331/httpx/_utils.py#L231
4
8
  - https://github.com/vladmandic/sdnext/blob/d5d857aa961edbc46c9e77e7698f2e60011e7439/installer.py#L154
5
-
6
- TODO this is not fully integrated into the codebase
7
9
  """
8
10
 
9
11
  import logging
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: structlog-config
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: A comprehensive structlog configuration with sensible defaults for development and production environments, featuring context management, exception formatting, and path prettification.
5
5
  Keywords: logging,structlog,json-logging,structured-logging
6
6
  Author: Michael Bianco
@@ -9,8 +9,10 @@ Requires-Dist: orjson>=3.10.15
9
9
  Requires-Dist: python-decouple-typed>=3.11.0
10
10
  Requires-Dist: python-ipware>=3.0.0
11
11
  Requires-Dist: structlog>=25.2.0
12
- Requires-Python: >=3.10
12
+ Requires-Dist: fastapi-ipware>=0.1.0 ; extra == 'fastapi'
13
+ Requires-Python: >=3.11
13
14
  Project-URL: Repository, https://github.com/iloveitaly/structlog-config
15
+ Provides-Extra: fastapi
14
16
  Description-Content-Type: text/markdown
15
17
 
16
18
  # Opinionated Defaults for Structlog
@@ -29,6 +31,18 @@ Here are the main goals:
29
31
  * Ability to log level and output (i.e. file path) *by logger* for easy development debugging
30
32
  * If you are using fastapi, structured logging for access logs
31
33
 
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install structlog-config
38
+ ```
39
+
40
+ Or with [uv](https://docs.astral.sh/uv/):
41
+
42
+ ```bash
43
+ uv add structlog-config
44
+ ```
45
+
32
46
  ## Usage
33
47
 
34
48
  ```python
@@ -106,8 +120,68 @@ For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.
106
120
  * `LOG_LEVEL_HTTPX=DEBUG`
107
121
  * `LOG_PATH_HTTPX=tmp/openai.log`
108
122
 
123
+ ## Custom Formatters
124
+
125
+ This package includes several custom formatters that automatically clean up log output:
126
+
127
+ ### Path Prettifier
128
+
129
+ Automatically formats `pathlib.Path` and `PosixPath` objects to show relative paths when possible, removing the wrapper class names:
130
+
131
+ ```python
132
+ from pathlib import Path
133
+ log.info("Processing file", file_path=Path.cwd() / "data" / "users.csv")
134
+ # Output: file_path=data/users.csv (instead of PosixPath('/home/user/data/users.csv'))
135
+ ```
136
+
137
+ ### Whenever Datetime Formatter
138
+
139
+ **Note:** Requires `pip install whenever` to be installed.
140
+
141
+ Formats [whenever](https://github.com/ariebovenberg/whenever) datetime objects without their class wrappers for cleaner output:
142
+
143
+ ```python
144
+ from whenever import ZonedDateTime
145
+
146
+ log.info("Event scheduled", event_time=ZonedDateTime(2025, 11, 2, 0, 0, 0, tz="UTC"))
147
+ # Output: event_time=2025-11-02T00:00:00+00:00[UTC]
148
+ # Instead of: event_time=ZonedDateTime("2025-11-02T00:00:00+00:00[UTC]")
149
+ ```
150
+
151
+ Supports all whenever datetime types: `ZonedDateTime`, `Instant`, `LocalDateTime`, `PlainDateTime`, etc.
152
+
153
+ ### ActiveModel Object Formatter
154
+
155
+ **Note:** Requires `pip install activemodel` and `pip install typeid-python` to be installed.
156
+
157
+ Automatically converts [ActiveModel](https://github.com/iloveitaly/activemodel) BaseModel instances to their ID representation and TypeID objects to strings:
158
+
159
+ ```python
160
+ from activemodel import BaseModel
161
+
162
+ user = User(id="user_123", name="Alice")
163
+ log.info("User action", user=user)
164
+ # Output: user_id=user_123 (instead of full object representation)
165
+ ```
166
+
167
+ ### FastAPI Context
168
+
169
+ **Note:** Requires `pip install starlette-context` to be installed.
170
+
171
+ Automatically includes all context data from [starlette-context](https://github.com/tomwojcik/starlette-context) in your logs, useful for request tracing:
172
+
173
+ ```python
174
+ # Context data (request_id, correlation_id, etc.) automatically included in all logs
175
+ log.info("Processing request")
176
+ # Output includes: request_id=abc-123 correlation_id=xyz-789 ...
177
+ ```
178
+
179
+ All formatters are optional and automatically enabled when their respective dependencies are installed. They work seamlessly in both development (console) and production (JSON) logging modes.
180
+
109
181
  ## FastAPI Access Logger
110
182
 
183
+ **Note:** Requires `pip install structlog-config[fastapi]` for FastAPI dependencies.
184
+
111
185
  Structured, simple access log with request timing to replace the default fastapi access log. Why?
112
186
 
113
187
  1. It's less verbose
@@ -119,6 +193,74 @@ Here's how to use it:
119
193
  1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
120
194
  2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
121
195
 
196
+ ## Pytest Plugin: Capture Logs on Failure
197
+
198
+ A pytest plugin that captures logs per-test and displays them only when tests fail. This keeps your test output clean while ensuring you have all the debugging information you need when something goes wrong.
199
+
200
+ ### Features
201
+
202
+ - Only shows logs for failing tests (keeps output clean)
203
+ - Captures logs from all test phases (setup, call, teardown)
204
+ - Unique log file per test
205
+ - Optional persistent log storage for debugging
206
+ - Automatically handles `PYTHON_LOG_PATH` environment variable
207
+
208
+ ### Usage
209
+
210
+ Enable the plugin with the `--capture-logs-on-fail` flag:
211
+
212
+ ```bash
213
+ pytest --capture-logs-on-fail
214
+ ```
215
+
216
+ Or enable it permanently in `pytest.ini` or `pyproject.toml`:
217
+
218
+ ```toml
219
+ [tool.pytest.ini_options]
220
+ addopts = ["--capture-logs-on-fail"]
221
+ ```
222
+
223
+ ### Persist Logs to Directory
224
+
225
+ To keep all test logs for later inspection (useful for CI/CD debugging):
226
+
227
+ ```bash
228
+ pytest --capture-logs-dir=./test-logs
229
+ ```
230
+
231
+ This creates a log file for each test and disables automatic cleanup.
232
+
233
+ ### How It Works
234
+
235
+ 1. Sets `PYTHON_LOG_PATH` environment variable to a unique temp file for each test
236
+ 2. Your application logs (via `configure_logger()`) write to this file
237
+ 3. On test failure, the plugin prints the captured logs to stdout
238
+ 4. Log files are cleaned up after the test session (unless `--capture-logs-dir` is used)
239
+
240
+ ### Example Output
241
+
242
+ When a test fails, you'll see:
243
+
244
+ ```
245
+ FAILED tests/test_user.py::test_user_login
246
+
247
+ --- Captured logs for failed test (call): tests/test_user.py::test_user_login ---
248
+ 2025-11-01 18:30:00 [info] User login started user_id=123
249
+ 2025-11-01 18:30:01 [error] Database connection failed timeout=5.0
250
+ ```
251
+
252
+ For passing tests, no log output is shown, keeping your test output clean and focused.
253
+
254
+ ## Beautiful Traceback Support
255
+
256
+ Optional support for [beautiful-traceback](https://github.com/iloveitaly/beautiful-traceback) provides enhanced exception formatting with improved readability, smart coloring, path aliasing (e.g., `<pwd>`, `<site>`), and better alignment. Automatically activates when installed:
257
+
258
+ ```bash
259
+ uv add beautiful-traceback --group dev
260
+ ```
261
+
262
+ No configuration needed - just install and `configure_logger()` will use it automatically.
263
+
122
264
  ## iPython
123
265
 
124
266
  Often it's helpful to update logging level within an iPython session. You can do this and make sure all loggers pick up on it.
@@ -0,0 +1,15 @@
1
+ structlog_config/__init__.py,sha256=wTIMChYHwuzzgqoahCyF6YkJxjIYb-Gpyx8LkPkUpGk,6798
2
+ structlog_config/constants.py,sha256=O1nPnB29yZdqqaI7aeTUrimA3LOtA5WpP6BGPLWJvj8,510
3
+ structlog_config/env_config.py,sha256=_EJO0rgAKndRPSh4wuBaH3bui9F3nIpn8FaEkjAjZso,1737
4
+ structlog_config/environments.py,sha256=JpZYVVDGxEf1EaKdPdn6Jo-4wJK6SqF0ueFl7e2TBvI,612
5
+ structlog_config/fastapi_access_logger.py,sha256=CYZsww0AIcdfrU5Wgr6POwdJxJ5vMB96ttsYqoE30BU,4363
6
+ structlog_config/formatters.py,sha256=KJ3PlkrFva1RKz5SmFmEwVCqJuHxq3Qsqhd9ezB983I,6715
7
+ structlog_config/levels.py,sha256=LqXG4l01mIpxS2qI7PF_Vp9K7IrO0D5qU_x-Uo3LNuM,2372
8
+ structlog_config/packages.py,sha256=xO4wHPIhAwGG6jv0kHdCr9NHpoIFx4VUeRzmztXp2is,591
9
+ structlog_config/pytest_plugin.py,sha256=XBtef1KpuGV_RmXoWf13b0EUy69iAKMAk4-e5ZZnAuM,6819
10
+ structlog_config/stdlib_logging.py,sha256=iHApYdsAs_ZC7Y8NpbKs23AHAJB1qLUCZPvFd-Nf_4I,7381
11
+ structlog_config/trace.py,sha256=esSzTulaVr63H3iOYpHlU-NXPSwpEGS7GEYbv6Iq-3A,2118
12
+ structlog_config/warnings.py,sha256=gKEcuHWqH0BaKitJtQkv-uJ0Z3uCH5nn6k8qpqjR-RM,998
13
+ structlog_config-0.7.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
14
+ structlog_config-0.7.0.dist-info/METADATA,sha256=yjnflk2btwm0xBy-0xRjhe8lwXPmsdToMx2Kn2doylU,10994
15
+ structlog_config-0.7.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- structlog_config/__init__.py,sha256=DyY4x3_dY_hPNbS1aM7JRCGadTa1dYDIPzgrHu3AP68,6733
2
- structlog_config/constants.py,sha256=O1nPnB29yZdqqaI7aeTUrimA3LOtA5WpP6BGPLWJvj8,510
3
- structlog_config/env_config.py,sha256=_EJO0rgAKndRPSh4wuBaH3bui9F3nIpn8FaEkjAjZso,1737
4
- structlog_config/environments.py,sha256=JpZYVVDGxEf1EaKdPdn6Jo-4wJK6SqF0ueFl7e2TBvI,612
5
- structlog_config/fastapi_access_logger.py,sha256=QuvteKBZGsEiVaPEpdpsK1jjCYEPKnhmsQDC89M2JKc,5452
6
- structlog_config/formatters.py,sha256=ll0Y0QeRs1DMmD-ft1n1zA4Vn2STRSK-mOrczYB2OjE,5898
7
- structlog_config/levels.py,sha256=LqXG4l01mIpxS2qI7PF_Vp9K7IrO0D5qU_x-Uo3LNuM,2372
8
- structlog_config/packages.py,sha256=asxrzLR-iRYAbkoSYutyTdIRcruTjHgkzfe2pjm2VFM,519
9
- structlog_config/stdlib_logging.py,sha256=Wnn59oRBIqn708CpR-akqVcG9ccSfCMLh56_7wxZRH0,7350
10
- structlog_config/trace.py,sha256=dBaSynxmw4Wg79wSHqYEMoByvv--v_oQw61dRdg4xUI,2016
11
- structlog_config/warnings.py,sha256=gKEcuHWqH0BaKitJtQkv-uJ0Z3uCH5nn6k8qpqjR-RM,998
12
- structlog_config-0.5.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
13
- structlog_config-0.5.0.dist-info/METADATA,sha256=d1CeOq9mFTIcBP80JKqnGZ_mEHud_5ooaI-6KDn51mM,6329
14
- structlog_config-0.5.0.dist-info/RECORD,,