hydra-structlog 0.1.0__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,5 @@
1
+ __all__ = [
2
+ "HydraStructlogSearchPathPlugin",
3
+ ]
4
+
5
+ from hydra_structlog.plugin import HydraStructlogSearchPathPlugin
File without changes
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,102 @@
1
+ # @package hydra.job_logging
2
+ # python logging configuration for tasks
3
+ version: 1
4
+ formatters:
5
+ console:
6
+ "()": hydra_structlog.formatters.build_processor_formatter
7
+ foreign_pre_chain: ${hydra.job_logging.formatters.json.foreign_pre_chain}
8
+ processors:
9
+ _target_: hydra_structlog.processors.named_processor_chain
10
+ remove_processors_meta:
11
+ _target_: structlog.stdlib.ProcessorFormatter.remove_processors_meta
12
+ _partial_: true
13
+ time_stamper:
14
+ _target_: structlog.processors.TimeStamper
15
+ fmt: "%Y-%m-%d %H:%M:%S"
16
+ utc: false
17
+ console_renderer:
18
+ _target_: structlog.dev.ConsoleRenderer
19
+ json:
20
+ "()": hydra_structlog.formatters.build_processor_formatter
21
+ foreign_pre_chain:
22
+ _target_: hydra_structlog.processors.named_processor_chain
23
+ merge_contextvars:
24
+ _target_: structlog.contextvars.merge_contextvars
25
+ _partial_: true
26
+ add_logger_name:
27
+ _target_: structlog.stdlib.add_logger_name
28
+ _partial_: true
29
+ add_log_level:
30
+ _target_: structlog.stdlib.add_log_level
31
+ _partial_: true
32
+ positional_arguments_formatter:
33
+ _target_: structlog.stdlib.PositionalArgumentsFormatter
34
+ extra_adder:
35
+ _target_: structlog.stdlib.ExtraAdder
36
+ stack_info_renderer:
37
+ _target_: structlog.processors.StackInfoRenderer
38
+ processors:
39
+ _target_: hydra_structlog.processors.named_processor_chain
40
+ remove_processors_meta:
41
+ _target_: structlog.stdlib.ProcessorFormatter.remove_processors_meta
42
+ _partial_: true
43
+ static_field_adder:
44
+ _target_: hydra_structlog.processors.StaticFieldAdder
45
+ service: ${oc.env:SERVICE,null}
46
+ version: ${oc.env:VERSION,null}
47
+ env: ${oc.env:ENV,dev}
48
+ task: ${hydra.job.name}
49
+ callsite_parameter_adder:
50
+ _target_: structlog.processors.CallsiteParameterAdder
51
+ parameters:
52
+ - _target_: structlog.processors.CallsiteParameter
53
+ _args_:
54
+ - lineno
55
+ - _target_: structlog.processors.CallsiteParameter
56
+ _args_:
57
+ - func_name
58
+ additional_ignores:
59
+ - warnings
60
+ - hydra_structlog
61
+ time_stamper:
62
+ _target_: structlog.processors.TimeStamper
63
+ fmt: iso
64
+ field_renamer:
65
+ _target_: hydra_structlog.processors.FieldRenamer
66
+ level: status
67
+ event: message
68
+ format_exc_info:
69
+ _target_: structlog.processors.format_exc_info
70
+ _partial_: true
71
+ json_renderer:
72
+ _target_: structlog.processors.JSONRenderer
73
+ sort_keys: true
74
+ handlers:
75
+ console:
76
+ class: hydra_structlog.handlers.TqdmStreamHandler
77
+ formatter: console
78
+ stream: ext://sys.stdout
79
+ file:
80
+ # Run-local copy of the log
81
+ class: logging.FileHandler
82
+ formatter: json
83
+ filename: ${hydra.runtime.output_dir}/${hydra.job.name}.log
84
+ encoding: utf-8
85
+ json:
86
+ # Global structured log for monitoring or similar
87
+ class: logging.handlers.RotatingFileHandler
88
+ formatter: json
89
+ level: INFO
90
+ # Fixed location for all runs
91
+ filename: ${hydra.runtime.cwd}/structured.log
92
+ encoding: utf-8
93
+ # 1024**2 = 1 MB
94
+ maxBytes: 1048576
95
+ # Important for safe rotation (structured.log.1)
96
+ backupCount: 1
97
+
98
+ root:
99
+ level: INFO
100
+ handlers: [console, file, json]
101
+
102
+ disable_existing_loggers: false
@@ -0,0 +1,12 @@
1
+ from hydra.utils import instantiate
2
+ from structlog.stdlib import ProcessorFormatter
3
+
4
+ from .init import init_plugin
5
+
6
+
7
+ def build_processor_formatter(**kwargs) -> ProcessorFormatter:
8
+ """Dynamically instantiate the processor chain from plain logging config."""
9
+ kwargs.setdefault("_target_", "structlog.stdlib.ProcessorFormatter")
10
+ processor = instantiate(kwargs)
11
+ init_plugin(processor.foreign_pre_chain)
12
+ return processor
@@ -0,0 +1,34 @@
1
+ import logging
2
+ from typing import Any, TextIO
3
+
4
+ try:
5
+ from tqdm.std import tqdm as std_tqdm # type: ignore[ty:unresolved-import]
6
+
7
+ TqdmClass = type[std_tqdm]
8
+ except ImportError:
9
+ std_tqdm: Any = None
10
+ TqdmClass = None
11
+
12
+
13
+ class TqdmStreamHandler(logging.StreamHandler):
14
+ """Handler inheriting from StreamHandler to route logging stream through tqdm."""
15
+
16
+ def __init__(
17
+ self,
18
+ stream: TextIO | None = None,
19
+ tqdm_class: TqdmClass = std_tqdm,
20
+ ):
21
+ super().__init__(stream=stream)
22
+ self.tqdm_class = tqdm_class
23
+
24
+ if std_tqdm is not None:
25
+
26
+ def emit(self, record: logging.LogRecord):
27
+ try:
28
+ msg = self.format(record)
29
+ self.tqdm_class.write(msg, end=self.terminator, file=self.stream) # type: ignore
30
+ self.flush()
31
+ except (KeyboardInterrupt, RecursionError, SystemExit):
32
+ raise
33
+ except Exception:
34
+ self.handleError(record)
@@ -0,0 +1,90 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import warnings
5
+ from collections.abc import Sequence
6
+ from types import TracebackType
7
+ from typing import Any, TextIO
8
+
9
+ import structlog
10
+
11
+ # Original warnings.showwarning and sys.excepthook
12
+ _showwarning = None
13
+ _excepthook = None
14
+ _configured_once = False
15
+
16
+
17
+ def warning_logger(
18
+ message: Warning | str,
19
+ category: type[Warning],
20
+ filename: str,
21
+ lineno: int,
22
+ file: TextIO | None = None,
23
+ line: str | None = None,
24
+ ) -> None:
25
+ """Emit Python-native warnings as logging warnings."""
26
+ frame = sys._getframe(2)
27
+ module = frame.f_globals.get("__name__")
28
+ logger = logging.getLogger(module)
29
+ logger.warning("%s: %s", category.__name__, f"{message} (line {lineno})")
30
+
31
+
32
+ def exception_logger(
33
+ exc_type: type[BaseException],
34
+ exc_value: BaseException,
35
+ exc_traceback: TracebackType | None,
36
+ ) -> Any:
37
+ """Exception hook to log uncaught exceptions."""
38
+ logger = logging.getLogger("exception")
39
+
40
+ if issubclass(exc_type, KeyboardInterrupt):
41
+ logger.error("User interrupt")
42
+ return
43
+
44
+ # Skip Hydra frames in stack trace (see hydra._internal.utils.run_and_report)
45
+ skip_files = (
46
+ "hydra/core/utils.py", # hydra
47
+ "hydra_zen/wrapper/_implementations.py", # hydra-zen
48
+ "hydra/main.py", # hydra through hydra-zen
49
+ "pydantic/_internal/_validate_call.py", # hydra-zen with pydantic wrapper
50
+ )
51
+ try:
52
+ tb = exc_traceback
53
+ while tb is not None:
54
+ frame = tb.tb_frame
55
+ tb = tb.tb_next
56
+ filename = frame.f_code.co_filename.replace(os.sep, "/")
57
+ if filename.endswith(skip_files) and tb:
58
+ exc_traceback = tb
59
+ except Exception:
60
+ pass
61
+
62
+ message = getattr(exc_value, "message", str(exc_value))
63
+ exc_info = exc_type, exc_value, exc_traceback
64
+ logger.critical(message, exc_info=exc_info)
65
+
66
+
67
+ def init_plugin(foreign_pre_chain: Sequence[structlog.typing.Processor]) -> None:
68
+ """Configure structlog and warning and exception hooks."""
69
+ global _excepthook, _showwarning, _configured_once
70
+
71
+ if _showwarning is None:
72
+ _showwarning = warnings.showwarning
73
+ warnings.showwarning = warning_logger # type: ignore[ty:invalid-assignment]
74
+
75
+ if _excepthook is None:
76
+ os.environ["HYDRA_FULL_ERROR"] = "1" # Suppress Hydra's exception hook
77
+ _excepthook = sys.excepthook
78
+ sys.excepthook = exception_logger
79
+
80
+ if _configured_once is False:
81
+ structlog.configure(
82
+ processors=(
83
+ list(foreign_pre_chain)
84
+ + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter]
85
+ ),
86
+ logger_factory=structlog.stdlib.LoggerFactory(),
87
+ wrapper_class=structlog.stdlib.BoundLogger,
88
+ cache_logger_on_first_use=True,
89
+ )
90
+ _configured_once = True
@@ -0,0 +1,7 @@
1
+ from hydra.core.config_search_path import ConfigSearchPath
2
+ from hydra.plugins.search_path_plugin import SearchPathPlugin
3
+
4
+
5
+ class HydraStructlogSearchPathPlugin(SearchPathPlugin):
6
+ def manipulate_search_path(self, search_path: ConfigSearchPath) -> None:
7
+ search_path.append("hydra-structlog", "pkg://hydra_structlog.conf")
@@ -0,0 +1,43 @@
1
+ from collections.abc import Callable, MutableMapping
2
+ from typing import Any
3
+
4
+ from structlog.typing import EventDict, WrappedLogger
5
+
6
+
7
+ class StaticFieldAdder:
8
+ """Structlog processor to add static fields to all logs."""
9
+
10
+ def __init__(self, **fields: str):
11
+ self.fields = fields
12
+
13
+ def __call__(
14
+ self, logger: WrappedLogger, name: str, event_dict: EventDict
15
+ ) -> EventDict:
16
+ for field, value in self.fields.items():
17
+ event_dict.setdefault(field, value)
18
+ return event_dict
19
+
20
+
21
+ class FieldRenamer:
22
+ """Structlog processor to rename fields. Existing fields of the new name are
23
+ overwritten.
24
+ """
25
+
26
+ def __init__(self, **mapping: str):
27
+ self.mapping = mapping
28
+
29
+ def __call__(
30
+ self, logger: WrappedLogger, name: str, event_dict: EventDict
31
+ ) -> EventDict:
32
+ for field, replace in self.mapping.items():
33
+ if field in event_dict:
34
+ event_dict[replace] = event_dict.pop(field)
35
+ return event_dict
36
+
37
+
38
+ Processor = Callable[[Any, str, MutableMapping], MutableMapping]
39
+
40
+
41
+ def named_processor_chain(**processors: Processor) -> list[Processor]:
42
+ """Convert a mapping of named processors into list. Skip None"""
43
+ return [p for p in processors.values() if p is not None]
File without changes
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: hydra-structlog
3
+ Version: 0.1.0
4
+ Summary: An opinionated hydra plugin to configure logging with structlog
5
+ Keywords: hydra,structlog,plugin,hydra-zen
6
+ Author: Sören Zapp
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Dist: hydra-core>=1.3.2
21
+ Requires-Dist: structlog>=25.5.0
22
+ Requires-Python: >=3.10
23
+ Project-URL: Source, https://github.com/szapp/hydra-structlog
24
+ Project-URL: Issues, https://github.com/szapp/hydra-structlog/issues
25
+ Project-URL: Funding, https://ko-fi.com/szapp
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Hydra-Structlog
29
+
30
+ An opinionated hydra plugin to configure logging with structlog.
31
+
32
+ ## Usage
33
+
34
+ Structlog is fully integrated in Hydra through a config group named `structlog` for `hydra.job_logging`.
35
+
36
+ 1. Install the plugin.
37
+
38
+ ```sh
39
+ uv add hydra-structlog
40
+ ```
41
+
42
+ 2. Override the job logging with a default in your config.
43
+
44
+ ```yaml
45
+ defaults:
46
+ - override hydra/job_logging=structlog
47
+ ```
48
+
49
+ 3. Hydra will now emit JSON logs (by default to `structured.log`) and print human readably logs to the console.
50
+
51
+ ## Customization
52
+
53
+ Structlog comes configured with opinionated defaults.
54
+
55
+ To adjust the processors, use the hydra defaults as outlined above and overwrite individual config entries afterwards.
56
+ For example:
57
+
58
+ ```yaml
59
+ defaults:
60
+ - override hydra/job_logging=structlog
61
+
62
+ # Change the storage location
63
+ hydra:
64
+ job_logging:
65
+ handlers:
66
+ json:
67
+ filename: ${hydra.runtime.cwd}/logs/structured.log
68
+ ```
69
+
70
+ ## Defaults
71
+
72
+ The configuration runs structlog through the standard library logging.
73
+ That has several advantages.
74
+
75
+ The use of structlog and logging, e.g. by third-party packages, are recorded and both exhibit structlog's bound contextvars.
76
+
77
+ The processor chain is conveniently exposed in the job_logging configuration and adjustable through hydra's config interface.
78
+
79
+ Because lists are not easily manipulated in hydra configs, the processor chains are defined as dictionaries.
80
+ The name of each processor does not matter as long as its unique in the chain.
81
+ The default processor chain can then be extended more comfortably.
82
+
83
+ To remove a processor set it to `null`. See example below.
84
+
85
+ Because structlog's ProcessorFormatter does not accept the processors as strings, the logging config contains `_target_` instantiation.
86
+
87
+ ```yaml
88
+ defaults:
89
+ - override hydra/job_logging=structlog
90
+
91
+ # Remove a processor from the default chain and add another one
92
+ hydra:
93
+ job_logging:
94
+ formatters:
95
+ json:
96
+ foreign_pre_chain:
97
+ # Remove the add-log-level processor
98
+ add_logger_name: null
99
+ # Append a new processor
100
+ arbitrary_name:
101
+ # Follow hydra's instantiate syntax
102
+ _target_: structlog.stdlib.add_log_level_number
103
+ _partial_: true
104
+ ```
105
+
106
+ ## Notes
107
+
108
+ Python 3.14 is currently not supported as `hydra.main` causes a ValueError on argparse-validation, see <https://github.com/facebookresearch/hydra/issues/3121>.
109
+ It is recommended to use `hydra-zen` which does not have that problem and works well with Python 3.14.
110
+
111
+ Tracer injection is not implemented here as data science projects using hydra typically do not implement web apps.
112
+ Nevertheless, an appropriate structlog processor can be added to the job_logging config.
@@ -0,0 +1,15 @@
1
+ hydra_plugins/hydra_structlog/__init__.py,sha256=b8nxszR3JwULO_rfmSkOs27b1QwJPM_dOHpmiRTXIP0,119
2
+ hydra_plugins/hydra_structlog/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ hydra_structlog/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ hydra_structlog/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ hydra_structlog/conf/hydra/job_logging/structlog.yaml,sha256=3U247Abw0ca6wN3END6ZFl8STk1-P7Wa_C6HKrJvBhk,3372
6
+ hydra_structlog/formatters.py,sha256=Tff7FtD9CQFHxhobGMrD2I94R8sldCIaL4Nng_zuvOc,436
7
+ hydra_structlog/handlers.py,sha256=2jR9CNCKPpFBpiN9fcUwza5VvRB-k1Fazyi60_jKVr4,992
8
+ hydra_structlog/init.py,sha256=xgz375XzRfcZ-8CFe4ewCKRp88ShRdnt6ax4zw2do2s,2856
9
+ hydra_structlog/plugin.py,sha256=o0QZHx2Grx8i8_LfskMrh0io4dQurvftKIpk65vuTPU,332
10
+ hydra_structlog/processors.py,sha256=vXW42l39Spaa32YYY2jnVJEs0R2sbsJ7oxo1vCB7nro,1275
11
+ hydra_structlog/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hydra_structlog-0.1.0.dist-info/licenses/LICENSE,sha256=748QfVuRg6qe0w5Byrj-yYr0coH58yJGP1WinEvDVvQ,1069
13
+ hydra_structlog-0.1.0.dist-info/WHEEL,sha256=M4DeIjVCA49okfALADZoWX5JOGwnmHb-JOpQHtI-1c0,80
14
+ hydra_structlog-0.1.0.dist-info/METADATA,sha256=jCqNB3W-5Xl9JOZJQZZFcjS4ECHOaWSBVTqNDd-tD-8,3803
15
+ hydra_structlog-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sören Zapp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.