mh_structlog 0.0.40__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.
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, WARN, WARNING
4
+
5
+ import structlog
6
+
7
+ from .config import filter_named_logger, setup
8
+ from .utils import determine_name_for_logger
9
+
10
+
11
+ def getLogger(name: str | None = None): # noqa: ANN201, N802
12
+ """Return a named logger."""
13
+ if name is None:
14
+ name = determine_name_for_logger()
15
+ return structlog.get_logger(name)
16
+
17
+
18
+ def get_logger(name: str | None = None): # noqa: ANN201
19
+ """Return a named logger."""
20
+ return getLogger(name)
21
+
22
+
23
+ __all__ = [
24
+ "CRITICAL",
25
+ "DEBUG",
26
+ "ERROR",
27
+ "FATAL",
28
+ "INFO",
29
+ "WARN",
30
+ "WARNING",
31
+ "filter_named_logger",
32
+ "getLogger",
33
+ "getLogger",
34
+ "get_logger",
35
+ "get_logger",
36
+ "setup",
37
+ ]
mh_structlog/config.py ADDED
@@ -0,0 +1,272 @@
1
+ from __future__ import annotations
2
+
3
+ import logging # noqa: I001
4
+ import logging.config
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import structlog
10
+ from structlog.dev import RichTracebackFormatter
11
+ from structlog.processors import CallsiteParameter
12
+
13
+ from . import processors
14
+
15
+
16
+ SELECTED_LOG_FORMAT = 'console'
17
+
18
+
19
+ class StructlogLoggingConfigExceptionError(Exception):
20
+ """Exception to raise if the config is not correct."""
21
+
22
+
23
+ def setup( # noqa: PLR0912, PLR0915, C901
24
+ log_format: Literal["console", "json", "gcp_json"] | None = None,
25
+ logging_configs: list[dict] | None = None,
26
+ include_source_location: bool = False, # noqa: FBT001, FBT002
27
+ global_filter_level: int | None = None,
28
+ log_file: str | Path | None = None,
29
+ log_file_format: Literal["console", "json"] | None = None,
30
+ testing_mode: bool = False, # noqa: FBT001, FBT002
31
+ max_frames: int = 100,
32
+ sentry_config: dict | None = None,
33
+ ) -> None:
34
+ """This method configures structlog and the standard library logging module."""
35
+ global SELECTED_LOG_FORMAT # noqa: PLW0603
36
+
37
+ # Unless we are in testing mode, don't configure logging if it was already configured.
38
+ # During testing, we need te flexibility to configure logging multiple times.
39
+ if structlog.is_configured() and not testing_mode:
40
+ from logging import getLogger # noqa: PLC0415
41
+
42
+ getLogger('mh_structlog').warning('logging was already configured, so I return and do nothing.')
43
+ return
44
+
45
+ shared_processors = [
46
+ structlog.stdlib.add_logger_name, # add the logger name
47
+ structlog.stdlib.add_log_level, # add the log level as textual representation
48
+ structlog.processors.TimeStamper(fmt="iso", utc=True), # add a timestamp
49
+ structlog.contextvars.merge_contextvars, # add variables and bound data from global context
50
+ ]
51
+
52
+ if max_frames <= 0:
53
+ raise StructlogLoggingConfigExceptionError("max_frames should be a positive integer.")
54
+
55
+ # Configure stdout formatter
56
+ if log_format is None:
57
+ log_format = "console" if sys.stdout.isatty() else "json"
58
+ if log_format not in {"console", "json", "gcp_json"}:
59
+ raise StructlogLoggingConfigExceptionError("Unknown logging format requested.")
60
+
61
+ SELECTED_LOG_FORMAT = log_format
62
+
63
+ if sentry_config and sentry_config.get('active', True):
64
+ # 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
+ # When a request errors, there are normally other exceptions that show up in Sentry for it; adding the
66
+ # access log at the end only results in a duplicate event.
67
+ #
68
+ # When you specify ignore_loggers manually, it is not ignored anymore, so you should add it yourself (when wanted).
69
+ sentry_config.setdefault('ignore_loggers', ['mh_structlog.django.access'])
70
+ shared_processors.append(processors.SentryProcessor(**sentry_config))
71
+ else:
72
+ # In case logging statements add sentry_skip, but Sentry isn't configured at all, we do not want to output that key.
73
+ shared_processors.append(processors.FieldDropper(['sentry_skip']))
74
+
75
+ if log_format == "console":
76
+ selected_formatter = "mh_structlog_colored"
77
+ elif log_format in {"json", "gcp_json"}:
78
+ shared_processors.extend(
79
+ [structlog.processors.dict_tracebacks, processors.CapExceptionFrames(max_frames=2 * max_frames)]
80
+ )
81
+ selected_formatter = "mh_structlog_json"
82
+
83
+ if include_source_location:
84
+ shared_processors.append(
85
+ structlog.processors.CallsiteParameterAdder(
86
+ parameters={CallsiteParameter.PATHNAME, CallsiteParameter.LINENO, CallsiteParameter.FUNC_NAME}
87
+ )
88
+ )
89
+
90
+ wrapper_class = structlog.stdlib.BoundLogger
91
+ if global_filter_level is not None:
92
+ wrapper_class = structlog.make_filtering_bound_logger(global_filter_level)
93
+
94
+ # Structlog configuration
95
+ structlog.configure(
96
+ processors=[
97
+ *shared_processors,
98
+ structlog.stdlib.filter_by_level, # filter based on the stdlib logging config
99
+ structlog.stdlib.PositionalArgumentsFormatter(), # Allow string formatting with positional arguments in log calls
100
+ structlog.processors.StackInfoRenderer(
101
+ additional_ignores=['mh_structlog']
102
+ ), # when you create a log and specify stack_info=True, add a stacktrace to the log
103
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
104
+ ],
105
+ logger_factory=structlog.stdlib.LoggerFactory(),
106
+ wrapper_class=wrapper_class,
107
+ cache_logger_on_first_use=not testing_mode, # https://www.structlog.org/en/stable/testing.html#testing
108
+ )
109
+
110
+ # Std lib logging configuration.
111
+ stdlib_logging_config = {
112
+ "version": 1,
113
+ "disable_existing_loggers": False,
114
+ "formatters": {
115
+ "mh_structlog_plain": {
116
+ "()": structlog.stdlib.ProcessorFormatter,
117
+ "processors": [
118
+ processors.add_flattened_extra, # extract the content of 'extra' and add it as entries in the event dict
119
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta, # remove some fields used by structlogs internal logic
120
+ structlog.processors.EventRenamer("message"),
121
+ structlog.dev.ConsoleRenderer(
122
+ colors=False,
123
+ force_colors=False,
124
+ pad_event_to=80,
125
+ sort_keys=True,
126
+ event_key="message",
127
+ exception_formatter=RichTracebackFormatter(
128
+ width=None, max_frames=max_frames, show_locals=True, locals_hide_dunder=True
129
+ ),
130
+ ),
131
+ ],
132
+ "foreign_pre_chain": shared_processors,
133
+ },
134
+ "mh_structlog_colored": {
135
+ "()": structlog.stdlib.ProcessorFormatter,
136
+ "processors": [
137
+ processors.add_flattened_extra, # extract the content of 'extra' and add it as entries in the event dict
138
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta, # remove some fields used by structlogs internal logic
139
+ structlog.processors.EventRenamer("message"),
140
+ structlog.dev.ConsoleRenderer(
141
+ colors=True,
142
+ pad_event_to=80,
143
+ sort_keys=True,
144
+ event_key="message",
145
+ exception_formatter=RichTracebackFormatter(
146
+ width=None, max_frames=max_frames, show_locals=True, locals_hide_dunder=True
147
+ ),
148
+ ),
149
+ ],
150
+ "foreign_pre_chain": shared_processors,
151
+ },
152
+ "mh_structlog_json": {
153
+ "()": structlog.stdlib.ProcessorFormatter,
154
+ "processors": [
155
+ processors.add_flattened_extra, # extract the content of 'extra' and add it as entries in the event dict
156
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta, # remove some fields used by structlogs internal logic
157
+ structlog.processors.EventRenamer("message"),
158
+ processors.FieldRenamer(
159
+ log_format == 'gcp_json', 'level', 'severity'
160
+ ), # rename the level field for GCP
161
+ processors.render_orjson,
162
+ ],
163
+ "foreign_pre_chain": shared_processors,
164
+ },
165
+ },
166
+ "filters": {},
167
+ "handlers": {
168
+ "mh_structlog_stdout": {
169
+ "level": "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level),
170
+ "class": "logging.StreamHandler",
171
+ "stream": "ext://sys.stdout",
172
+ "formatter": selected_formatter,
173
+ }
174
+ },
175
+ "loggers": {
176
+ "": {
177
+ "handlers": ["mh_structlog_stdout"],
178
+ "level": "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level),
179
+ "propagate": True,
180
+ },
181
+ "stdout": {
182
+ "handlers": ["mh_structlog_stdout"],
183
+ "level": "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level),
184
+ "propagate": False,
185
+ },
186
+ },
187
+ }
188
+
189
+ # Add a handler to output to a file
190
+ if log_file:
191
+ # Select formatter
192
+ if log_file_format is None:
193
+ log_file_format = "console" if sys.stdout.isatty() else "json"
194
+ if log_file_format not in {"console", "json"}:
195
+ raise StructlogLoggingConfigExceptionError("Unknown logging format requested.")
196
+
197
+ if log_file_format == "console":
198
+ selected_file_formatter = "mh_structlog_plain"
199
+ elif log_file_format == "json":
200
+ selected_file_formatter = "mh_structlog_json"
201
+
202
+ log_file = Path(log_file)
203
+ log_file.parent.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Add a handler with file output to the root logger
206
+ stdlib_logging_config['handlers']['mh_structlog_file'] = {
207
+ "level": "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level),
208
+ "class": "logging.FileHandler",
209
+ "formatter": selected_file_formatter,
210
+ 'filename': str(log_file.resolve()),
211
+ }
212
+ stdlib_logging_config['loggers']['']['handlers'].append('mh_structlog_file')
213
+ # Add a named logger to log to the file only (the root logger logs to both stdout and file)
214
+ stdlib_logging_config['loggers']['file'] = {
215
+ "handlers": ["mh_structlog_file"],
216
+ "level": "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level),
217
+ "propagate": False,
218
+ }
219
+
220
+ # Merge in additional logging configs that were passed in by the caller.
221
+ if logging_configs:
222
+ for lc in logging_configs:
223
+ for k, v in lc.get("loggers", {}).items():
224
+ if k in {"", "root"}:
225
+ raise StructlogLoggingConfigExceptionError(
226
+ "It is not allowed to specify a custom root logger, since structlog configures that one."
227
+ )
228
+ # Add our handler if none was specified explicitly
229
+ if "handlers" not in v:
230
+ v["handlers"] = ["mh_structlog_stdout"]
231
+ if log_file:
232
+ v['handlers'].append('mh_structlog_file')
233
+ if "level" not in v:
234
+ v["level"] = "DEBUG" if global_filter_level is None else logging.getLevelName(global_filter_level)
235
+ v["propagate"] = False
236
+ stdlib_logging_config["loggers"][k] = v
237
+ for k, v in lc.get("handlers", {}).items():
238
+ # Set the formatter to ours if none was specified explicitly
239
+ if "formatter" not in v:
240
+ # If we are logging to a file and we do not do json format, use the non-colored formatter
241
+ if "file" in v["class"].lower() and selected_formatter == "mh_structlog_colored":
242
+ v["formatter"] = "mh_structlog_plain"
243
+ else:
244
+ v["formatter"] = selected_formatter
245
+ stdlib_logging_config["handlers"][k] = v
246
+ for k, v in lc.get("formatters", {}).items():
247
+ if k in {"mh_structlog_plain", "mh_structlog_colored", "mh_structlog_json"}:
248
+ raise StructlogLoggingConfigExceptionError(
249
+ f"It is not allowed to specify a formatter with the name {k}, since structlog configures that one."
250
+ )
251
+ stdlib_logging_config["formatters"][k] = v
252
+ for k, v in lc.get("filters", {}).items():
253
+ stdlib_logging_config["filters"][k] = v
254
+
255
+ logging.config.dictConfig(stdlib_logging_config)
256
+
257
+
258
+ def filter_named_logger(logger_name: str, level: int) -> dict:
259
+ """Return a dict containing a configuration for a named logger with a certain level filter.
260
+
261
+ Use this to silence a named logger by passing this config to the setup() method.
262
+ """
263
+ # fmt: off
264
+ return {
265
+ "loggers": {
266
+ logger_name: {
267
+ "level": level,
268
+ "propagate": False,
269
+ },
270
+ }
271
+ }
272
+ # fmt: on
mh_structlog/django.py ADDED
@@ -0,0 +1,80 @@
1
+ import time
2
+
3
+ import structlog
4
+ from asgiref.sync import iscoroutinefunction
5
+ from django.http import HttpRequest, HttpResponse
6
+ from django.utils.decorators import sync_and_async_middleware
7
+
8
+ from mh_structlog.config import SELECTED_LOG_FORMAT # noqa: PLC0415
9
+
10
+
11
+ logger = structlog.getLogger("mh_structlog.django.access")
12
+
13
+
14
+ def get_fields_to_log(request: HttpRequest, response: HttpResponse, latency_ms: int) -> dict:
15
+ """Extracts fields to log from the request object."""
16
+
17
+ fields_to_log = {'latency_ms': latency_ms, 'method': request.method, 'status': response.status_code}
18
+
19
+ if SELECTED_LOG_FORMAT == 'gcp_json':
20
+ fields_to_log['httpRequest'] = {
21
+ 'requestMethod': request.method,
22
+ 'requestUrl': request.build_absolute_uri(),
23
+ 'status': response.status_code,
24
+ 'latency': f"{latency_ms / 1000}s",
25
+ "userAgent": request.headers.get('User-Agent', ''),
26
+ "responseSize": str(response.headers.get('Content-Length', 0)),
27
+ }
28
+
29
+ return fields_to_log
30
+
31
+
32
+ @sync_and_async_middleware
33
+ def StructLogAccessLoggingMiddleware(get_response): # noqa: N802
34
+ """Middleware that logs access requests with some extra fields as structured logs."""
35
+
36
+ if iscoroutinefunction(get_response):
37
+
38
+ async def middleware(request):
39
+ start = time.time()
40
+ response = await get_response(request)
41
+ end = time.time()
42
+
43
+ latency_ms = int(1000 * (end - start))
44
+ fields_to_log = get_fields_to_log(request, response, latency_ms)
45
+
46
+ # in case Sentry is enabled, prevent logging to it.
47
+ # The actual exception will be logged if necessary somewhere else, but the response access log to the client should not be on there.
48
+
49
+ if response.status_code >= 500: # noqa: PLR2004
50
+ await logger.aerror(request.get_full_path(), sentry_skip=True, **fields_to_log)
51
+ elif response.status_code >= 400: # noqa: PLR2004
52
+ await logger.awarning(request.get_full_path(), sentry_skip=True, **fields_to_log)
53
+ else:
54
+ await logger.ainfo(request.get_full_path(), **fields_to_log)
55
+
56
+ return response
57
+
58
+ else:
59
+
60
+ def middleware(request):
61
+ start = time.time()
62
+ response = get_response(request)
63
+ end = time.time()
64
+
65
+ latency_ms = int(1000 * (end - start))
66
+ fields_to_log = get_fields_to_log(request, response, latency_ms)
67
+
68
+ # in case Sentry is enabled, prevent logging to it.
69
+ # The actual exception will be logged if necessary somewhere else, but the response access log to the client should not be on there.
70
+
71
+ if response.status_code >= 500: # noqa: PLR2004
72
+ logger.error(request.get_full_path(), sentry_skip=True, **fields_to_log)
73
+ elif response.status_code >= 400: # noqa: PLR2004
74
+ logger.warning(request.get_full_path(), sentry_skip=True, **fields_to_log)
75
+ else:
76
+ logger.info(request.get_full_path(), **fields_to_log)
77
+
78
+ return response
79
+
80
+ return middleware
@@ -0,0 +1,106 @@
1
+ import logging
2
+
3
+ import orjson
4
+ import structlog
5
+ from structlog.processors import CallsiteParameter
6
+ from structlog.typing import EventDict
7
+ from structlog_sentry import SentryProcessor as _SentryProcessor
8
+
9
+
10
+ # Inspect a default logging library record so we can find out which keys on a LogRecord are 'extra' and not default ones.
11
+ _LOG_RECORD_KEYS = set(logging.LogRecord("name", 0, "pathname", 0, "msg", (), None).__dict__.keys())
12
+
13
+
14
+ def add_flattened_extra(_, __, event_dict: dict) -> dict: # noqa: ANN001
15
+ """Include the content of 'extra' in the output log, flattened the attributes."""
16
+ if event_dict.get("_from_structlog"):
17
+ # Coming from structlog logging call
18
+ extra = event_dict.pop("extra", {})
19
+ event_dict.update(extra)
20
+ else:
21
+ # Coming from standard logging call
22
+ record = event_dict.get("_record")
23
+ if record is not None:
24
+ event_dict.update({k: v for k, v in record.__dict__.items() if k not in _LOG_RECORD_KEYS})
25
+
26
+ return event_dict
27
+
28
+
29
+ def _merge_pathname_lineno_function_to_location(logger: structlog.BoundLogger, name: str, event_dict: dict) -> dict: # noqa: ARG001
30
+ """Add the source of the log as a single attribute."""
31
+ pathname = event_dict.pop(CallsiteParameter.PATHNAME.value, None)
32
+ lineno = event_dict.pop(CallsiteParameter.LINENO.value, None)
33
+ func_name = event_dict.pop(CallsiteParameter.FUNC_NAME.value, None)
34
+ event_dict["location"] = f"{pathname}:{lineno}({func_name})"
35
+ return event_dict
36
+
37
+
38
+ def render_orjson(logger: structlog.BoundLogger, name: str, event_dict: dict) -> str: # noqa: ARG001
39
+ """Render the event_dict as a json string using orjson."""
40
+ return orjson.dumps(event_dict, default=repr).decode()
41
+
42
+
43
+ class FieldDropper:
44
+ """Drop fields from the event dict if present."""
45
+
46
+ def __init__(self, fields: list): # noqa: D107
47
+ self.fields = fields
48
+
49
+ def __call__(self, logger: logging.Logger, name: str, event_dict: EventDict) -> EventDict: # noqa: D102,ARG001,ARG002
50
+ for field in self.fields:
51
+ event_dict.pop(field, None)
52
+ return event_dict
53
+
54
+
55
+ class FieldRenamer:
56
+ """Rename fields in the event dict."""
57
+
58
+ def __init__(self, enable: bool, name_from: str, name_to: str): # noqa: D107
59
+ self.enable = enable
60
+ self.name_from = name_from
61
+ self.name_to = name_to
62
+
63
+ def __call__(self, logger: logging.Logger, name: str, event_dict: EventDict) -> EventDict: # noqa: D102,ARG001,ARG002
64
+ if self.enable and self.name_from in event_dict:
65
+ event_dict[self.name_to] = event_dict.pop(self.name_from)
66
+
67
+ return event_dict
68
+
69
+
70
+ class CapExceptionFrames:
71
+ """Limit the number of frames in the exception traceback.
72
+
73
+ With the builtin ConsoleRenderer, this can be given as argument (max_frames), but not when dict_tracebacks is used.
74
+ """
75
+
76
+ def __init__(self, max_frames: int):
77
+ """Set the max number of frames to keep in exception tracebacks."""
78
+ self.max_frames = max_frames
79
+
80
+ def __call__(self, logger: structlog.BoundLogger, name: str, event_dict: EventDict) -> EventDict: # noqa: ARG002, D102
81
+ if self.max_frames is not None and 'exception' in event_dict and 'frames' in event_dict["exception"]:
82
+ event_dict['exception']['frames'] = event_dict['exception']['frames'][-self.max_frames :]
83
+ 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
mh_structlog/utils.py ADDED
@@ -0,0 +1,22 @@
1
+ import inspect
2
+
3
+
4
+ def determine_name_for_logger():
5
+ """Return a name for a logger depending on the stackframe."""
6
+ frames = inspect.stack()
7
+
8
+ for f in frames:
9
+ frame = f
10
+ if 'mh_structlog' not in f[1]:
11
+ break
12
+
13
+ # Make a name ourselves
14
+ name: str = frame[1].lstrip('/').rstrip('.py').replace('/', '.')
15
+
16
+ # Strip away some common 'prefixes' paths
17
+ for location in ['src', 'code', 'app']:
18
+ if f'{location}.' in name:
19
+ _, _, name = name.partition(f'{location}.')
20
+ break
21
+
22
+ return name
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.3
2
+ Name: mh_structlog
3
+ Version: 0.0.40
4
+ Summary: Some Structlog configuration and wrappers to easily use structlog.
5
+ Author: Mathieu Hinderyckx
6
+ Author-email: Mathieu Hinderyckx <mathieu.hinderyckx@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Dist: orjson>=3.11.5
11
+ Requires-Dist: rich>=14.0.0
12
+ Requires-Dist: structlog>=25.5.0
13
+ Requires-Dist: structlog-sentry>=2.2.1
14
+ Requires-Dist: django>=5.0 ; extra == 'django'
15
+ Requires-Python: >=3.11
16
+ Provides-Extra: django
17
+ Description-Content-Type: text/markdown
18
+
19
+ # MH-Structlog
20
+
21
+ This package is used to setup the python logging system in combination with structlog. It configures both structlog and the standard library logging module, so your code can either use a structlog logger (which is recommended) or keep working with the standard logging library. This way all third-party packages that are producing logs (which use the stdlib logging module) will follow your logging setup and you will always output structured logging.
22
+
23
+ It is a fairly opinionated setup but has some configuration options to influence the behaviour. The two output log formats are either pretty-printing (for interactive views) or json. It includes optional reporting to Sentry, and can also log to a file.
24
+
25
+ ## Usage
26
+
27
+ This library should behave mostly as a drop-in import instead of the logging library import.
28
+
29
+ So instead of
30
+
31
+ ```python
32
+ import logging
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ logger.info('hey')
37
+ ```
38
+
39
+ you can do
40
+
41
+ ```python
42
+ import mh_structlog as logging
43
+ logging.setup() # necessary once at program startup, see readme further below
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ logger.info('hey')
48
+ ```
49
+
50
+ One big advantage of using the structlog logger over de stdlib logging one, is that you can pass arbitrary keyword arguments to our loggers when producing logs. E.g.
51
+
52
+ ```python
53
+ import mh_structlog as logging
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ logger.info('some message', hey='ho', a_list=[1,2,3])
58
+ ```
59
+
60
+ These extra key-value pairs will be included in the produced logs; either pretty-printed to the console or as data in the json entries.
61
+
62
+ ## Configuration via `setup()`
63
+
64
+ To configure your logging, call the `setup` function, which should be called once as early as possible in your program execution. This function configures all loggers.
65
+
66
+ ```python
67
+ import mh_structlog as logging
68
+
69
+ logging.setup()
70
+ ```
71
+
72
+ This will work out of the box with sane defaults: it logs to stdout in a pretty colored output when running in an interactive terminal, else it defaults to producing json output. See the next section for information on the arguments to this method.
73
+
74
+ ### Configuration options
75
+
76
+ For a setup which logs everything to the console in a pretty (colored) output, simply do:
77
+
78
+ ```python
79
+ from mh_structlog import *
80
+
81
+ setup(
82
+ log_format='console',
83
+ )
84
+
85
+ getLogger().info('hey')
86
+ ```
87
+
88
+ To log as json:
89
+
90
+ ```python
91
+ from mh_structlog import *
92
+
93
+ setup(
94
+ log_format='json',
95
+ )
96
+
97
+ getLogger().info('hey')
98
+ ```
99
+
100
+ To filter everything out up to a certain level:
101
+
102
+ ```python
103
+ from mh_structlog import *
104
+
105
+ setup(
106
+ log_format='console',
107
+ global_filter_level=WARNING,
108
+ )
109
+
110
+ getLogger().info('hey') # this does not get printed
111
+ getLogger().error('hey') # this does get printed
112
+ ```
113
+
114
+ To write logs to a file additionally (next to stdout):
115
+
116
+ ```python
117
+ from mh_structlog import *
118
+
119
+ setup(
120
+ log_format='console',
121
+ log_file='myfile.log',
122
+ )
123
+
124
+ getLogger().info('hey')
125
+ ```
126
+
127
+ To silence specific named loggers specifically (instead of setting the log level globally, it can be done per named logger):
128
+
129
+ ```python
130
+ from mh_structlog import *
131
+
132
+ setup(
133
+ log_format='console',
134
+ logging_configs=[
135
+ filter_named_logger('some_named_logger', WARNING),
136
+ ],
137
+ )
138
+
139
+ getLogger('some_named_logger').info('hey') # does not get logged
140
+ getLogger('some_named_logger').warning('hey') # does get logged
141
+
142
+ getLogger('some_other_named_logger').info('hey') # does get logged
143
+ getLogger('some_other_named_logger').warning('hey') # does get logged
144
+ ```
145
+
146
+ To include the source information about where a log was produced:
147
+
148
+ ```python
149
+ from mh_structlog import *
150
+
151
+ setup(
152
+ include_source_location=True
153
+ )
154
+
155
+ getLogger().info('hey')
156
+ ```
157
+
158
+ To choose how many frames you want to include in stacktraces on logging exceptions:
159
+
160
+ ```python
161
+ from mh_structlog import *
162
+
163
+ setup(
164
+ log_format='json',
165
+ max_frames=3,
166
+ )
167
+
168
+ try:
169
+ 5 / 0
170
+ except Exception as e:
171
+ getLogger().exception(e)
172
+ ```
173
+
174
+ To enable Sentry integration, pass a dict with a config according to the arguments which [structlog-sentry](https://github.com/kiwicom/structlog-sentry?tab=readme-ov-file#usage) allows to the setup function:
175
+
176
+ ```python
177
+ from mh_structlog import *
178
+ import sentry_sdk
179
+
180
+ config = {'dsn': '1234'}
181
+ sentry_sdk.init(dsn=config['dsn'])
182
+
183
+ setup(
184
+ sentry_config={'event_level': WARNING} # pass everything starting from WARNING level to Sentry
185
+ )
186
+
187
+ ```
@@ -0,0 +1,8 @@
1
+ mh_structlog/__init__.py,sha256=XeqylRUMZncSgFBpj625p9xlV-uZHkgNPcRRn_Me9Ww,756
2
+ mh_structlog/config.py,sha256=8aXzLxd-vwDeYELCZl4I_wEPlBJwS3jxnlhcjc1e_KI,12616
3
+ mh_structlog/django.py,sha256=69GFavKfQmBUYklQJnrU2G0dAza0T-3DxhW3kTFKnts,3130
4
+ mh_structlog/processors.py,sha256=hfH9o2k-AO-qHMfz-rK-J19ZBLOVUA_MU7j9tksqqOo,4328
5
+ mh_structlog/utils.py,sha256=UJ1p82ntgBeWGKd-b-v3j_eNT6LnnvBsD__spNVxAnU,557
6
+ mh_structlog-0.0.40.dist-info/WHEEL,sha256=xDCZ-UyfvkGuEHPeI7BcJzYKIZzdqN8A8o1M5Om8IyA,79
7
+ mh_structlog-0.0.40.dist-info/METADATA,sha256=YCcrskko4GYghZvGV2gi0ptF8i4Ca6rge2ZWGraUf4g,4932
8
+ mh_structlog-0.0.40.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any