logdruid 0.1.0.dev2__tar.gz → 0.1.0.dev3__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
  MIT License
2
2
 
3
- Copyright (c) 2026 Frank Herbrand
3
+ Copyright (c) 2026 github.com/fherb2
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,19 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logdruid
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev3
4
4
  Summary: A drop-in logging setup layer for multi-process applications, built on loguru.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
- Author: Frank Herbrand
8
- Author-email: f.herbrand@hzdr.de
7
+ Author: github.com/fherb2
9
8
  Requires-Python: >=3.11,<4.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Operating System :: POSIX :: Linux
10
11
  Classifier: Programming Language :: Python :: 3
11
12
  Classifier: Programming Language :: Python :: 3.11
12
13
  Classifier: Programming Language :: Python :: 3.12
13
14
  Classifier: Programming Language :: Python :: 3.13
14
- Classifier: Programming Language :: Python :: 3.14
15
- Classifier: Programming Language :: Python :: 3.15
16
15
  Requires-Dist: loguru (>=0.7.3)
16
+ Project-URL: Changelog, https://fherb2.github.io/logdruid/changelog/
17
+ Project-URL: Documentation, https://fherb2.github.io/logdruid/
18
+ Project-URL: Issues, https://github.com/fherb2/logdruid/issues
19
+ Project-URL: Repository, https://github.com/fherb2/logdruid
17
20
  Description-Content-Type: text/markdown
18
21
 
19
22
  # logdruid
@@ -24,8 +27,7 @@ Description-Content-Type: text/markdown
24
27
  A drop-in logging setup layer for multi-process applications, built on
