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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mh_structlog
3
- Version: 0.0.44
3
+ Version: 0.0.46
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,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.44"
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
- if log_format not in {"console", "json", "gcp_json"}:
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