structlog-config 0.1.0__tar.gz → 0.4.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.4.0/PKG-INFO +135 -0
- structlog_config-0.4.0/README.md +120 -0
- {structlog_config-0.1.0 → structlog_config-0.4.0}/pyproject.toml +8 -3
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/__init__.py +23 -10
- structlog_config-0.4.0/structlog_config/constants.py +16 -0
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/env_config.py +1 -1
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/fastapi_access_logger.py +61 -3
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/formatters.py +1 -0
- structlog_config-0.4.0/structlog_config/levels.py +35 -0
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/stdlib_logging.py +70 -39
- structlog_config-0.4.0/structlog_config/trace.py +62 -0
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/warnings.py +6 -16
- structlog_config-0.1.0/.copier-answers.yml +0 -2
- structlog_config-0.1.0/.envrc +0 -11
- structlog_config-0.1.0/.github/dependabot.yml +0 -12
- structlog_config-0.1.0/.github/workflows/build_and_publish.yml +0 -56
- structlog_config-0.1.0/.github/workflows/repo-sync.yml +0 -16
- structlog_config-0.1.0/.gitignore +0 -129
- structlog_config-0.1.0/.tool-versions +0 -3
- structlog_config-0.1.0/.vscode/settings.json +0 -36
- structlog_config-0.1.0/CHANGELOG.md +0 -30
- structlog_config-0.1.0/Makefile +0 -11
- structlog_config-0.1.0/PKG-INFO +0 -62
- structlog_config-0.1.0/README.md +0 -49
- structlog_config-0.1.0/copier.yml +0 -68
- structlog_config-0.1.0/structlog_config/constants.py +0 -9
- structlog_config-0.1.0/tests/__init__.py +0 -0
- structlog_config-0.1.0/tests/capture_utils.py +0 -53
- structlog_config-0.1.0/tests/conftest.py +0 -114
- structlog_config-0.1.0/tests/test_environment.py +0 -118
- structlog_config-0.1.0/tests/test_fastapi_access_logger.py +0 -189
- structlog_config-0.1.0/tests/test_import.py +0 -8
- structlog_config-0.1.0/tests/test_logging.py +0 -167
- structlog_config-0.1.0/tests/utils.py +0 -35
- structlog_config-0.1.0/uv.lock +0 -1680
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/environments.py +0 -0
- {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/packages.py +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: structlog-config
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: 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
|
+
Author: Michael Bianco
|
|
7
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
+
Requires-Dist: orjson>=3.10.15
|
|
9
|
+
Requires-Dist: python-decouple-typed>=3.11.0
|
|
10
|
+
Requires-Dist: python-ipware>=3.0.0
|
|
11
|
+
Requires-Dist: structlog>=25.2.0
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Project-URL: Repository, https://github.com/iloveitaly/structlog-config
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Opinionated Defaults for Structlog
|
|
17
|
+
|
|
18
|
+
Logging is really important. Getting logging to work well in python feels like black magic: there's a ton of configuration
|
|
19
|
+
across structlog, warnings, std loggers, fastapi + celery context, JSON logging in production, etc that requires lots of
|
|
20
|
+
fiddling and testing to get working. I finally got this working for me in my [project template](https://github.com/iloveitaly/python-starter-template) and extracted this out into a nice package.
|
|
21
|
+
|
|
22
|
+
Here are the main goals:
|
|
23
|
+
|
|
24
|
+
* High performance JSON logging in production
|
|
25
|
+
* All loggers, even plugin or system loggers, should route through the same formatter
|
|
26
|
+
* Structured logging everywhere
|
|
27
|
+
* Ability to easily set thread-local log context
|
|
28
|
+
* Nice log formatters for stack traces, ORM ([ActiveModel/SQLModel](https://github.com/iloveitaly/activemodel)), etc
|
|
29
|
+
* Ability to log level and output (i.e. file path) *by logger* for easy development debugging
|
|
30
|
+
* If you are using fastapi, structured logging for access logs
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from structlog_config import configure_logger
|
|
36
|
+
|
|
37
|
+
log = configure_logger()
|
|
38
|
+
|
|
39
|
+
log.info("the log", key="value")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## TRACE Logging Level
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
The `TRACE` level is automatically set up when you call `configure_logger()`. You can use it like any other logging level:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import logging
|
|
50
|
+
from structlog_config import configure_logger
|
|
51
|
+
|
|
52
|
+
log = configure_logger()
|
|
53
|
+
|
|
54
|
+
# Using structlog
|
|
55
|
+
log.info("This is info")
|
|
56
|
+
log.debug("This is debug")
|
|
57
|
+
log.trace("This is trace") # Most verbose
|
|
58
|
+
|
|
59
|
+
# Using stdlib logging
|
|
60
|
+
logging.trace("Module-level trace message")
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
logger.trace("Instance trace message")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set the log level to TRACE using the environment variable:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
LOG_LEVEL=TRACE
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Stdlib Log Management
|
|
72
|
+
|
|
73
|
+
By default, all stdlib loggers are:
|
|
74
|
+
|
|
75
|
+
1. Given the same global logging level, with some default adjustments for noisy loggers (looking at you, `httpx`)
|
|
76
|
+
2. Use a structlog formatter (you get structured logging, context, etc in any stdlib logger calls)
|
|
77
|
+
3. The root processor is overwritten so any child loggers created after initialization will use the same formatter
|
|
78
|
+
|
|
79
|
+
You can customize loggers by name (i.e. the name used in `logging.getLogger(__name__)`) using ENV variables.
|
|
80
|
+
|
|
81
|
+
For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16):
|
|
82
|
+
|
|
83
|
+
* `LOG_LEVEL_OPENAI=DEBUG`
|
|
84
|
+
* `LOG_PATH_OPENAI=tmp/openai.log`
|
|
85
|
+
* `LOG_LEVEL_HTTPX=DEBUG`
|
|
86
|
+
* `LOG_PATH_HTTPX=tmp/openai.log`
|
|
87
|
+
|
|
88
|
+
## FastAPI Access Logger
|
|
89
|
+
|
|
90
|
+
Structured, simple access log with request timing to replace the default fastapi access log. Why?
|
|
91
|
+
|
|
92
|
+
1. It's less verbose
|
|
93
|
+
2. Uses structured logging params instead of string interpolation
|
|
94
|
+
3. debug level logs any static assets
|
|
95
|
+
|
|
96
|
+
Here's how to use it:
|
|
97
|
+
|
|
98
|
+
1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
|
|
99
|
+
2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
|
|
100
|
+
|
|
101
|
+
## iPython
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
%env LOG_LEVEL=DEBUG
|
|
107
|
+
from structlog_config import configure_logger
|
|
108
|
+
configure_logger()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Related Projects
|
|
112
|
+
|
|
113
|
+
* https://github.com/underyx/structlog-pretty
|
|
114
|
+
* https://pypi.org/project/httpx-structlog/
|
|
115
|
+
|
|
116
|
+
## References
|
|
117
|
+
|
|
118
|
+
General logging:
|
|
119
|
+
|
|
120
|
+
- https://github.com/replicate/cog/blob/2e57549e18e044982bd100e286a1929f50880383/python/cog/logging.py#L20
|
|
121
|
+
- https://github.com/apache/airflow/blob/4280b83977cd5a53c2b24143f3c9a6a63e298acc/task_sdk/src/airflow/sdk/log.py#L187
|
|
122
|
+
- https://github.com/kiwicom/structlog-sentry
|
|
123
|
+
- https://github.com/jeremyh/datacube-explorer/blob/b289b0cde0973a38a9d50233fe0fff00e8eb2c8e/cubedash/logs.py#L40C21-L40C42
|
|
124
|
+
- https://stackoverflow.com/questions/76256249/logging-in-the-open-ai-python-library/78214464#78214464
|
|
125
|
+
- https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16
|
|
126
|
+
- https://www.python-httpx.org/logging/
|
|
127
|
+
|
|
128
|
+
FastAPI access logger:
|
|
129
|
+
|
|
130
|
+
- https://github.com/iloveitaly/fastapi-logger/blob/main/fastapi_structlog/middleware/access_log.py#L70
|
|
131
|
+
- https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/timing.py
|
|
132
|
+
- https://pypi.org/project/fastapi-structlog/
|
|
133
|
+
- https://pypi.org/project/asgi-correlation-id/
|
|
134
|
+
- https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
|
|
135
|
+
- https://github.com/sharu1204/fastapi-structlog/blob/master/app/main.py
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Opinionated Defaults for Structlog
|
|
2
|
+
|
|
3
|
+
Logging is really important. Getting logging to work well in python feels like black magic: there's a ton of configuration
|
|
4
|
+
across structlog, warnings, std loggers, fastapi + celery context, JSON logging in production, etc that requires lots of
|
|
5
|
+
fiddling and testing to get working. I finally got this working for me in my [project template](https://github.com/iloveitaly/python-starter-template) and extracted this out into a nice package.
|
|
6
|
+
|
|
7
|
+
Here are the main goals:
|
|
8
|
+
|
|
9
|
+
* High performance JSON logging in production
|
|
10
|
+
* All loggers, even plugin or system loggers, should route through the same formatter
|
|
11
|
+
* Structured logging everywhere
|
|
12
|
+
* Ability to easily set thread-local log context
|
|
13
|
+
* Nice log formatters for stack traces, ORM ([ActiveModel/SQLModel](https://github.com/iloveitaly/activemodel)), etc
|
|
14
|
+
* Ability to log level and output (i.e. file path) *by logger* for easy development debugging
|
|
15
|
+
* If you are using fastapi, structured logging for access logs
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from structlog_config import configure_logger
|
|
21
|
+
|
|
22
|
+
log = configure_logger()
|
|
23
|
+
|
|
24
|
+
log.info("the log", key="value")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## TRACE Logging Level
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
The `TRACE` level is automatically set up when you call `configure_logger()`. You can use it like any other logging level:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import logging
|
|
35
|
+
from structlog_config import configure_logger
|
|
36
|
+
|
|
37
|
+
log = configure_logger()
|
|
38
|
+
|
|
39
|
+
# Using structlog
|
|
40
|
+
log.info("This is info")
|
|
41
|
+
log.debug("This is debug")
|
|
42
|
+
log.trace("This is trace") # Most verbose
|
|
43
|
+
|
|
44
|
+
# Using stdlib logging
|
|
45
|
+
logging.trace("Module-level trace message")
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
logger.trace("Instance trace message")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Set the log level to TRACE using the environment variable:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
LOG_LEVEL=TRACE
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Stdlib Log Management
|
|
57
|
+
|
|
58
|
+
By default, all stdlib loggers are:
|
|
59
|
+
|
|
60
|
+
1. Given the same global logging level, with some default adjustments for noisy loggers (looking at you, `httpx`)
|
|
61
|
+
2. Use a structlog formatter (you get structured logging, context, etc in any stdlib logger calls)
|
|
62
|
+
3. The root processor is overwritten so any child loggers created after initialization will use the same formatter
|
|
63
|
+
|
|
64
|
+
You can customize loggers by name (i.e. the name used in `logging.getLogger(__name__)`) using ENV variables.
|
|
65
|
+
|
|
66
|
+
For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16):
|
|
67
|
+
|
|
68
|
+
* `LOG_LEVEL_OPENAI=DEBUG`
|
|
69
|
+
* `LOG_PATH_OPENAI=tmp/openai.log`
|
|
70
|
+
* `LOG_LEVEL_HTTPX=DEBUG`
|
|
71
|
+
* `LOG_PATH_HTTPX=tmp/openai.log`
|
|
72
|
+
|
|
73
|
+
## FastAPI Access Logger
|
|
74
|
+
|
|
75
|
+
Structured, simple access log with request timing to replace the default fastapi access log. Why?
|
|
76
|
+
|
|
77
|
+
1. It's less verbose
|
|
78
|
+
2. Uses structured logging params instead of string interpolation
|
|
79
|
+
3. debug level logs any static assets
|
|
80
|
+
|
|
81
|
+
Here's how to use it:
|
|
82
|
+
|
|
83
|
+
1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
|
|
84
|
+
2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
|
|
85
|
+
|
|
86
|
+
## iPython
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
%env LOG_LEVEL=DEBUG
|
|
92
|
+
from structlog_config import configure_logger
|
|
93
|
+
configure_logger()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Related Projects
|
|
97
|
+
|
|
98
|
+
* https://github.com/underyx/structlog-pretty
|
|
99
|
+
* https://pypi.org/project/httpx-structlog/
|
|
100
|
+
|
|
101
|
+
## References
|
|
102
|
+
|
|
103
|
+
General logging:
|
|
104
|
+
|
|
105
|
+
- https://github.com/replicate/cog/blob/2e57549e18e044982bd100e286a1929f50880383/python/cog/logging.py#L20
|
|
106
|
+
- https://github.com/apache/airflow/blob/4280b83977cd5a53c2b24143f3c9a6a63e298acc/task_sdk/src/airflow/sdk/log.py#L187
|
|
107
|
+
- https://github.com/kiwicom/structlog-sentry
|
|
108
|
+
- https://github.com/jeremyh/datacube-explorer/blob/b289b0cde0973a38a9d50233fe0fff00e8eb2c8e/cubedash/logs.py#L40C21-L40C42
|
|
109
|
+
- https://stackoverflow.com/questions/76256249/logging-in-the-open-ai-python-library/78214464#78214464
|
|
110
|
+
- https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16
|
|
111
|
+
- https://www.python-httpx.org/logging/
|
|
112
|
+
|
|
113
|
+
FastAPI access logger:
|
|
114
|
+
|
|
115
|
+
- https://github.com/iloveitaly/fastapi-logger/blob/main/fastapi_structlog/middleware/access_log.py#L70
|
|
116
|
+
- https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/timing.py
|
|
117
|
+
- https://pypi.org/project/fastapi-structlog/
|
|
118
|
+
- https://pypi.org/project/asgi-correlation-id/
|
|
119
|
+
- https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
|
|
120
|
+
- https://github.com/sharu1204/fastapi-structlog/blob/master/app/main.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "structlog-config"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "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
|
readme = "README.md"
|
|
@@ -8,14 +8,19 @@ requires-python = ">=3.10"
|
|
|
8
8
|
dependencies = [
|
|
9
9
|
"orjson>=3.10.15",
|
|
10
10
|
"python-decouple-typed>=3.11.0",
|
|
11
|
+
"python-ipware>=3.0.0",
|
|
11
12
|
"structlog>=25.2.0",
|
|
12
13
|
]
|
|
13
14
|
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
14
15
|
urls = { "Repository" = "https://github.com/iloveitaly/structlog-config" }
|
|
15
16
|
|
|
16
17
|
[build-system]
|
|
17
|
-
requires = ["
|
|
18
|
-
build-backend = "
|
|
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 = ""
|
|
19
24
|
|
|
20
25
|
[dependency-groups]
|
|
21
26
|
debugging-extras = [
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Protocol
|
|
1
|
+
from contextlib import _GeneratorContextManager
|
|
2
|
+
from typing import Generator, Protocol
|
|
3
3
|
|
|
4
4
|
import orjson
|
|
5
5
|
import structlog
|
|
6
6
|
import structlog.dev
|
|
7
|
+
from decouple import config
|
|
7
8
|
from structlog.processors import ExceptionRenderer
|
|
8
9
|
from structlog.tracebacks import ExceptionDictTransformer
|
|
9
10
|
from structlog.typing import FilteringBoundLogger
|
|
@@ -16,17 +17,19 @@ from structlog_config.formatters import (
|
|
|
16
17
|
simplify_activemodel_objects,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
|
-
from . import
|
|
20
|
-
|
|
20
|
+
from . import (
|
|
21
|
+
packages,
|
|
22
|
+
trace, # noqa: F401
|
|
23
|
+
)
|
|
24
|
+
from .constants import NO_COLOR, package_logger
|
|
21
25
|
from .environments import is_production, is_pytest, is_staging
|
|
26
|
+
from .levels import get_environment_log_level_as_string
|
|
22
27
|
from .stdlib_logging import (
|
|
23
|
-
get_environment_log_level_as_string,
|
|
24
28
|
redirect_stdlib_loggers,
|
|
25
29
|
)
|
|
30
|
+
from .trace import setup_trace
|
|
26
31
|
from .warnings import redirect_showwarnings
|
|
27
32
|
|
|
28
|
-
package_logger = logging.getLogger(__name__)
|
|
29
|
-
|
|
30
33
|
|
|
31
34
|
def log_processors_for_mode(json_logger: bool) -> list[structlog.types.Processor]:
|
|
32
35
|
if json_logger:
|
|
@@ -99,11 +102,19 @@ def _logger_factory(json_logger: bool):
|
|
|
99
102
|
In production, optimized for speed (https://www.structlog.org/en/stable/performance.html)
|
|
100
103
|
"""
|
|
101
104
|
|
|
105
|
+
# avoid a constant for this ENV so we can mutate within tests
|
|
106
|
+
python_log_path = config("PYTHON_LOG_PATH", default=None)
|
|
107
|
+
|
|
102
108
|
if json_logger:
|
|
109
|
+
# TODO I guess we could support this, but the assumption is stdout is going to be used in prod environments
|
|
110
|
+
if python_log_path:
|
|
111
|
+
package_logger.warning(
|
|
112
|
+
"PYTHON_LOG_PATH is not supported with a JSON logger, forcing stdout"
|
|
113
|
+
)
|
|
103
114
|
return structlog.BytesLoggerFactory()
|
|
104
115
|
|
|
105
|
-
if
|
|
106
|
-
python_log = open(
|
|
116
|
+
if python_log_path:
|
|
117
|
+
python_log = open(python_log_path, "a", encoding="utf-8")
|
|
107
118
|
return structlog.PrintLoggerFactory(file=python_log)
|
|
108
119
|
|
|
109
120
|
# Default case
|
|
@@ -118,7 +129,7 @@ class LoggerWithContext(FilteringBoundLogger, Protocol):
|
|
|
118
129
|
want to replicate.
|
|
119
130
|
"""
|
|
120
131
|
|
|
121
|
-
def context(self, *args, **kwargs) -> None:
|
|
132
|
+
def context(self, *args, **kwargs) -> _GeneratorContextManager[None, None, None]:
|
|
122
133
|
"context manager to temporarily set and clear logging context"
|
|
123
134
|
...
|
|
124
135
|
|
|
@@ -158,6 +169,8 @@ def configure_logger(
|
|
|
158
169
|
json_logger: Optional flag to use JSON logging. If None, defaults to
|
|
159
170
|
production or staging environment sourced from PYTHON_ENV.
|
|
160
171
|
"""
|
|
172
|
+
setup_trace()
|
|
173
|
+
|
|
161
174
|
# Reset structlog configuration to make sure we're starting fresh
|
|
162
175
|
# This is important for tests where configure_logger might be called multiple times
|
|
163
176
|
structlog.reset_defaults()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from decouple import config
|
|
5
|
+
|
|
6
|
+
PYTHONASYNCIODEBUG = config("PYTHONASYNCIODEBUG", default=False, cast=bool)
|
|
7
|
+
"this is a builtin env var, we check for it to ensure we don't silence this log level"
|
|
8
|
+
|
|
9
|
+
NO_COLOR = "NO_COLOR" in os.environ
|
|
10
|
+
"support NO_COLOR standard https://no-color.org"
|
|
11
|
+
|
|
12
|
+
package_logger = logging.getLogger(__name__)
|
|
13
|
+
"strange name to not be confused with all of the log-related names floating around"
|
|
14
|
+
|
|
15
|
+
TRACE_LOG_LEVEL = 5
|
|
16
|
+
"Custom log level for trace logging, lower than DEBUG"
|
|
@@ -10,7 +10,7 @@ LOG_LEVEL_PATTERN = re.compile(r"^LOG_LEVEL_(.+)$")
|
|
|
10
10
|
LOG_PATH_PATTERN = re.compile(r"^LOG_PATH_(.+)$")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def get_custom_logger_config() -> dict[str, dict[str, str]]:
|
|
14
14
|
"""
|
|
15
15
|
Parse environment variables to extract custom logger configurations.
|
|
16
16
|
|
|
@@ -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:
|
|
@@ -63,6 +66,42 @@ def get_client_addr(scope: Scope) -> str:
|
|
|
63
66
|
return f"{ip}:{port}"
|
|
64
67
|
|
|
65
68
|
|
|
69
|
+
def client_ip_from_request(request: Request | WebSocket) -> str | None:
|
|
70
|
+
"""
|
|
71
|
+
Get the client IP address from the request.
|
|
72
|
+
|
|
73
|
+
Headers are not case-sensitive.
|
|
74
|
+
|
|
75
|
+
Uses ipware library to properly extract client IP from various proxy headers.
|
|
76
|
+
Fallback to direct client connection if no proxy headers found.
|
|
77
|
+
"""
|
|
78
|
+
headers = request.headers
|
|
79
|
+
|
|
80
|
+
# TODO this seems really inefficient, we should just rewrite the ipware into this repo :/
|
|
81
|
+
# Convert Starlette headers to format expected by ipware (HTTP_ prefixed)
|
|
82
|
+
# ipware expects headers in WSGI/Django-style meta format where HTTP headers
|
|
83
|
+
# are prefixed with "HTTP_" and dashes become underscores.
|
|
84
|
+
# See: https://github.com/un33k/python-ipware/blob/main/python_ipware/python_ipware.py#L33-L40
|
|
85
|
+
meta_dict = {}
|
|
86
|
+
for name, value in headers.items():
|
|
87
|
+
# Convert header name to HTTP_ prefixed format
|
|
88
|
+
meta_key = f"HTTP_{name.upper().replace('-', '_')}"
|
|
89
|
+
meta_dict[meta_key] = value
|
|
90
|
+
|
|
91
|
+
# Use ipware to extract IP from headers
|
|
92
|
+
ip, trusted_route = ipw.get_client_ip(meta=meta_dict)
|
|
93
|
+
if ip:
|
|
94
|
+
log.debug(
|
|
95
|
+
"extracted client IP from headers", ip=ip, trusted_route=trusted_route
|
|
96
|
+
)
|
|
97
|
+
return str(ip)
|
|
98
|
+
|
|
99
|
+
# Fallback to direct client connection
|
|
100
|
+
host = request.client.host if request.client else None
|
|
101
|
+
|
|
102
|
+
return host
|
|
103
|
+
|
|
104
|
+
|
|
66
105
|
# TODO we should look at the static asset logic and pull the prefix path from tha
|
|
67
106
|
def is_static_assets_request(scope: Scope) -> bool:
|
|
68
107
|
"""Check if the request is for static assets. Pretty naive check.
|
|
@@ -73,13 +112,32 @@ def is_static_assets_request(scope: Scope) -> bool:
|
|
|
73
112
|
Returns:
|
|
74
113
|
bool: True if the request is for static assets, False otherwise.
|
|
75
114
|
"""
|
|
76
|
-
return
|
|
115
|
+
return (
|
|
116
|
+
scope["path"].endswith(".css")
|
|
117
|
+
or scope["path"].endswith(".js")
|
|
118
|
+
# .map files are attempted when devtools are enabled
|
|
119
|
+
or scope["path"].endswith(".js.map")
|
|
120
|
+
or scope["path"].endswith(".ico")
|
|
121
|
+
or scope["path"].endswith(".png")
|
|
122
|
+
or scope["path"].endswith(".jpg")
|
|
123
|
+
or scope["path"].endswith(".jpeg")
|
|
124
|
+
or scope["path"].endswith(".gif")
|
|
125
|
+
)
|
|
77
126
|
|
|
78
127
|
|
|
79
128
|
def add_middleware(
|
|
80
129
|
app: FastAPI,
|
|
81
130
|
) -> None:
|
|
82
|
-
"""
|
|
131
|
+
"""
|
|
132
|
+
Add better access logging to fastapi:
|
|
133
|
+
|
|
134
|
+
>>> from structlog_config import fastapi_access_logger
|
|
135
|
+
>>> fastapi_access_logger.add_middleware(app)
|
|
136
|
+
|
|
137
|
+
You'll also want to disable the default uvicorn logs:
|
|
138
|
+
|
|
139
|
+
>>> uvicorn.run(..., log_config=None, access_log=False)
|
|
140
|
+
"""
|
|
83
141
|
|
|
84
142
|
@app.middleware("http")
|
|
85
143
|
async def access_log_middleware(
|
|
@@ -108,7 +166,7 @@ def add_middleware(
|
|
|
108
166
|
method=scope["method"],
|
|
109
167
|
path=scope["path"],
|
|
110
168
|
query=scope["query_string"].decode(),
|
|
111
|
-
client_ip=
|
|
169
|
+
client_ip=client_ip_from_request(request),
|
|
112
170
|
route=route_name,
|
|
113
171
|
)
|
|
114
172
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from decouple import config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_environment_log_level_as_string() -> str:
|
|
7
|
+
level = config("LOG_LEVEL", default="INFO", cast=str).upper()
|
|
8
|
+
|
|
9
|
+
if not level.strip():
|
|
10
|
+
level = "INFO"
|
|
11
|
+
|
|
12
|
+
return level
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compare_log_levels(left: str, right: str) -> int:
|
|
16
|
+
"""
|
|
17
|
+
Compare log levels using logging.getLevelNamesMapping for accurate int values.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> compare_log_levels("DEBUG", "INFO")
|
|
21
|
+
-1 # DEBUG is less than INFO
|
|
22
|
+
|
|
23
|
+
Asks the question "Is INFO higher than DEBUG?"
|
|
24
|
+
"""
|
|
25
|
+
level_map = logging.getLevelNamesMapping()
|
|
26
|
+
left_level = level_map.get(left, left)
|
|
27
|
+
right_level = level_map.get(right, right)
|
|
28
|
+
|
|
29
|
+
# TODO should more gracefully fail here, but let's see what happens
|
|
30
|
+
if not isinstance(left_level, int) or not isinstance(right_level, int):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Invalid log level comparison: {left} ({type(left_level)}) vs {right} ({type(right_level)})"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return left_level - right_level
|