structlog-config 0.2.0__py3-none-any.whl → 0.4.1__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.
- structlog_config/__init__.py +3 -0
- structlog_config/fastapi_access_logger.py +50 -13
- structlog_config/formatters.py +1 -0
- structlog_config/stdlib_logging.py +7 -1
- {structlog_config-0.2.0.dist-info → structlog_config-0.4.1.dist-info}/METADATA +62 -8
- structlog_config-0.4.1.dist-info/RECORD +14 -0
- structlog_config-0.4.1.dist-info/WHEEL +4 -0
- structlog_config-0.2.0.dist-info/RECORD +0 -14
- structlog_config-0.2.0.dist-info/WHEEL +0 -4
structlog_config/__init__.py
CHANGED
|
@@ -9,6 +9,9 @@ from structlog.processors import ExceptionRenderer
|
|
|
9
9
|
from structlog.tracebacks import ExceptionDictTransformer
|
|
10
10
|
from structlog.typing import FilteringBoundLogger
|
|
11
11
|
|
|
12
|
+
from structlog_config.fastapi_access_logger import (
|
|
13
|
+
client_ip_from_request,
|
|
14
|
+
)
|
|
12
15
|
from structlog_config.formatters import (
|
|
13
16
|
PathPrettifier,
|
|
14
17
|
add_fastapi_context,
|
|
@@ -3,13 +3,16 @@ from urllib.parse import quote
|
|
|
3
3
|
|
|
4
4
|
import structlog
|
|
5
5
|
from fastapi import FastAPI
|
|
6
|
+
from python_ipware import IpWare
|
|
6
7
|
from starlette.middleware.base import RequestResponseEndpoint
|
|
7
8
|
from starlette.requests import Request
|
|
8
9
|
from starlette.responses import Response
|
|
9
10
|
from starlette.routing import Match, Mount
|
|
10
11
|
from starlette.types import Scope
|
|
12
|
+
from starlette.websockets import WebSocket
|
|
11
13
|
|
|
12
14
|
log = structlog.get_logger("access_log")
|
|
15
|
+
ipw = IpWare()
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def get_route_name(app: FastAPI, scope: Scope, prefix: str = "") -> str:
|
|
@@ -47,20 +50,40 @@ def get_path_with_query_string(scope: Scope) -> str:
|
|
|
47
50
|
return path_with_query_string
|
|
48
51
|
|
|
49
52
|
|
|
50
|
-
def
|
|
51
|
-
"""
|
|
53
|
+
def client_ip_from_request(request: Request | WebSocket) -> str | None:
|
|
54
|
+
"""
|
|
55
|
+
Get the client IP address from the request.
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
scope (Scope): Current context.
|
|
57
|
+
Headers are not case-sensitive.
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
Uses ipware library to properly extract client IP from various proxy headers.
|
|
60
|
+
Fallback to direct client connection if no proxy headers found.
|
|
58
61
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
headers = request.headers
|
|
63
|
+
|
|
64
|
+
# TODO this seems really inefficient, we should just rewrite the ipware into this repo :/
|
|
65
|
+
# Convert Starlette headers to format expected by ipware (HTTP_ prefixed)
|
|
66
|
+
# ipware expects headers in WSGI/Django-style meta format where HTTP headers
|
|
67
|
+
# are prefixed with "HTTP_" and dashes become underscores.
|
|
68
|
+
# See: https://github.com/un33k/python-ipware/blob/main/python_ipware/python_ipware.py#L33-L40
|
|
69
|
+
meta_dict = {}
|
|
70
|
+
for name, value in headers.items():
|
|
71
|
+
# Convert header name to HTTP_ prefixed format
|
|
72
|
+
meta_key = f"HTTP_{name.upper().replace('-', '_')}"
|
|
73
|
+
meta_dict[meta_key] = value
|
|
74
|
+
|
|
75
|
+
# Use ipware to extract IP from headers
|
|
76
|
+
ip, trusted_route = ipw.get_client_ip(meta=meta_dict)
|
|
77
|
+
if ip:
|
|
78
|
+
log.debug(
|
|
79
|
+
"extracted client IP from headers", ip=ip, trusted_route=trusted_route
|
|
80
|
+
)
|
|
81
|
+
return str(ip)
|
|
82
|
+
|
|
83
|
+
# Fallback to direct client connection
|
|
84
|
+
host = request.client.host if request.client else None
|
|
85
|
+
|
|
86
|
+
return host
|
|
64
87
|
|
|
65
88
|
|
|
66
89
|
# TODO we should look at the static asset logic and pull the prefix path from tha
|
|
@@ -78,13 +101,27 @@ def is_static_assets_request(scope: Scope) -> bool:
|
|
|
78
101
|
or scope["path"].endswith(".js")
|
|
79
102
|
# .map files are attempted when devtools are enabled
|
|
80
103
|
or scope["path"].endswith(".js.map")
|
|
104
|
+
or scope["path"].endswith(".ico")
|
|
105
|
+
or scope["path"].endswith(".png")
|
|
106
|
+
or scope["path"].endswith(".jpg")
|
|
107
|
+
or scope["path"].endswith(".jpeg")
|
|
108
|
+
or scope["path"].endswith(".gif")
|
|
81
109
|
)
|
|
82
110
|
|
|
83
111
|
|
|
84
112
|
def add_middleware(
|
|
85
113
|
app: FastAPI,
|
|
86
114
|
) -> None:
|
|
87
|
-
"""
|
|
115
|
+
"""
|
|
116
|
+
Add better access logging to fastapi:
|
|
117
|
+
|
|
118
|
+
>>> from structlog_config import fastapi_access_logger
|
|
119
|
+
>>> fastapi_access_logger.add_middleware(app)
|
|
120
|
+
|
|
121
|
+
You'll also want to disable the default uvicorn logs:
|
|
122
|
+
|
|
123
|
+
>>> uvicorn.run(..., log_config=None, access_log=False)
|
|
124
|
+
"""
|
|
88
125
|
|
|
89
126
|
@app.middleware("http")
|
|
90
127
|
async def access_log_middleware(
|
|
@@ -113,7 +150,7 @@ def add_middleware(
|
|
|
113
150
|
method=scope["method"],
|
|
114
151
|
path=scope["path"],
|
|
115
152
|
query=scope["query_string"].decode(),
|
|
116
|
-
client_ip=
|
|
153
|
+
client_ip=client_ip_from_request(request),
|
|
117
154
|
route=route_name,
|
|
118
155
|
)
|
|
119
156
|
|
structlog_config/formatters.py
CHANGED
|
@@ -77,6 +77,7 @@ def pretty_traceback_exception_formatter(sio: TextIO, exc_info: ExcInfo) -> None
|
|
|
77
77
|
from pretty_traceback.formatting import exc_to_traceback_str
|
|
78
78
|
|
|
79
79
|
_, exc_value, traceback = exc_info
|
|
80
|
+
# TODO support local_stack_only env var support
|
|
80
81
|
formatted_exception = exc_to_traceback_str(exc_value, traceback, color=not NO_COLOR)
|
|
81
82
|
sio.write("\n" + formatted_exception)
|
|
82
83
|
|
|
@@ -97,8 +97,8 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
97
97
|
root_logger.propagate = True
|
|
98
98
|
|
|
99
99
|
# TODO there is a JSON-like format that can be used to configure loggers instead :/
|
|
100
|
+
# we should probably transition to using that format instead of this customized mapping
|
|
100
101
|
std_logging_configuration = {
|
|
101
|
-
# "httpcore": {},
|
|
102
102
|
"httpx": {
|
|
103
103
|
"levels": {
|
|
104
104
|
"INFO": "WARNING",
|
|
@@ -109,6 +109,12 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
109
109
|
"INFO": "WARNING",
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
+
# stripe INFO logs are pretty noisy by default
|
|
113
|
+
"stripe": {
|
|
114
|
+
"levels": {
|
|
115
|
+
"INFO": "WARNING",
|
|
116
|
+
}
|
|
117
|
+
},
|
|
112
118
|
}
|
|
113
119
|
"""
|
|
114
120
|
These loggers either:
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: structlog-config
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
+
Author: Michael Bianco
|
|
6
7
|
Author-email: Michael Bianco <mike@mikebian.co>
|
|
7
|
-
Keywords: json-logging,logging,structlog,structured-logging
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
8
|
Requires-Dist: orjson>=3.10.15
|
|
10
9
|
Requires-Dist: python-decouple-typed>=3.11.0
|
|
10
|
+
Requires-Dist: python-ipware>=3.0.0
|
|
11
11
|
Requires-Dist: structlog>=25.2.0
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Project-URL: Repository, https://github.com/iloveitaly/structlog-config
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
|
|
14
16
|
# Opinionated Defaults for Structlog
|
|
@@ -30,12 +32,64 @@ Here are the main goals:
|
|
|
30
32
|
## Usage
|
|
31
33
|
|
|
32
34
|
```python
|
|
33
|
-
from structlog_config import
|
|
35
|
+
from structlog_config import configure_logger
|
|
36
|
+
|
|
37
|
+
log = configure_logger()
|
|
38
|
+
|
|
39
|
+
log.info("the log", key="value")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## JSON Logging for Production
|
|
43
|
+
|
|
44
|
+
JSON logging is automatically enabled in production and staging environments (`PYTHON_ENV=production` or `PYTHON_ENV=staging`):
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from structlog_config import configure_logger
|
|
48
|
+
|
|
49
|
+
# Automatic JSON logging in production
|
|
50
|
+
log = configure_logger()
|
|
51
|
+
log.info("User login", user_id="123", action="login")
|
|
52
|
+
# Output: {"action":"login","event":"User login","level":"info","timestamp":"2025-09-24T18:03:00Z","user_id":"123"}
|
|
53
|
+
|
|
54
|
+
# Force JSON logging regardless of environment
|
|
55
|
+
log = configure_logger(json_logger=True)
|
|
56
|
+
|
|
57
|
+
# Force console logging regardless of environment
|
|
58
|
+
log = configure_logger(json_logger=False)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
JSON logs use [orjson](https://github.com/ijl/orjson) for performance, include sorted keys and ISO timestamps, and serialize exceptions cleanly. Note that `PYTHON_LOG_PATH` is ignored with JSON logging (stdout only).
|
|
62
|
+
|
|
63
|
+
## TRACE Logging Level
|
|
64
|
+
|
|
65
|
+
This package adds support for a custom `TRACE` logging level (level 5) that's even more verbose than `DEBUG`. This is useful for extremely detailed debugging scenarios.
|
|
66
|
+
|
|
67
|
+
The `TRACE` level is automatically set up when you call `configure_logger()`. You can use it like any other logging level:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import logging
|
|
71
|
+
from structlog_config import configure_logger
|
|
72
|
+
|
|
73
|
+
log = configure_logger()
|
|
74
|
+
|
|
75
|
+
# Using structlog
|
|
76
|
+
log.info("This is info")
|
|
77
|
+
log.debug("This is debug")
|
|
78
|
+
log.trace("This is trace") # Most verbose
|
|
79
|
+
|
|
80
|
+
# Using stdlib logging
|
|
81
|
+
logging.trace("Module-level trace message")
|
|
82
|
+
logger = logging.getLogger(__name__)
|
|
83
|
+
logger.trace("Instance trace message")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set the log level to TRACE using the environment variable:
|
|
34
87
|
|
|
35
|
-
|
|
88
|
+
```bash
|
|
89
|
+
LOG_LEVEL=TRACE
|
|
36
90
|
```
|
|
37
91
|
|
|
38
|
-
##
|
|
92
|
+
## Stdlib Log Management
|
|
39
93
|
|
|
40
94
|
By default, all stdlib loggers are:
|
|
41
95
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
structlog_config/__init__.py,sha256=sSO1F6S__4-3FZU1JursNYzudWZd3uT3KndddF-cUck,6816
|
|
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=hHkueC2m-19su5lIEfeJbzMprJ1HeC_WoOKPpaQmHPo,5143
|
|
6
|
+
structlog_config/formatters.py,sha256=ll0Y0QeRs1DMmD-ft1n1zA4Vn2STRSK-mOrczYB2OjE,5898
|
|
7
|
+
structlog_config/levels.py,sha256=z1fTpvCCbAwcFK2k7rHWh_p-FqfFh4yIWCTZ1MNf_4U,993
|
|
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.4.1.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
13
|
+
structlog_config-0.4.1.dist-info/METADATA,sha256=9ZEpVM56Y6ZOXhqwc7YmFIYbB-wVSNfTHwY2mM5wRx8,6329
|
|
14
|
+
structlog_config-0.4.1.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=31tSlpe50joO5uBPAQzUWGfv_Vs_Z-9dMhuq3hwa9iY,3592
|
|
6
|
-
structlog_config/formatters.py,sha256=cprGEjvRFphJixbb0nVCpPn9sfw_Wv4d2vPtKDpM05A,5846
|
|
7
|
-
structlog_config/levels.py,sha256=z1fTpvCCbAwcFK2k7rHWh_p-FqfFh4yIWCTZ1MNf_4U,993
|
|
8
|
-
structlog_config/packages.py,sha256=asxrzLR-iRYAbkoSYutyTdIRcruTjHgkzfe2pjm2VFM,519
|
|
9
|
-
structlog_config/stdlib_logging.py,sha256=oUbd-3joR89hOXTbZxgrsSOyhBMjEyW6h68qQpCXabU,7120
|
|
10
|
-
structlog_config/trace.py,sha256=dBaSynxmw4Wg79wSHqYEMoByvv--v_oQw61dRdg4xUI,2016
|
|
11
|
-
structlog_config/warnings.py,sha256=gKEcuHWqH0BaKitJtQkv-uJ0Z3uCH5nn6k8qpqjR-RM,998
|
|
12
|
-
structlog_config-0.2.0.dist-info/METADATA,sha256=Rq5mcJeFR_0SgIinFLTfSeIXZsGphXDJsvJkJNC-ltY,4606
|
|
13
|
-
structlog_config-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
structlog_config-0.2.0.dist-info/RECORD,,
|