structlog-config 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {structlog_config-0.5.0 → structlog_config-0.6.0}/PKG-INFO +76 -2
- {structlog_config-0.5.0 → structlog_config-0.6.0}/README.md +72 -0
- structlog_config-0.6.0/pyproject.toml +34 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/__init__.py +1 -2
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/fastapi_access_logger.py +6 -33
- structlog_config-0.6.0/structlog_config/pytest_plugin.py +222 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/stdlib_logging.py +8 -5
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/trace.py +4 -2
- structlog_config-0.5.0/pyproject.toml +0 -61
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/constants.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/env_config.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/environments.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/formatters.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/levels.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/packages.py +0 -0
- {structlog_config-0.5.0 → structlog_config-0.6.0}/structlog_config/warnings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: structlog-config
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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-
|
|
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
|
|
@@ -108,6 +122,8 @@ For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.
|
|
|
108
122
|
|
|
109
123
|
## FastAPI Access Logger
|
|
110
124
|
|
|
125
|
+
**Note:** Requires `pip install structlog-config[fastapi]` for FastAPI dependencies.
|
|
126
|
+
|
|
111
127
|
Structured, simple access log with request timing to replace the default fastapi access log. Why?
|
|
112
128
|
|
|
113
129
|
1. It's less verbose
|
|
@@ -119,6 +135,64 @@ Here's how to use it:
|
|
|
119
135
|
1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
|
|
120
136
|
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
137
|
|
|
138
|
+
## Pytest Plugin: Capture Logs on Failure
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
### Features
|
|
143
|
+
|
|
144
|
+
- Only shows logs for failing tests (keeps output clean)
|
|
145
|
+
- Captures logs from all test phases (setup, call, teardown)
|
|
146
|
+
- Unique log file per test
|
|
147
|
+
- Optional persistent log storage for debugging
|
|
148
|
+
- Automatically handles `PYTHON_LOG_PATH` environment variable
|
|
149
|
+
|
|
150
|
+
### Usage
|
|
151
|
+
|
|
152
|
+
Enable the plugin with the `--capture-logs-on-fail` flag:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pytest --capture-logs-on-fail
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or enable it permanently in `pytest.ini` or `pyproject.toml`:
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
[tool.pytest.ini_options]
|
|
162
|
+
addopts = ["--capture-logs-on-fail"]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Persist Logs to Directory
|
|
166
|
+
|
|
167
|
+
To keep all test logs for later inspection (useful for CI/CD debugging):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pytest --capture-logs-dir=./test-logs
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This creates a log file for each test and disables automatic cleanup.
|
|
174
|
+
|
|
175
|
+
### How It Works
|
|
176
|
+
|
|
177
|
+
1. Sets `PYTHON_LOG_PATH` environment variable to a unique temp file for each test
|
|
178
|
+
2. Your application logs (via `configure_logger()`) write to this file
|
|
179
|
+
3. On test failure, the plugin prints the captured logs to stdout
|
|
180
|
+
4. Log files are cleaned up after the test session (unless `--capture-logs-dir` is used)
|
|
181
|
+
|
|
182
|
+
### Example Output
|
|
183
|
+
|
|
184
|
+
When a test fails, you'll see:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
FAILED tests/test_user.py::test_user_login
|
|
188
|
+
|
|
189
|
+
--- Captured logs for failed test (call): tests/test_user.py::test_user_login ---
|
|
190
|
+
2025-11-01 18:30:00 [info] User login started user_id=123
|
|
191
|
+
2025-11-01 18:30:01 [error] Database connection failed timeout=5.0
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
For passing tests, no log output is shown, keeping your test output clean and focused.
|
|
195
|
+
|
|
122
196
|
## iPython
|
|
123
197
|
|
|
124
198
|
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.
|
|
@@ -14,6 +14,18 @@ Here are the main goals:
|
|
|
14
14
|
* Ability to log level and output (i.e. file path) *by logger* for easy development debugging
|
|
15
15
|
* If you are using fastapi, structured logging for access logs
|
|
16
16
|
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install structlog-config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add structlog-config
|
|
27
|
+
```
|
|
28
|
+
|
|
17
29
|
## Usage
|
|
18
30
|
|
|
19
31
|
```python
|
|
@@ -93,6 +105,8 @@ For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.
|
|
|
93
105
|
|
|
94
106
|
## FastAPI Access Logger
|
|
95
107
|
|
|
108
|
+
**Note:** Requires `pip install structlog-config[fastapi]` for FastAPI dependencies.
|
|
109
|
+
|
|
96
110
|
Structured, simple access log with request timing to replace the default fastapi access log. Why?
|
|
97
111
|
|
|
98
112
|
1. It's less verbose
|
|
@@ -104,6 +118,64 @@ Here's how to use it:
|
|
|
104
118
|
1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
|
|
105
119
|
2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
|
|
106
120
|
|
|
121
|
+
## Pytest Plugin: Capture Logs on Failure
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
### Features
|
|
126
|
+
|
|
127
|
+
- Only shows logs for failing tests (keeps output clean)
|
|
128
|
+
- Captures logs from all test phases (setup, call, teardown)
|
|
129
|
+
- Unique log file per test
|
|
130
|
+
- Optional persistent log storage for debugging
|
|
131
|
+
- Automatically handles `PYTHON_LOG_PATH` environment variable
|
|
132
|
+
|
|
133
|
+
### Usage
|
|
134
|
+
|
|
135
|
+
Enable the plugin with the `--capture-logs-on-fail` flag:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pytest --capture-logs-on-fail
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Or enable it permanently in `pytest.ini` or `pyproject.toml`:
|
|
142
|
+
|
|
143
|
+
```toml
|
|
144
|
+
[tool.pytest.ini_options]
|
|
145
|
+
addopts = ["--capture-logs-on-fail"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Persist Logs to Directory
|
|
149
|
+
|
|
150
|
+
To keep all test logs for later inspection (useful for CI/CD debugging):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pytest --capture-logs-dir=./test-logs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This creates a log file for each test and disables automatic cleanup.
|
|
157
|
+
|
|
158
|
+
### How It Works
|
|
159
|
+
|
|
160
|
+
1. Sets `PYTHON_LOG_PATH` environment variable to a unique temp file for each test
|
|
161
|
+
2. Your application logs (via `configure_logger()`) write to this file
|
|
162
|
+
3. On test failure, the plugin prints the captured logs to stdout
|
|
163
|
+
4. Log files are cleaned up after the test session (unless `--capture-logs-dir` is used)
|
|
164
|
+
|
|
165
|
+
### Example Output
|
|
166
|
+
|
|
167
|
+
When a test fails, you'll see:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
FAILED tests/test_user.py::test_user_login
|
|
171
|
+
|
|
172
|
+
--- Captured logs for failed test (call): tests/test_user.py::test_user_login ---
|
|
173
|
+
2025-11-01 18:30:00 [info] User login started user_id=123
|
|
174
|
+
2025-11-01 18:30:01 [error] Database connection failed timeout=5.0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For passing tests, no log output is shown, keeping your test output clean and focused.
|
|
178
|
+
|
|
107
179
|
## iPython
|
|
108
180
|
|
|
109
181
|
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,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "structlog-config"
|
|
3
|
+
version = "0.6.0"
|
|
4
|
+
description = "A comprehensive structlog configuration with sensible defaults for development and production environments, featuring context management, exception formatting, and path prettification."
|
|
5
|
+
keywords = ["logging", "structlog", "json-logging", "structured-logging"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"orjson>=3.10.15",
|
|
10
|
+
"python-decouple-typed>=3.11.0",
|
|
11
|
+
"python-ipware>=3.0.0",
|
|
12
|
+
"structlog>=25.2.0",
|
|
13
|
+
]
|
|
14
|
+
urls = { "Repository" = "https://github.com/iloveitaly/structlog-config" }
|
|
15
|
+
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
fastapi = ["fastapi-ipware>=0.1.0"]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.8.11,<0.9.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[tool.uv.build-backend]
|
|
25
|
+
# avoids the src/ directory structure
|
|
26
|
+
module-root = ""
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"fastapi>=0.115.12",
|
|
31
|
+
"httpx>=0.28.1",
|
|
32
|
+
"pytest>=8.3.3",
|
|
33
|
+
"fastapi_ipware>=0.1.0",
|
|
34
|
+
]
|
|
@@ -45,8 +45,7 @@ def log_processors_for_mode(json_logger: bool) -> list[structlog.types.Processor
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
return [
|
|
48
|
-
#
|
|
49
|
-
structlog.processors.format_exc_info,
|
|
48
|
+
# omit `structlog.processors.format_exc_info` so we can use structured logging for exceptions
|
|
50
49
|
# simple, short exception rendering in prod since sentry is in place
|
|
51
50
|
# https://www.structlog.org/en/stable/exceptions.html this is a customized version of dict_tracebacks
|
|
52
51
|
ExceptionRenderer(
|
|
@@ -7,7 +7,7 @@ from urllib.parse import quote
|
|
|
7
7
|
|
|
8
8
|
import structlog
|
|
9
9
|
from fastapi import FastAPI
|
|
10
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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=[
|
|
@@ -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,61 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "structlog-config"
|
|
3
|
-
version = "0.5.0"
|
|
4
|
-
description = "A comprehensive structlog configuration with sensible defaults for development and production environments, featuring context management, exception formatting, and path prettification."
|
|
5
|
-
keywords = ["logging", "structlog", "json-logging", "structured-logging"]
|
|
6
|
-
readme = "README.md"
|
|
7
|
-
requires-python = ">=3.10"
|
|
8
|
-
dependencies = [
|
|
9
|
-
"orjson>=3.10.15",
|
|
10
|
-
"python-decouple-typed>=3.11.0",
|
|
11
|
-
"python-ipware>=3.0.0",
|
|
12
|
-
"structlog>=25.2.0",
|
|
13
|
-
]
|
|
14
|
-
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
15
|
-
urls = { "Repository" = "https://github.com/iloveitaly/structlog-config" }
|
|
16
|
-
|
|
17
|
-
[build-system]
|
|
18
|
-
requires = ["uv_build>=0.8.11,<0.9.0"]
|
|
19
|
-
build-backend = "uv_build"
|
|
20
|
-
|
|
21
|
-
[tool.uv.build-backend]
|
|
22
|
-
# avoids the src/ directory structure
|
|
23
|
-
module-root = ""
|
|
24
|
-
|
|
25
|
-
[dependency-groups]
|
|
26
|
-
debugging-extras = [
|
|
27
|
-
"colorama>=0.4.6",
|
|
28
|
-
"datamodel-code-generator>=0.28.5",
|
|
29
|
-
"debugpy>=1.8.13",
|
|
30
|
-
"docrepr>=0.2.0",
|
|
31
|
-
"funcy-pipe>=0.11.1",
|
|
32
|
-
"httpdbg>=1.1.2",
|
|
33
|
-
"icecream>=2.1.4",
|
|
34
|
-
"ipdb",
|
|
35
|
-
"ipython>=8.34.0",
|
|
36
|
-
"ipython-autoimport>=0.5.1",
|
|
37
|
-
"ipython-ctrlr-fzf>=0.2.1",
|
|
38
|
-
"ipython-playground>=0.2.0",
|
|
39
|
-
"ipython-suggestions",
|
|
40
|
-
"ipythonclipboard>=1.0b2",
|
|
41
|
-
"jedi>=0.19.2",
|
|
42
|
-
"pdbr[ipython]>=0.9.0",
|
|
43
|
-
"pipdeptree>=2.26.0",
|
|
44
|
-
"pre-commit>=4.2.0",
|
|
45
|
-
"pretty-traceback",
|
|
46
|
-
"pudb>=2024.1.3",
|
|
47
|
-
"py-spy>=0.4.0",
|
|
48
|
-
"pyfzf>=0.3.1",
|
|
49
|
-
"pytest-fzf>=0.1.2.post1",
|
|
50
|
-
"rich>=14.0.0",
|
|
51
|
-
"rpdb>=0.2.0",
|
|
52
|
-
"sqlparse>=0.5.3",
|
|
53
|
-
"uv-development-toggle>=0.4.0",
|
|
54
|
-
]
|
|
55
|
-
dev = ["fastapi>=0.115.12", "httpx>=0.28.1", "pytest>=8.3.3"]
|
|
56
|
-
|
|
57
|
-
[tool.uv.sources]
|
|
58
|
-
ipdb = { git = "https://github.com/iloveitaly/ipdb", rev = "support-executables" }
|
|
59
|
-
pdbr = { git = "https://github.com/iloveitaly/pdbr", rev = "ipython-9.x" }
|
|
60
|
-
pretty-traceback = { git = "https://github.com/iloveitaly/pretty-traceback.git", rev = "custom" }
|
|
61
|
-
ipython-suggestions = { git = "https://github.com/iloveitaly/ipython-suggestions.git", rev = "ipython-9.x" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|