mh_structlog 0.0.44__tar.gz → 0.0.46__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.44 → mh_structlog-0.0.46}/PKG-INFO +1 -1
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/pyproject.toml +1 -1
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/aws.py +21 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/config.py +41 -3
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/processors.py +22 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/README.md +0 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/__init__.py +0 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/django.py +0 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/sentry.py +0 -0
- {mh_structlog-0.0.44 → mh_structlog-0.0.46}/src/mh_structlog/utils.py +0 -0
|
@@ -8,7 +8,7 @@ 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.46"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
classifiers = [
|
|
@@ -1,18 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
import structlog
|
|
2
4
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
3
5
|
|
|
4
6
|
|
|
7
|
+
is_cold_start = True
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _reset_cold_start_flag() -> None:
|
|
11
|
+
"""Reset the cold start flag to True. This is primarily intended for testing purposes."""
|
|
12
|
+
global is_cold_start # noqa: PLW0603
|
|
13
|
+
is_cold_start = True
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
def bind_lambda_context(lambda_context: LambdaContext) -> None:
|
|
6
17
|
"""Bind AWS Lambda context information to the structlog context variables, so log entries contain Lambda function metadata.
|
|
7
18
|
|
|
8
19
|
Args:
|
|
9
20
|
lambda_context (LambdaContext): The AWS Lambda context object.
|
|
10
21
|
"""
|
|
22
|
+
global is_cold_start # noqa: PLW0603
|
|
23
|
+
|
|
11
24
|
if lambda_context:
|
|
12
25
|
structlog.contextvars.clear_contextvars()
|
|
26
|
+
|
|
27
|
+
if os.getenv('AWS_LAMBDA_INITIALIZATION_TYPE', '') == "provisioned-concurrency":
|
|
28
|
+
is_cold_start = False
|
|
29
|
+
|
|
13
30
|
structlog.contextvars.bind_contextvars(
|
|
14
31
|
function_name=lambda_context.function_name,
|
|
15
32
|
function_memory_size=lambda_context.memory_limit_in_mb,
|
|
16
33
|
function_arn=lambda_context.invoked_function_arn,
|
|
17
34
|
function_request_id=lambda_context.aws_request_id,
|
|
35
|
+
cold_start=is_cold_start,
|
|
18
36
|
)
|
|
37
|
+
|
|
38
|
+
# After the first invocation of an environment, set cold_start to False for further invocations
|
|
39
|
+
is_cold_start = False
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging # noqa: I001
|
|
4
4
|
import logging.config
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Literal
|
|
@@ -21,7 +22,7 @@ class StructlogLoggingConfigExceptionError(Exception):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def setup( # noqa: PLR0912, PLR0915, C901
|
|
24
|
-
log_format: Literal["console", "json", "gcp_json"] | None = None,
|
|
25
|
+
log_format: Literal["console", "json", "gcp_json", "aws_json"] | None = None,
|
|
25
26
|
logging_configs: list[dict] | None = None,
|
|
26
27
|
include_source_location: bool = False, # noqa: FBT001, FBT002
|
|
27
28
|
global_filter_level: int | None = None,
|
|
@@ -31,6 +32,7 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
31
32
|
max_frames: int = 100,
|
|
32
33
|
sentry_config: dict | None = None,
|
|
33
34
|
additional_processors: list | None = None, # noqa: FBT001, FBT002
|
|
35
|
+
timestamp_ms_precision: bool | None = True,
|
|
34
36
|
) -> None:
|
|
35
37
|
"""This method configures structlog and the standard library logging module."""
|
|
36
38
|
global SELECTED_LOG_FORMAT # noqa: PLW0603
|
|
@@ -50,6 +52,9 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
50
52
|
structlog.contextvars.merge_contextvars, # add variables and bound data from global context
|
|
51
53
|
]
|
|
52
54
|
|
|
55
|
+
if timestamp_ms_precision:
|
|
56
|
+
shared_processors.append(processors.cap_timestamp_to_ms_precision)
|
|
57
|
+
|
|
53
58
|
if additional_processors:
|
|
54
59
|
shared_processors.extend(additional_processors)
|
|
55
60
|
|
|
@@ -59,7 +64,14 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
59
64
|
# Configure stdout formatter
|
|
60
65
|
if log_format is None:
|
|
61
66
|
log_format = "console" if sys.stdout.isatty() else "json"
|
|
62
|
-
|
|
67
|
+
|
|
68
|
+
# Determine a more specific log format based on environment if possible.
|
|
69
|
+
if log_format == "json":
|
|
70
|
+
if os.environ.get("GCP_PROJECT"):
|
|
71
|
+
log_format = "gcp_json"
|
|
72
|
+
elif os.environ.get("AWS_REGION"):
|
|
73
|
+
log_format = "aws_json"
|
|
74
|
+
if log_format not in {"console", "json", "gcp_json", "aws_json"}:
|
|
63
75
|
raise StructlogLoggingConfigExceptionError("Unknown logging format requested.")
|
|
64
76
|
|
|
65
77
|
SELECTED_LOG_FORMAT = log_format
|
|
@@ -85,7 +97,7 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
85
97
|
|
|
86
98
|
if log_format == "console":
|
|
87
99
|
selected_formatter = "mh_structlog_colored"
|
|
88
|
-
elif log_format in {"json", "gcp_json"}:
|
|
100
|
+
elif log_format in {"json", "gcp_json", "aws_json"}:
|
|
89
101
|
shared_processors.extend(
|
|
90
102
|
[structlog.processors.dict_tracebacks, processors.CapExceptionFrames(max_frames=2 * max_frames)]
|
|
91
103
|
)
|
|
@@ -99,8 +111,33 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
99
111
|
)
|
|
100
112
|
|
|
101
113
|
wrapper_class = structlog.stdlib.BoundLogger
|
|
114
|
+
|
|
115
|
+
env_log_level_str = os.environ.get('LOG_LEVEL', '').upper()
|
|
116
|
+
env_log_level_constant = logging._nameToLevel.get(env_log_level_str) # noqa: SLF001
|
|
117
|
+
|
|
118
|
+
if env_log_level_str and not env_log_level_constant:
|
|
119
|
+
raise StructlogLoggingConfigExceptionError(
|
|
120
|
+
f"LOG_LEVEL environment variable has unrecognized value: {env_log_level_str}"
|
|
121
|
+
)
|
|
122
|
+
|
|
102
123
|
if global_filter_level is not None:
|
|
124
|
+
if global_filter_level not in logging._nameToLevel.values(): # noqa: SLF001
|
|
125
|
+
raise StructlogLoggingConfigExceptionError(
|
|
126
|
+
f"global_filter_level has unrecognized value: {global_filter_level}"
|
|
127
|
+
)
|
|
128
|
+
if env_log_level_constant and env_log_level_constant != global_filter_level:
|
|
129
|
+
from logging import getLogger # noqa: PLC0415
|
|
130
|
+
|
|
131
|
+
getLogger('mh_structlog').warning(
|
|
132
|
+
'Both global_filter_level (%s, %s) and LOG_LEVEL environment variable (%s, %s) are set, but they differ. global_filter_level takes precedence.',
|
|
133
|
+
global_filter_level,
|
|
134
|
+
logging.getLevelName(global_filter_level),
|
|
135
|
+
env_log_level_constant,
|
|
136
|
+
logging.getLevelName(env_log_level_constant),
|
|
137
|
+
)
|
|
103
138
|
wrapper_class = structlog.make_filtering_bound_logger(global_filter_level)
|
|
139
|
+
elif env_log_level_constant:
|
|
140
|
+
wrapper_class = structlog.make_filtering_bound_logger(env_log_level_constant) # noqa: SLF001
|
|
104
141
|
|
|
105
142
|
# Structlog configuration
|
|
106
143
|
structlog.configure(
|
|
@@ -169,6 +206,7 @@ def setup( # noqa: PLR0912, PLR0915, C901
|
|
|
169
206
|
processors.FieldRenamer(
|
|
170
207
|
log_format == 'gcp_json', 'level', 'severity'
|
|
171
208
|
), # rename the level field for GCP
|
|
209
|
+
processors.FieldTransformer(log_format == 'aws_json', 'level', lambda v: v.upper()),
|
|
172
210
|
processors.render_orjson,
|
|
173
211
|
],
|
|
174
212
|
"foreign_pre_chain": shared_processors,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
|
|
3
4
|
import orjson
|
|
4
5
|
import structlog
|
|
@@ -81,6 +82,20 @@ class FieldRenamer:
|
|
|
81
82
|
return event_dict
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
class FieldTransformer:
|
|
86
|
+
"""Transform a field in the event dict using a provided function."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, enable: bool, field_name: str, transform_function: Callable): # noqa: D107
|
|
89
|
+
self.enable = enable
|
|
90
|
+
self.field_name = field_name
|
|
91
|
+
self.transform_function = transform_function
|
|
92
|
+
|
|
93
|
+
def __call__(self, logger: logging.Logger, name: str, event_dict: EventDict) -> EventDict: # noqa: D102,ARG001,ARG002
|
|
94
|
+
if self.enable and self.field_name in event_dict:
|
|
95
|
+
event_dict[self.field_name] = self.transform_function(event_dict[self.field_name])
|
|
96
|
+
return event_dict
|
|
97
|
+
|
|
98
|
+
|
|
84
99
|
class CapExceptionFrames:
|
|
85
100
|
"""Limit the number of frames in the exception traceback.
|
|
86
101
|
|
|
@@ -95,3 +110,10 @@ class CapExceptionFrames:
|
|
|
95
110
|
if self.max_frames is not None and 'exception' in event_dict and 'frames' in event_dict["exception"]:
|
|
96
111
|
event_dict['exception']['frames'] = event_dict['exception']['frames'][-self.max_frames :]
|
|
97
112
|
return event_dict
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cap_timestamp_to_ms_precision(_, __, event_dict: dict) -> dict: # noqa: ANN001
|
|
116
|
+
"""Cap the timestamp to millisecond precision, dropping the microseconds part."""
|
|
117
|
+
if ts := event_dict.get("timestamp"):
|
|
118
|
+
event_dict['timestamp'] = ts[:-4] + 'Z'
|
|
119
|
+
return event_dict
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|