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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mh_structlog
3
- Version: 0.0.41
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.0.0
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-Python: >=3.11
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.41"
11
+ version = "0.0.44"
12
12
  readme = "README.md"
13
- requires-python = ">=3.11"
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.0.0",
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 = ["ipdb>=0.13.13", "ipython>=8.18.1", "pre-commit>=4.0.1", "ruff>=0.12.1"]
39
+ dev = [
40
+ "ipdb>=0.13.13",
41
+ "ipython>=8.18.1",
42
+ "ruff>=0.14.9",
43
+ ]
38
44
  tests = [
39
- "coverage>=7.5.3",
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>=3.15.0",
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.11"
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.4.5"
80
- target-version = "py39"
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(processors.SentryProcessor(**sentry_config))
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
- for location in ['var.task', 'src', 'code', 'app']:
18
- if f'{location}.' in name:
19
- _, _, name = name.partition(f'{location}.')
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