25
28
  [loguru](https://loguru.readthedocs.io/).
26
29
 
27
- **Installation, usage, configuration and API:
28
- <https://fherb2.github.io/logdruid/>**
30
+ **[Documentation](https://fherb2.github.io/logdruid/)** – installation, usage, configuration and API.
29
31
 
30
32
  ## What it is for
31
33
 
@@ -65,13 +67,12 @@ through `logging`.
65
67
  | Configuration from a file, precedence defaults < application < file < command line | no, only environment variables | yes |
66
68
  | Mistakes in the configuration do not stop the application | no | yes, they only produce a warning |
67
69
 
68
- ## For contributors
70
+ ## Documentation
69
71
 
70
- Design rules and rationale are in
71
- [`running_implementation_doc/`](https://github.com/fherb2/logdruid/tree/main/running_implementation_doc)
72
- (in German).
72
+ Installation, usage, configuration, the API reference and the changelog:
73
+ <https://fherb2.github.io/logdruid/>
73
74
 
74
75
  ## License
75
76
 
76
- MIT, see [`LICENSE`](https://github.com/fherb2/logdruid/blob/main/LICENSE).
77
+ MIT, see [LICENSE](https://github.com/fherb2/logdruid/blob/main/LICENSE).
77
78
 
@@ -6,8 +6,7 @@
6
6
  A drop-in logging setup layer for multi-process applications, built on
7
7
  [loguru](https://loguru.readthedocs.io/).
8
8
 
9
- **Installation, usage, configuration and API:
10
- <https://fherb2.github.io/logdruid/>**
9
+ **[Documentation](https://fherb2.github.io/logdruid/)** – installation, usage, configuration and API.
11
10
 
12
11
  ## What it is for
13
12
 
@@ -47,12 +46,11 @@ through `logging`.
47
46
  | Configuration from a file, precedence defaults < application < file < command line | no, only environment variables | yes |
48
47
  | Mistakes in the configuration do not stop the application | no | yes, they only produce a warning |
49
48
 
50
- ## For contributors
49
+ ## Documentation
51
50
 
52
- Design rules and rationale are in
53
- [`running_implementation_doc/`](https://github.com/fherb2/logdruid/tree/main/running_implementation_doc)
54
- (in German).
51
+ Installation, usage, configuration, the API reference and the changelog:
52
+ <https://fherb2.github.io/logdruid/>
55
53
 
56
54
  ## License
57
55
 
58
- MIT, see [`LICENSE`](https://github.com/fherb2/logdruid/blob/main/LICENSE).
56
+ MIT, see [LICENSE](https://github.com/fherb2/logdruid/blob/main/LICENSE).
@@ -4,19 +4,33 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "logdruid"
7
- version = "0.1.0.dev2"
7
+ version = "0.1.0.dev3"
8
8
  description = "A drop-in logging setup layer for multi-process applications, built on loguru."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<4.0"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [
14
- { name = "Frank Herbrand", email = "f.herbrand@hzdr.de" },
14
+ { name = "github.com/fherb2" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
15
23
  ]
16
24
  dependencies = [
17
25
  "loguru>=0.7.3",
18
26
  ]
19
27
 
28
+ [project.urls]
29
+ Documentation = "https://fherb2.github.io/logdruid/"
30
+ Repository = "https://github.com/fherb2/logdruid"
31
+ Issues = "https://github.com/fherb2/logdruid/issues"
32
+ Changelog = "https://fherb2.github.io/logdruid/changelog/"
33
+
20
34
  [tool.poetry]
21
35
  packages = [{ include = "logdruid", from = "src" }]
22
36
 
@@ -0,0 +1,22 @@
1
+ """A logging setup layer for multi-process applications, built on loguru.
2
+
3
+ Reporting uses loguru's own logger unchanged. logdruid sets it up and adds
4
+ one time-ordered output for all processes, the takeover of the standard
5
+ ``logging`` module, and a configuration file of its own.
6
+
7
+ Documentation: https://fherb2.github.io/logdruid/
8
+ """
9
+
10
+ from .logdruid import (
11
+ CONFIG_FILE_NAME,
12
+ LogLink,
13
+ attach,
14
+ check_config,
15
+ link,
16
+ logger,
17
+ setup,
18
+ shutdown,
19
+ )
20
+
21
+ __all__ = ["logger", "setup", "check_config", "link", "attach", "shutdown",
22
+ "LogLink", "CONFIG_FILE_NAME"]
@@ -24,8 +24,12 @@ from loguru import logger
24
24
 
25
25
  # --- Module parameters ------------------------------------------------
26
26
 
27
- # Name of the configuration file looked for next to the entry point.
28
27
  CONFIG_FILE_NAME = "logdruid.toml"
28
+ """Name of the configuration file setup() looks for next to the entry point."""
29
+
30
+ # Name under which logdruid writes its own warnings. They bypass the
31
+ # minimum levels: a level set too high must not hide a configuration error.
32
+ OWN_NAME = "logdruid"
29
33
 
30
34
  # Lowest layer: what applies when neither API, file nor command line
31
35
  # say anything. No translation, one level for everything.
@@ -112,16 +116,8 @@ class LogLink:
112
116
  """What a child process needs to log into the main process.
113
117
 
114
118
  Created by link() in the main process and handed to the child as an
115
- argument; the child passes it to attach().
116
-
117
- Attributes
118
- ----------
119
- process_name : str
120
- Name the child process carries in every message.
121
- conn : multiprocessing.connection.Connection
122
- Write end of the child's own pipe.
123
- settings : Settings
124
- Resolved configuration of the main process.
119
+ argument; the child passes it to attach(). Its fields are internal;
120
+ only release() is part of the public interface.
125
121
  """
126
122
 
127
123
  process_name: str
@@ -171,15 +167,8 @@ def setup(config_path: Optional[Path] = None, *,
171
167
 
172
168
  mp.current_process().name = process_name
173
169
  warnings: List[str] = []
174
- file_layer = _read_config_file(config_path, warnings)
175
- cli_layer: Dict[str, Any] = {}
176
- if cli_level is not None:
177
- cli_layer["std_level"] = cli_level
178
- if cli_log_file is not None:
179
- cli_layer["output"] = {"file": {"path": cli_log_file}}
180
-
181
- raw_layers = [cli_layer, file_layer, defaults or {}, MODULE_DEFAULTS]
182
- _settings = _resolve(raw_layers, warnings)
170
+ _settings = _load_settings(config_path, defaults, cli_level, cli_log_file,
171
+ warnings)
183
172
  _threshold_cache.clear()
184
173
 
185
174
  logger.remove()
@@ -195,7 +184,33 @@ def setup(config_path: Optional[Path] = None, *,
195
184
  atexit.register(shutdown)
196
185
 
197
186
  for text in warnings:
198
- logger.warning(text)
187
+ _output.patch(_own_name).warning(text)
188
+
189
+
190
+ def check_config(config_path: Optional[Path] = None, *,
191
+ defaults: Optional[Dict[str, Any]] = None,
192
+ cli_level: Optional[str] = None,
193
+ cli_log_file: Optional[str] = None) -> List[str]:
194
+ """Check a configuration without setting anything up.
195
+
196
+ Resolves the same layers as setup() and returns the warnings setup()
197
+ would report about them. Whether an output can actually be created,
198
+ for example whether the path of the log file is writable, only setup()
199
+ finds out.
200
+
201
+ Parameters
202
+ ----------
203
+ config_path, defaults, cli_level, cli_log_file
204
+ As for setup().
205
+
206
+ Returns
207
+ -------
208
+ list of str
209
+ The warnings; empty if the configuration is sound.
210
+ """
211
+ warnings: List[str] = []
212
+ _load_settings(config_path, defaults, cli_level, cli_log_file, warnings)
213
+ return warnings
199
214
 
200
215
 
201
216
  def link(process_name: str) -> LogLink:
@@ -258,6 +273,22 @@ def shutdown() -> None:
258
273
 
259
274
  # --- Configuration ----------------------------------------------------
260
275
 
276
+ def _load_settings(config_path: Optional[Path],
277
+ defaults: Optional[Dict[str, Any]],
278
+ cli_level: Optional[str],
279
+ cli_log_file: Optional[str],
280
+ warnings: List[str]) -> Settings:
281
+ """Resolve the four layers into Settings, collecting the warnings."""
282
+ file_layer = _read_config_file(config_path, warnings)
283
+ cli_layer: Dict[str, Any] = {}
284
+ if cli_level is not None:
285
+ cli_layer["std_level"] = cli_level
286
+ if cli_log_file is not None:
287
+ cli_layer["output"] = {"file": {"path": cli_log_file}}
288
+ raw_layers = [cli_layer, file_layer, defaults or {}, MODULE_DEFAULTS]
289
+ return _resolve(raw_layers, warnings)
290
+
291
+
261
292
  def _read_config_file(config_path: Optional[Path],
262
293
  warnings: List[str]) -> Dict[str, Any]:
263
294
  """Return the file layer, or an empty one with a warning."""
@@ -528,6 +559,11 @@ def _take_over_std_logging(process: str) -> None:
528
559
 
529
560
  # --- Transport between the processes ------------------------------------
530
561
 
562
+ def _own_name(record: Dict[str, Any]) -> None:
563
+ """loguru patcher: mark a message as logdruid's own warning."""
564
+ record.update(name=OWN_NAME)
565
+
566
+
531
567
  def _to_item(message) -> Dict[str, Any]:
532
568
  """What travels for one message: its data, not loguru's objects."""
533
569
  record = message.record
@@ -595,7 +631,7 @@ class _Collector(threading.Thread):
595
631
  self._stopping = True
596
632
  self.join(timeout_s)
597
633
  if self.is_alive():
598
- self._output.warning(
634
+ self._output.patch(_own_name).warning(
599
635
  f"Logging pipes of {sorted(self._pipes.values())} did not "
600
636
  f"end within {timeout_s} s; their remaining messages are lost")
601
637
  with self._lock:
@@ -648,9 +684,9 @@ class _Collector(threading.Thread):
648
684
  name = self._pipes.pop(conn, "?")
649
685
  conn.close()
650
686
  if exc is not None:
651
- self._output.warning(f"Logging pipe of {name} broke off "
652
- f"({type(exc).__name__}); later messages "
653
- f"of that process are lost")
687
+ self._output.patch(_own_name).warning(
688
+ f"Logging pipe of {name} broke off ({type(exc).__name__}); "
689
+ f"later messages of that process are lost")
654
690
 
655
691
  def _push(self, item: Dict[str, Any]) -> None:
656
692
  """Queue a message by its time stamp."""
@@ -1,86 +0,0 @@
1
- """Logging setup for multi-process applications: a setup layer around loguru.
2
-
3
- Reporting uses loguru unchanged: `logger` is loguru's global logger.
4
- This module only sets it up, and adds what loguru does not offer:
5
-
6
- - One output in the main process. Console and file are created there
7
- only; every child process sends its messages through a pipe of its
8
- own. Unlike loguru's ``enqueue=True`` no lock is shared between
9
- processes, so a child that is killed hard blocks nobody but itself.
10
- - Ordering by time. The main process holds messages back for a short
11
- window and writes them sorted by their time stamp, its own included.
12
- - Taking over the standard ``logging`` module, with the logger name as
13
- the name of the message and with levels translated per logger.
14
- - Levels per process and per name, configured in ``logdruid.toml`` in
15
- four layers: module defaults < API < file < command line.
16
-
17
- Usage
18
- -----
19
- Main process, once, before any child process is created:
20
-
21
- >>> import logdruid
22
- >>> from logdruid import logger
23
- >>> logdruid.setup(config_path, cli_level=args.log_level)
24
- >>> logger.success("Logging active")
25
-
26
- Per child process, in the main process:
27
-
28
- >>> link = logdruid.link("raw_writer")
29
- >>> process = ctx.Process(target=raw_writer_prc, args=(link, ...),
30
- ... name="raw_writer")
31
- >>> process.start()
32
- >>> link.release()
33
-
34
- First thing in the child process (after ignoring SIGINT):
35
-
36
- >>> logdruid.attach(link)
37
-
38
- At the end of the main process, after the children have ended:
39
-
40
- >>> logdruid.shutdown()
41
-
42
- Configuration file (all keys optional)::
43
-
44
- std_level = "INFO" # level of everything without its own
45
- sort_window_ms = 100 # 0 switches the ordering off
46
-
47
- [output.console] # parameters of loguru's add(),
48
- enabled = true # passed through; 'enabled' and
49
- format = "..." # 'path' are logdruid's own
50
- diagnose = false
51
-
52
- [output.file]
53
- enabled = false
54
- path = "app.log"
55
- rotation = "10 MB"
56
- retention = 5
57
-
58
- [processes.raw_writer]
59
- level = "DEBUG"
60
-
61
- [loggers.ognbligg] # applies to ognbligg.* as well
62
- level = "INFO"
63
- translate = { DEBUG = "TRACE", INFO = false } # false drops
64
-
65
- Notes on further development
66
- ----------------------------
67
- - An application started through a packaged entry point has no
68
- ``logdruid.toml`` next to ``__main__``; it passes the path instead.
69
- - Exceptions travel through the pipe as text formatted in the child;
70
- ``backtrace`` and ``diagnose`` therefore act where a message arises.
71
- """
72
-
73
-
74
- from .logdruid import (
75
- CONFIG_FILE_NAME,
76
- LogLink,
77
- Settings,
78
- attach,
79
- link,
80
- logger,
81
- setup,
82
- shutdown,
83
- )
84
-
85
- __all__ = ["logger", "setup", "link", "attach", "shutdown", "LogLink",
86
- "Settings", "CONFIG_FILE_NAME"]