mh_structlog 0.0.41__tar.gz → 0.0.44__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.
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/PKG-INFO +19 -4
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/README.md +14 -0
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/pyproject.toml +18 -27
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/__init__.py +4 -0
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/config.py +12 -1
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/processors.py +15 -24
- mh_structlog-0.0.44/src/mh_structlog/sentry.py +25 -0
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/utils.py +5 -5
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/aws.py +0 -0
- {mh_structlog-0.0.41 → mh_structlog-0.0.44}/src/mh_structlog/django.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mh_structlog
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.44
|
|
4
4
|
Summary: Some Structlog configuration and wrappers to easily use structlog.
|
|
5
5
|
Author: Mathieu Hinderyckx
|
|
6
6
|
Author-email: Mathieu Hinderyckx <mathieu.hinderyckx@gmail.com>
|
|
@@ -8,14 +8,15 @@ Classifier: Programming Language :: Python :: 3
|
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Dist: orjson>=3.11.5
|
|
11
|
-
Requires-Dist: rich>=14.
|
|
11
|
+
Requires-Dist: rich>=14.2.0
|
|
12
12
|
Requires-Dist: structlog>=25.5.0
|
|
13
|
-
Requires-Dist: structlog-sentry>=2.2.1
|
|
14
13
|
Requires-Dist: aws-lambda-powertools>=3.23.0 ; extra == 'aws'
|
|
15
14
|
Requires-Dist: django>=5.0 ; extra == 'django'
|
|
16
|
-
Requires-
|
|
15
|
+
Requires-Dist: structlog-sentry>=2.2.1 ; extra == 'sentry'
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
17
|
Provides-Extra: aws
|
|
18
18
|
Provides-Extra: django
|
|
19
|
+
Provides-Extra: sentry
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
|
|
21
22
|
# MH-Structlog
|
|
@@ -187,3 +188,17 @@ setup(
|
|
|
187
188
|
)
|
|
188
189
|
|
|
189
190
|
```
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
Install the environment:
|
|
195
|
+
|
|
196
|
+
```shell
|
|
197
|
+
uv sync --python-preference only-managed --frozen --all-extras --all-groups
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Run the unittests:
|
|
201
|
+
|
|
202
|
+
```shell
|
|
203
|
+
uv run pytest -s --pdb --pdbcls=IPython.terminal.debugger:Pdb
|
|
204
|
+
```
|
|
@@ -167,3 +167,17 @@ setup(
|
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
```
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
Install the environment:
|
|
174
|
+
|
|
175
|
+
```shell
|
|
176
|
+
uv sync --python-preference only-managed --frozen --all-extras --all-groups
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Run the unittests:
|
|
180
|
+
|
|
181
|
+
```shell
|
|
182
|
+
uv run pytest -s --pdb --pdbcls=IPython.terminal.debugger:Pdb
|
|
183
|
+
```
|
|
@@ -8,9 +8,9 @@ authors = [
|
|
|
8
8
|
{ name = "Mathieu Hinderyckx", email = "mathieu.hinderyckx@gmail.com" },
|
|
9
9
|
]
|
|
10
10
|
description = "Some Structlog configuration and wrappers to easily use structlog."
|
|
11
|
-
version = "0.0.
|
|
11
|
+
version = "0.0.44"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
@@ -18,9 +18,8 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
20
|
"orjson>=3.11.5",
|
|
21
|
-
"rich>=14.
|
|
21
|
+
"rich>=14.2.0",
|
|
22
22
|
"structlog>=25.5.0",
|
|
23
|
-
"structlog-sentry>=2.2.1",
|
|
24
23
|
]
|
|
25
24
|
[project.optional-dependencies]
|
|
26
25
|
django = [
|
|
@@ -29,42 +28,34 @@ django = [
|
|
|
29
28
|
aws = [
|
|
30
29
|
"aws-lambda-powertools>=3.23.0",
|
|
31
30
|
]
|
|
31
|
+
sentry = [
|
|
32
|
+
"structlog-sentry>=2.2.1",
|
|
33
|
+
]
|
|
32
34
|
|
|
33
35
|
[tool.setuptools_scm]
|
|
34
36
|
version_file = "src/mh_structlog/_version.py"
|
|
35
37
|
|
|
36
38
|
[dependency-groups]
|
|
37
|
-
dev = [
|
|
39
|
+
dev = [
|
|
40
|
+
"ipdb>=0.13.13",
|
|
41
|
+
"ipython>=8.18.1",
|
|
42
|
+
"ruff>=0.14.9",
|
|
43
|
+
]
|
|
38
44
|
tests = [
|
|
39
|
-
"coverage>=7.
|
|
45
|
+
"coverage>=7.13.0",
|
|
46
|
+
"django>=5",
|
|
47
|
+
"freezegun>=1.5.5",
|
|
40
48
|
"pytest>=9.0.2",
|
|
41
49
|
"pytest-cov>=7.0.0",
|
|
42
50
|
"pytest-django>=4.11.1",
|
|
43
51
|
"pytest-env>=1.2.0",
|
|
44
|
-
"pytest-randomly>=
|
|
52
|
+
"pytest-randomly>=4.0.1",
|
|
45
53
|
"pytest-sugar>=1.1.1",
|
|
46
|
-
"django>=5.2",
|
|
47
|
-
"freezegun>=1.5.5",
|
|
48
|
-
]
|
|
49
|
-
pages = [
|
|
50
|
-
"black>=24.10.0",
|
|
51
|
-
"mkdocs-literate-nav>=0.6.1",
|
|
52
|
-
"mkdocs-gen-files>=0.5.0",
|
|
53
|
-
"mkdocstrings[python]>=0.26.2",
|
|
54
|
-
"pymdown-extensions>=10.12",
|
|
55
|
-
"mkdocs-include-dir-to-nav>=1.2.0",
|
|
56
|
-
"mkdocs-git-authors-plugin>=0.9.2",
|
|
57
|
-
"mkdocs-glightbox>=0.4.0",
|
|
58
|
-
"mkdocs-git-revision-date-localized-plugin>=1.3.0",
|
|
59
|
-
"mkdocs-roamlinks-plugin>=0.3.2",
|
|
60
|
-
"mkdocs>=1.6.1",
|
|
61
|
-
"mkdocs-material>=9.5.44",
|
|
62
54
|
]
|
|
63
55
|
|
|
64
56
|
[tool.uv]
|
|
65
|
-
required-version = ">=0.9.
|
|
57
|
+
required-version = ">=0.9.17"
|
|
66
58
|
package = true
|
|
67
|
-
cache-keys = [{ file = "pyproject.toml" }]
|
|
68
59
|
environments = [
|
|
69
60
|
"sys_platform == 'darwin'",
|
|
70
61
|
"sys_platform == 'linux'",
|
|
@@ -76,8 +67,8 @@ environments = [
|
|
|
76
67
|
line-length = 120
|
|
77
68
|
indent-width = 4
|
|
78
69
|
preview = false
|
|
79
|
-
required-version = ">=0.
|
|
80
|
-
target-version = "
|
|
70
|
+
required-version = ">=0.12"
|
|
71
|
+
target-version = "py310"
|
|
81
72
|
unsafe-fixes = true
|
|
82
73
|
|
|
83
74
|
# Overwritten in CI/CD
|
|
@@ -5,6 +5,7 @@ from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, WARN, WARNING
|
|
|
5
5
|
import structlog
|
|
6
6
|
|
|
7
7
|
from .config import filter_named_logger, setup
|
|
8
|
+
from .processors import FieldDropper, FieldRenamer, FieldsAdder
|
|
8
9
|
from .utils import determine_name_for_logger
|
|
9
10
|
|
|
10
11
|
|
|
@@ -28,6 +29,9 @@ __all__ = [
|
|
|
28
29
|
"INFO",
|
|
29
30
|
"WARN",
|
|
30
31
|
"WARNING",
|
|
32
|
+
"FieldDropper",
|
|
33
|
+
"FieldRenamer",
|
|
34
|
+
"FieldsAdder",
|
|
31
35
|
"filter_named_logger",
|
|
32
36
|
"getLogger",
|
|
33
37
|
"getLogger",
|
|
@@ -30,6 +30,7 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
30
30
|
testing_mode: bool = False, # noqa: FBT001, FBT002
|
|
31
31
|
max_frames: int = 100,
|
|
32
32
|
sentry_config: dict | None = None,
|
|
33
|
+
additional_processors: list | None = None, # noqa: FBT001, FBT002
|
|
33
34
|
) -> None:
|
|
34
35
|
"""This method configures structlog and the standard library logging module."""
|
|
35
36
|
global SELECTED_LOG_FORMAT # noqa: PLW0603
|
|
@@ -49,6 +50,9 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
49
50
|
structlog.contextvars.merge_contextvars, # add variables and bound data from global context
|
|
50
51
|
]
|
|
51
52
|
|
|
53
|
+
if additional_processors:
|
|
54
|
+
shared_processors.extend(additional_processors)
|
|
55
|
+
|
|
52
56
|
if max_frames <= 0:
|
|
53
57
|
raise StructlogLoggingConfigExceptionError("max_frames should be a positive integer.")
|
|
54
58
|
|
|
@@ -61,13 +65,20 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
61
65
|
SELECTED_LOG_FORMAT = log_format
|
|
62
66
|
|
|
63
67
|
if sentry_config and sentry_config.get('active', True):
|
|
68
|
+
try:
|
|
69
|
+
from . import sentry # noqa: PLC0415
|
|
70
|
+
except ImportError as e:
|
|
71
|
+
raise StructlogLoggingConfigExceptionError(
|
|
72
|
+
"sentry_config was provided, but mh_structlog.sentry could not be imported. "
|
|
73
|
+
"Make sure this package is installed with its 'sentry' extra."
|
|
74
|
+
) from e
|
|
64
75
|
# By default, ignore our own request access logger (which is only used when you use the Django access logger from this package in your project).
|
|
65
76
|
# When a request errors, there are normally other exceptions that show up in Sentry for it; adding the
|
|
66
77
|
# access log at the end only results in a duplicate event.
|
|
67
78
|
#
|
|
68
79
|
# When you specify ignore_loggers manually, it is not ignored anymore, so you should add it yourself (when wanted).
|
|
69
80
|
sentry_config.setdefault('ignore_loggers', ['mh_structlog.django.access'])
|
|
70
|
-
shared_processors.append(
|
|
81
|
+
shared_processors.append(sentry.SentryProcessor(**sentry_config))
|
|
71
82
|
else:
|
|
72
83
|
# In case logging statements add sentry_skip, but Sentry isn't configured at all, we do not want to output that key.
|
|
73
84
|
shared_processors.append(processors.FieldDropper(['sentry_skip']))
|
|
@@ -4,7 +4,6 @@ import orjson
|
|
|
4
4
|
import structlog
|
|
5
5
|
from structlog.processors import CallsiteParameter
|
|
6
6
|
from structlog.typing import EventDict
|
|
7
|
-
from structlog_sentry import SentryProcessor as _SentryProcessor
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
# Inspect a default logging library record so we can find out which keys on a LogRecord are 'extra' and not default ones.
|
|
@@ -40,6 +39,21 @@ def render_orjson(logger: structlog.BoundLogger, name: str, event_dict: dict) ->
|
|
|
40
39
|
return orjson.dumps(event_dict, default=repr).decode()
|
|
41
40
|
|
|
42
41
|
|
|
42
|
+
class FieldsAdder:
|
|
43
|
+
"""Add static fields to each event dict.
|
|
44
|
+
|
|
45
|
+
E.g. you can configure it to add {"service": "my-service", "env": "production"} to each log at program startup,
|
|
46
|
+
instead of having to configure them on every logger.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, data: dict): # noqa: D107
|
|
50
|
+
self.data = data
|
|
51
|
+
|
|
52
|
+
def __call__(self, logger: logging.Logger, name: str, event_dict: EventDict) -> EventDict: # noqa: D102,ARG001,ARG002
|
|
53
|
+
event_dict.update(self.data)
|
|
54
|
+
return event_dict
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
class FieldDropper:
|
|
44
58
|
"""Drop fields from the event dict if present."""
|
|
45
59
|
|
|
@@ -81,26 +95,3 @@ class CapExceptionFrames:
|
|
|
81
95
|
if self.max_frames is not None and 'exception' in event_dict and 'frames' in event_dict["exception"]:
|
|
82
96
|
event_dict['exception']['frames'] = event_dict['exception']['frames'][-self.max_frames :]
|
|
83
97
|
return event_dict
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class SentryProcessor(_SentryProcessor):
|
|
87
|
-
"""The SentryProcessor but with some of our own defaults and slight customization applied."""
|
|
88
|
-
|
|
89
|
-
def __init__(self, **kwargs): # noqa: D107
|
|
90
|
-
# Unless otherwise specified, add all extra attributes from the log to Sentry as tags.
|
|
91
|
-
# Explicitly pass tag_keys=None to avoid this behaviour.
|
|
92
|
-
if 'tag_keys' not in kwargs:
|
|
93
|
-
kwargs['tag_keys'] = '__all__'
|
|
94
|
-
super().__init__(**kwargs)
|
|
95
|
-
|
|
96
|
-
def _get_event_and_hint(self, event_dict: EventDict) -> tuple[dict, dict]:
|
|
97
|
-
"""Filter out tag_keys which are not primitive types, because Sentry gives an error otherwise."""
|
|
98
|
-
|
|
99
|
-
event, hint = super()._get_event_and_hint(event_dict)
|
|
100
|
-
|
|
101
|
-
if 'tags' in event:
|
|
102
|
-
for k in list(event['tags'].keys()):
|
|
103
|
-
if not isinstance(event['tags'][k], (bool, str, int, float, type(None))): # noqa: UP038
|
|
104
|
-
del event['tags'][k]
|
|
105
|
-
|
|
106
|
-
return event, hint
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from structlog.typing import EventDict
|
|
2
|
+
from structlog_sentry import SentryProcessor as _SentryProcessor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SentryProcessor(_SentryProcessor):
|
|
6
|
+
"""The SentryProcessor but with some of our own defaults and slight customization applied."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, **kwargs): # noqa: D107
|
|
9
|
+
# Unless otherwise specified, add all extra attributes from the log to Sentry as tags.
|
|
10
|
+
# Explicitly pass tag_keys=None to avoid this behaviour.
|
|
11
|
+
if 'tag_keys' not in kwargs:
|
|
12
|
+
kwargs['tag_keys'] = '__all__'
|
|
13
|
+
super().__init__(**kwargs)
|
|
14
|
+
|
|
15
|
+
def _get_event_and_hint(self, event_dict: EventDict) -> tuple[dict, dict]:
|
|
16
|
+
"""Filter out tag_keys which are not primitive types, because Sentry gives an error otherwise."""
|
|
17
|
+
|
|
18
|
+
event, hint = super()._get_event_and_hint(event_dict)
|
|
19
|
+
|
|
20
|
+
if 'tags' in event:
|
|
21
|
+
for k in list(event['tags'].keys()):
|
|
22
|
+
if not isinstance(event['tags'][k], (bool, str, int, float, type(None))): # noqa: UP038
|
|
23
|
+
del event['tags'][k]
|
|
24
|
+
|
|
25
|
+
return event, hint
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def determine_name_for_logger():
|
|
@@ -10,13 +11,12 @@ def determine_name_for_logger():
|
|
|
10
11
|
if 'mh_structlog' not in f[1]:
|
|
11
12
|
break
|
|
12
13
|
|
|
13
|
-
# Make a name ourselves
|
|
14
|
+
# Make a name ourselves based on the path in the stackframe
|
|
14
15
|
name: str = frame[1].lstrip('/').rstrip('.py').replace('/', '.')
|
|
15
16
|
|
|
16
17
|
# Strip away some common 'prefixes' paths
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
break
|
|
18
|
+
cwd = str(Path.cwd()).lstrip('/').rstrip('.py').replace('/', '.')
|
|
19
|
+
for location in [cwd, 'var.task', 'src', 'code', 'app']:
|
|
20
|
+
name = name.removeprefix(f'{location}.')
|
|
21
21
|
|
|
22
22
|
return name
|
|
File without changes
|
|
File without changes
|