structlog-config 0.1.0__py3-none-any.whl → 0.2.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.
- structlog_config/__init__.py +23 -10
- structlog_config/constants.py +8 -1
- structlog_config/env_config.py +1 -1
- structlog_config/fastapi_access_logger.py +6 -1
- structlog_config/levels.py +35 -0
- structlog_config/stdlib_logging.py +64 -39
- structlog_config/trace.py +62 -0
- structlog_config/warnings.py +6 -16
- {structlog_config-0.1.0.dist-info → structlog_config-0.2.0.dist-info}/METADATA +53 -13
- structlog_config-0.2.0.dist-info/RECORD +14 -0
- structlog_config-0.1.0.dist-info/RECORD +0 -12
- {structlog_config-0.1.0.dist-info → structlog_config-0.2.0.dist-info}/WHEEL +0 -0
structlog_config/__init__.py
CHANGED
|
@@ -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()
|
structlog_config/constants.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
|
|
3
4
|
from decouple import config
|
|
4
5
|
|
|
5
|
-
PYTHON_LOG_PATH = config("PYTHON_LOG_PATH", default=None)
|
|
6
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"
|
|
7
8
|
|
|
8
9
|
NO_COLOR = "NO_COLOR" in os.environ
|
|
9
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"
|
structlog_config/env_config.py
CHANGED
|
@@ -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
|
|
|
@@ -73,7 +73,12 @@ def is_static_assets_request(scope: Scope) -> bool:
|
|
|
73
73
|
Returns:
|
|
74
74
|
bool: True if the request is for static assets, False otherwise.
|
|
75
75
|
"""
|
|
76
|
-
return
|
|
76
|
+
return (
|
|
77
|
+
scope["path"].endswith(".css")
|
|
78
|
+
or scope["path"].endswith(".js")
|
|
79
|
+
# .map files are attempted when devtools are enabled
|
|
80
|
+
or scope["path"].endswith(".js.map")
|
|
81
|
+
)
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
def add_middleware(
|
|
@@ -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
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redirect all stdlib loggers to use the structlog configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
import logging
|
|
2
6
|
import sys
|
|
3
7
|
from pathlib import Path
|
|
@@ -5,52 +9,51 @@ from pathlib import Path
|
|
|
5
9
|
import structlog
|
|
6
10
|
from decouple import config
|
|
7
11
|
|
|
8
|
-
from structlog_config.env_config import get_custom_logger_configs
|
|
9
|
-
|
|
10
12
|
from .constants import PYTHONASYNCIODEBUG
|
|
13
|
+
from .env_config import get_custom_logger_config
|
|
11
14
|
from .environments import is_production, is_staging
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
from .levels import (
|
|
16
|
+
compare_log_levels,
|
|
17
|
+
get_environment_log_level_as_string,
|
|
18
|
+
)
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def reset_stdlib_logger(
|
|
19
|
-
logger_name: str,
|
|
20
|
-
default_structlog_handler: logging.Handler,
|
|
21
|
-
level_override: str | None = None,
|
|
22
|
+
logger_name: str, default_structlog_handler: logging.Handler, level_override: str
|
|
22
23
|
):
|
|
23
24
|
std_logger = logging.getLogger(logger_name)
|
|
24
25
|
std_logger.propagate = False
|
|
25
26
|
std_logger.handlers = []
|
|
26
27
|
std_logger.addHandler(default_structlog_handler)
|
|
27
|
-
|
|
28
|
-
if level_override:
|
|
29
|
-
std_logger.setLevel(level_override)
|
|
28
|
+
std_logger.setLevel(level_override)
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def redirect_stdlib_loggers(json_logger: bool):
|
|
33
32
|
"""
|
|
34
33
|
Redirect all standard logging module loggers to use the structlog configuration.
|
|
35
34
|
|
|
35
|
+
- json_loggers determines if logs are rendered as JSON or not
|
|
36
|
+
- The stdlib log stream is used to write logs to the output device (normally, stdout)
|
|
37
|
+
|
|
36
38
|
Inspired by: https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
|
|
37
39
|
"""
|
|
38
40
|
from structlog.stdlib import ProcessorFormatter
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
global_log_level = get_environment_log_level_as_string()
|
|
41
43
|
|
|
42
44
|
# TODO I don't understand why we can't use a processor stack as-is here. Need to investigate further.
|
|
43
45
|
|
|
46
|
+
# TODO why are we importing this here?
|
|
44
47
|
# Use ProcessorFormatter to format log records using structlog processors
|
|
45
48
|
from .__init__ import get_default_processors
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
default_processors = get_default_processors(json_logger=json_logger)
|
|
48
51
|
|
|
49
52
|
formatter = ProcessorFormatter(
|
|
50
53
|
processors=[
|
|
51
54
|
# required to strip extra keys that the structlog stdlib bindings add in
|
|
52
55
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
53
|
-
|
|
56
|
+
default_processors[-1]
|
|
54
57
|
if not is_production() and not is_staging()
|
|
55
58
|
# don't use ORJSON here, as the stdlib formatter chain expects a str not a bytes
|
|
56
59
|
else structlog.processors.JSONRenderer(sort_keys=True),
|
|
@@ -61,7 +64,7 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
61
64
|
# https://github.com/hynek/structlog/issues/254
|
|
62
65
|
structlog.stdlib.add_logger_name,
|
|
63
66
|
# omit the renderer so we can implement our own
|
|
64
|
-
*
|
|
67
|
+
*default_processors[:-1],
|
|
65
68
|
],
|
|
66
69
|
)
|
|
67
70
|
|
|
@@ -73,22 +76,29 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
73
76
|
file_handler.setFormatter(formatter)
|
|
74
77
|
return file_handler
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
python_log_path = config("PYTHON_LOG_PATH", default=None)
|
|
80
|
+
|
|
81
|
+
# if json_logger and python_log_path:
|
|
82
|
+
|
|
83
|
+
default_handler = (
|
|
84
|
+
logging.FileHandler(python_log_path)
|
|
85
|
+
if python_log_path
|
|
86
|
+
else logging.StreamHandler(sys.stdout)
|
|
87
|
+
)
|
|
88
|
+
default_handler.setLevel(global_log_level)
|
|
89
|
+
default_handler.setFormatter(formatter)
|
|
80
90
|
|
|
81
91
|
# Configure the root logger
|
|
82
92
|
root_logger = logging.getLogger()
|
|
83
|
-
root_logger.setLevel(
|
|
84
|
-
root_logger.handlers = [
|
|
93
|
+
root_logger.setLevel(global_log_level)
|
|
94
|
+
root_logger.handlers = [default_handler]
|
|
85
95
|
|
|
86
96
|
# Disable propagation to avoid duplicate logs
|
|
87
97
|
root_logger.propagate = True
|
|
88
98
|
|
|
89
99
|
# TODO there is a JSON-like format that can be used to configure loggers instead :/
|
|
90
100
|
std_logging_configuration = {
|
|
91
|
-
"httpcore": {},
|
|
101
|
+
# "httpcore": {},
|
|
92
102
|
"httpx": {
|
|
93
103
|
"levels": {
|
|
94
104
|
"INFO": "WARNING",
|
|
@@ -100,11 +110,6 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
100
110
|
}
|
|
101
111
|
},
|
|
102
112
|
}
|
|
103
|
-
|
|
104
|
-
# Merged from silence_loud_loggers - only silence asyncio if not explicitly debugging it
|
|
105
|
-
if not PYTHONASYNCIODEBUG:
|
|
106
|
-
std_logging_configuration["asyncio"] = {"level": "WARNING"}
|
|
107
|
-
|
|
108
113
|
"""
|
|
109
114
|
These loggers either:
|
|
110
115
|
|
|
@@ -116,45 +121,65 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
116
121
|
for a set of standard loggers.
|
|
117
122
|
"""
|
|
118
123
|
|
|
119
|
-
|
|
124
|
+
# TODO do we need this? could be AI slop
|
|
125
|
+
|
|
126
|
+
if not PYTHONASYNCIODEBUG:
|
|
127
|
+
std_logging_configuration["asyncio"] = {"level": "WARNING"}
|
|
128
|
+
|
|
129
|
+
environment_logger_config = get_custom_logger_config()
|
|
120
130
|
|
|
121
131
|
# now, let's handle some loggers that are probably already initialized with a handler
|
|
122
132
|
for logger_name, logger_config in std_logging_configuration.items():
|
|
123
133
|
level_override = None
|
|
124
134
|
|
|
125
|
-
#
|
|
135
|
+
# if we have a level override, use that
|
|
126
136
|
if "level" in logger_config:
|
|
127
137
|
level_override = logger_config["level"]
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
assert isinstance(level_override, str), (
|
|
139
|
+
f"Expected level override for {logger_name} to be a string, got {type(level_override)}"
|
|
140
|
+
)
|
|
141
|
+
# Check if we have a level mapping for the current log level
|
|
142
|
+
elif "levels" in logger_config and global_log_level in logger_config["levels"]:
|
|
143
|
+
level_override = logger_config["levels"][global_log_level]
|
|
144
|
+
|
|
145
|
+
# if a static override exists, only use it if it is lower than the global log level
|
|
146
|
+
if level_override and (
|
|
147
|
+
compare_log_levels(
|
|
148
|
+
level_override,
|
|
149
|
+
global_log_level,
|
|
150
|
+
)
|
|
151
|
+
< 0
|
|
152
|
+
):
|
|
153
|
+
level_override = None
|
|
154
|
+
|
|
155
|
+
handler_for_logger = default_handler
|
|
133
156
|
|
|
134
157
|
# Override with environment-specific config if available
|
|
135
158
|
if logger_name in environment_logger_config:
|
|
136
159
|
env_config = environment_logger_config[logger_name]
|
|
137
160
|
|
|
138
161
|
# if we have a custom path, use that instead
|
|
162
|
+
# right now this is the only handler override type we support
|
|
139
163
|
if "path" in env_config:
|
|
140
164
|
handler_for_logger = handler_for_path(env_config["path"])
|
|
141
165
|
|
|
166
|
+
# if the level is set via dynamic config, always use that
|
|
142
167
|
if "level" in env_config:
|
|
143
168
|
level_override = env_config["level"]
|
|
144
169
|
|
|
145
170
|
reset_stdlib_logger(
|
|
146
171
|
logger_name,
|
|
147
172
|
handler_for_logger,
|
|
148
|
-
level_override,
|
|
173
|
+
level_override or global_log_level,
|
|
149
174
|
)
|
|
150
175
|
|
|
151
176
|
# Handle any additional loggers defined in environment variables
|
|
152
177
|
for logger_name, logger_config in environment_logger_config.items():
|
|
153
|
-
# skip if already configured
|
|
178
|
+
# skip if already configured via the above loop
|
|
154
179
|
if logger_name in std_logging_configuration:
|
|
155
180
|
continue
|
|
156
181
|
|
|
157
|
-
handler_for_logger =
|
|
182
|
+
handler_for_logger = default_handler
|
|
158
183
|
|
|
159
184
|
if "path" in logger_config:
|
|
160
185
|
# if we have a custom path, use that instead
|
|
@@ -163,7 +188,7 @@ def redirect_stdlib_loggers(json_logger: bool):
|
|
|
163
188
|
reset_stdlib_logger(
|
|
164
189
|
logger_name,
|
|
165
190
|
handler_for_logger,
|
|
166
|
-
logger_config.get("level"),
|
|
191
|
+
logger_config.get("level", global_log_level),
|
|
167
192
|
)
|
|
168
193
|
|
|
169
194
|
# TODO do i need to setup exception overrides as well?
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapted from:
|
|
3
|
+
- https://github.com/willmcgugan/httpx/blob/973d1ed4e06577d928061092affe8f94def03331/httpx/_utils.py#L231
|
|
4
|
+
- https://github.com/vladmandic/sdnext/blob/d5d857aa961edbc46c9e77e7698f2e60011e7439/installer.py#L154
|
|
5
|
+
|
|
6
|
+
TODO this is not fully integrated into the codebase
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import typing
|
|
11
|
+
from functools import partial, partialmethod
|
|
12
|
+
|
|
13
|
+
from structlog._log_levels import NAME_TO_LEVEL
|
|
14
|
+
from structlog._native import LEVEL_TO_FILTERING_LOGGER, _make_filtering_bound_logger
|
|
15
|
+
|
|
16
|
+
from structlog_config.constants import TRACE_LOG_LEVEL
|
|
17
|
+
|
|
18
|
+
# Track if setup has already been called
|
|
19
|
+
_setup_called = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Stub for type checkers.
|
|
23
|
+
class Logger(logging.Logger):
|
|
24
|
+
def trace(
|
|
25
|
+
self, message: str, *args: typing.Any, **kwargs: typing.Any
|
|
26
|
+
) -> None: ... # pragma: nocover
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# def trace(self, message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
30
|
+
# if self.isEnabledFor(TRACE_LOG_LEVEL):
|
|
31
|
+
# self._log(TRACE_LOG_LEVEL, message, args, **kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def setup_trace() -> None:
|
|
35
|
+
"""Setup TRACE logging level. Safe to call multiple times."""
|
|
36
|
+
global _setup_called
|
|
37
|
+
|
|
38
|
+
if _setup_called:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# TODO consider adding warning to check the state of the underlying patched code
|
|
42
|
+
# patch structlog maps to include the additional level
|
|
43
|
+
NAME_TO_LEVEL["trace"] = TRACE_LOG_LEVEL
|
|
44
|
+
LEVEL_TO_FILTERING_LOGGER[TRACE_LOG_LEVEL] = _make_filtering_bound_logger(
|
|
45
|
+
TRACE_LOG_LEVEL
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logging.TRACE = TRACE_LOG_LEVEL
|
|
49
|
+
logging.addLevelName(TRACE_LOG_LEVEL, "TRACE")
|
|
50
|
+
|
|
51
|
+
if hasattr(logging.Logger, "trace"):
|
|
52
|
+
logging.warning("Logger.trace method already exists, not overriding it")
|
|
53
|
+
else:
|
|
54
|
+
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
|
|
55
|
+
|
|
56
|
+
# Check if trace function already exists in logging module
|
|
57
|
+
if hasattr(logging, "trace"):
|
|
58
|
+
logging.warning("logging.trace function already exists, overriding it")
|
|
59
|
+
else:
|
|
60
|
+
logging.trace = partial(logging.log, logging.TRACE)
|
|
61
|
+
|
|
62
|
+
_setup_called = True
|
structlog_config/warnings.py
CHANGED
|
@@ -9,6 +9,8 @@ import structlog
|
|
|
9
9
|
|
|
10
10
|
_original_warnings_showwarning: Any = None
|
|
11
11
|
|
|
12
|
+
warning_logger = structlog.get_logger(logger_name="py.warnings")
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def _showwarning(
|
|
14
16
|
message: Warning | str,
|
|
@@ -20,23 +22,11 @@ def _showwarning(
|
|
|
20
22
|
) -> Any:
|
|
21
23
|
"""
|
|
22
24
|
Redirects warnings to structlog so they appear in task logs etc.
|
|
23
|
-
|
|
24
|
-
Implementation of showwarnings which redirects to logging, which will first
|
|
25
|
-
check to see if the file parameter is None. If a file is specified, it will
|
|
26
|
-
delegate to the original warnings implementation of showwarning. Otherwise,
|
|
27
|
-
it will call warnings.formatwarning and will log the resulting string to a
|
|
28
|
-
warnings logger named "py.warnings" with level logging.WARNING.
|
|
29
25
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
else:
|
|
36
|
-
log = structlog.get_logger(logger_name="py.warnings")
|
|
37
|
-
log.warning(
|
|
38
|
-
str(message), category=category.__name__, filename=filename, lineno=lineno
|
|
39
|
-
)
|
|
26
|
+
|
|
27
|
+
warning_logger.warning(
|
|
28
|
+
str(message), category=category.__name__, filename=filename, lineno=lineno
|
|
29
|
+
)
|
|
40
30
|
|
|
41
31
|
|
|
42
32
|
def redirect_showwarnings():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structlog-config
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
Project-URL: Repository, https://github.com/iloveitaly/structlog-config
|
|
6
6
|
Author-email: Michael Bianco <mike@mikebian.co>
|
|
@@ -11,21 +11,46 @@ Requires-Dist: python-decouple-typed>=3.11.0
|
|
|
11
11
|
Requires-Dist: structlog>=25.2.0
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
|
|
14
|
-
# Structlog
|
|
14
|
+
# Opinionated Defaults for Structlog
|
|
15
15
|
|
|
16
|
-
Logging is really important:
|
|
16
|
+
Logging is really important. Getting logging to work well in python feels like black magic: there's a ton of configuration
|
|
17
|
+
across structlog, warnings, std loggers, fastapi + celery context, JSON logging in production, etc that requires lots of
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
Here are the main goals:
|
|
17
21
|
|
|
18
22
|
* High performance JSON logging in production
|
|
19
23
|
* All loggers, even plugin or system loggers, should route through the same formatter
|
|
20
24
|
* Structured logging everywhere
|
|
21
25
|
* Ability to easily set thread-local log context
|
|
26
|
+
* Nice log formatters for stack traces, ORM ([ActiveModel/SQLModel](https://github.com/iloveitaly/activemodel)), etc
|
|
27
|
+
* Ability to log level and output (i.e. file path) *by logger* for easy development debugging
|
|
28
|
+
* If you are using fastapi, structured logging for access logs
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from structlog_config import configure_logging
|
|
34
|
+
|
|
35
|
+
configure_logging()
|
|
36
|
+
```
|
|
22
37
|
|
|
23
38
|
## Stdib Log Management
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
By default, all stdlib loggers are:
|
|
41
|
+
|
|
42
|
+
1. Given the same global logging level, with some default adjustments for noisy loggers (looking at you, `httpx`)
|
|
43
|
+
2. Use a structlog formatter (you get structured logging, context, etc in any stdlib logger calls)
|
|
44
|
+
3. The root processor is overwritten so any child loggers created after initialization will use the same formatter
|
|
45
|
+
|
|
46
|
+
You can customize loggers by name (i.e. the name used in `logging.getLogger(__name__)`) using ENV variables.
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16):
|
|
49
|
+
|
|
50
|
+
* `LOG_LEVEL_OPENAI=DEBUG`
|
|
51
|
+
* `LOG_PATH_OPENAI=tmp/openai.log`
|
|
52
|
+
* `LOG_LEVEL_HTTPX=DEBUG`
|
|
53
|
+
* `LOG_PATH_HTTPX=tmp/openai.log`
|
|
29
54
|
|
|
30
55
|
## FastAPI Access Logger
|
|
31
56
|
|
|
@@ -40,14 +65,15 @@ Here's how to use it:
|
|
|
40
65
|
1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
|
|
41
66
|
2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
## iPython
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
%env LOG_LEVEL=DEBUG
|
|
74
|
+
from structlog_config import configure_logger
|
|
75
|
+
configure_logger()
|
|
76
|
+
```
|
|
51
77
|
|
|
52
78
|
## Related Projects
|
|
53
79
|
|
|
@@ -56,7 +82,21 @@ Adapted from:
|
|
|
56
82
|
|
|
57
83
|
## References
|
|
58
84
|
|
|
85
|
+
General logging:
|
|
86
|
+
|
|
59
87
|
- https://github.com/replicate/cog/blob/2e57549e18e044982bd100e286a1929f50880383/python/cog/logging.py#L20
|
|
60
88
|
- https://github.com/apache/airflow/blob/4280b83977cd5a53c2b24143f3c9a6a63e298acc/task_sdk/src/airflow/sdk/log.py#L187
|
|
61
89
|
- https://github.com/kiwicom/structlog-sentry
|
|
62
90
|
- https://github.com/jeremyh/datacube-explorer/blob/b289b0cde0973a38a9d50233fe0fff00e8eb2c8e/cubedash/logs.py#L40C21-L40C42
|
|
91
|
+
- https://stackoverflow.com/questions/76256249/logging-in-the-open-ai-python-library/78214464#78214464
|
|
92
|
+
- https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16
|
|
93
|
+
- https://www.python-httpx.org/logging/
|
|
94
|
+
|
|
95
|
+
FastAPI access logger:
|
|
96
|
+
|
|
97
|
+
- https://github.com/iloveitaly/fastapi-logger/blob/main/fastapi_structlog/middleware/access_log.py#L70
|
|
98
|
+
- https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/timing.py
|
|
99
|
+
- https://pypi.org/project/fastapi-structlog/
|
|
100
|
+
- https://pypi.org/project/asgi-correlation-id/
|
|
101
|
+
- https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
|
|
102
|
+
- https://github.com/sharu1204/fastapi-structlog/blob/master/app/main.py
|
|
@@ -0,0 +1,14 @@
|
|
|
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,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
structlog_config/__init__.py,sha256=OA1J4X3oWWPyqu1vojagsCrHmWDahqyFP_tAJJMYpTk,6162
|
|
2
|
-
structlog_config/constants.py,sha256=uwfeIMlu6yzl67dOS_JP427CO-9nyHX1kRyjp-Obb1M,260
|
|
3
|
-
structlog_config/env_config.py,sha256=CEjovBIJWxHtbzeqU2VAZ0SwYl8VKL_ECSgIfBU2Pbs,1738
|
|
4
|
-
structlog_config/environments.py,sha256=JpZYVVDGxEf1EaKdPdn6Jo-4wJK6SqF0ueFl7e2TBvI,612
|
|
5
|
-
structlog_config/fastapi_access_logger.py,sha256=DjO0Gn4zRNxXNBeOiibgwlovyg2dHbUFB2UMUzAE4Iw,3462
|
|
6
|
-
structlog_config/formatters.py,sha256=cprGEjvRFphJixbb0nVCpPn9sfw_Wv4d2vPtKDpM05A,5846
|
|
7
|
-
structlog_config/packages.py,sha256=asxrzLR-iRYAbkoSYutyTdIRcruTjHgkzfe2pjm2VFM,519
|
|
8
|
-
structlog_config/stdlib_logging.py,sha256=hQfX-NpEezqbPyvfw-F95i5-i3-zoaAvaWzSLEjsggM,6097
|
|
9
|
-
structlog_config/warnings.py,sha256=c74VRLxhx7jW96vkYfYwrKkGOaqQLLIfKQuaeB7i4n0,1594
|
|
10
|
-
structlog_config-0.1.0.dist-info/METADATA,sha256=uvFkIiX-qnlT0it-Zp1rJ0vb_VAMUsEP_HXIdwlMruM,2654
|
|
11
|
-
structlog_config-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
structlog_config-0.1.0.dist-info/RECORD,,
|
|
File without changes
|