Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pip install logurich[click]
```python
from rich.panel import Panel

from logurich import init_logger, logger, shutdown_logger
from logurich import init_logger, logger

init_logger("INFO", enqueue=False)

Expand Down Expand Up @@ -46,11 +46,12 @@ logger.info(
},
)

shutdown_logger()
```

`logger.ctx(...)` is shorthand for the existing module-level `ctx(...)` helper. `logger.contextualize(...)` is a convenience alias for `global_context_configure(...)`. The module-level helpers remain supported if you prefer `global_context_configure(...)` or `extra={"context": {"key": ctx(...)}}`.

For short-lived scripts and CLIs, `init_logger()` automatically registers an `atexit` hook, so you do not need to call `shutdown_logger()` just to flush logs at process exit.

## Using Logurich in Reusable Libraries

If you are writing a Python library that will be imported by another program, the library should not call `init_logger()` on its own. Let the main application own logging configuration, handler setup, and shutdown.
Expand Down Expand Up @@ -88,14 +89,12 @@ Then configure Logurich once in the main program:

```python
# main.py
from logurich import init_logger, shutdown_logger
from logurich import init_logger
from mylib.service import run_job

init_logger("INFO", enqueue=False)

run_job("job-42")

shutdown_logger()
```

Guidelines for libraries:
Expand All @@ -114,7 +113,7 @@ When `enqueue=True`, Logurich is process-safe only if worker processes send reco
import logging
import multiprocessing as mp

from logurich import configure_child_logging, get_log_queue, init_logger, shutdown_logger
from logurich import configure_child_logging, get_log_queue, init_logger


def worker(log_queue: mp.Queue, worker_id: int) -> None:
Expand All @@ -137,11 +136,12 @@ def main() -> None:
for process in processes:
process.join()

shutdown_logger()
```

Only the process that calls `init_logger(..., enqueue=True)` owns the console and file handlers. Child processes must call `configure_child_logging(queue)` before logging.

Call `shutdown_logger()` explicitly only when you need deterministic teardown before process exit, such as in tests or when reconfiguring logging multiple times in the same interpreter.

## Click CLI helper

Install the optional Click extra to automatically expose logger configuration flags inside your commands:
Expand Down
6 changes: 1 addition & 5 deletions examples/async_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
get_log_queue,
global_context_configure,
init_logger,
shutdown_logger,
)

request_log = logging.getLogger("example.request")
Expand Down Expand Up @@ -90,7 +89,4 @@ async def main(log_queue: mp.Queue) -> None:
if __name__ == "__main__":
init_logger("INFO", enqueue=True)
log_queue = get_log_queue()
try:
asyncio.run(main(log_queue))
finally:
shutdown_logger()
asyncio.run(main(log_queue))
4 changes: 1 addition & 3 deletions examples/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rich.panel import Panel
from rich.table import Table

from logurich import ctx, init_logger, logger, shutdown_logger
from logurich import ctx, init_logger, logger


def create_rich_table() -> Table:
Expand Down Expand Up @@ -53,5 +53,3 @@ def create_rich_table() -> Table:
request_id=ctx("req-99", style="cyan", show_key=True),
)
req_logger.info("Handling request")

shutdown_logger()
3 changes: 0 additions & 3 deletions examples/mp_adv_data_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
global_context_configure,
global_context_set,
init_logger,
shutdown_logger,
)


Expand Down Expand Up @@ -164,8 +163,6 @@ def main():
extra={"renderables": (results_table, sample_results)},
)

shutdown_logger()


if __name__ == "__main__":
mp.set_start_method("spawn", force=True)
Expand Down
3 changes: 0 additions & 3 deletions examples/mp_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
get_log_queue,
global_context_configure,
init_logger,
shutdown_logger,
)


Expand Down Expand Up @@ -109,8 +108,6 @@ def main() -> None:
process.pid,
)

shutdown_logger()


if __name__ == "__main__":
mp.set_start_method("spawn", force=True)
Expand Down
4 changes: 1 addition & 3 deletions examples/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rich.panel import Panel
from rich.table import Table

from logurich import ctx, global_context_configure, init_logger, logger, shutdown_logger
from logurich import ctx, global_context_configure, init_logger, logger


def build_table() -> Table:
Expand Down Expand Up @@ -52,5 +52,3 @@ def build_table() -> Table:
raise RuntimeError("serialize example failure")
except RuntimeError:
logger.exception("Exception payload")

shutdown_logger()
11 changes: 11 additions & 0 deletions src/logurich/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import atexit
import contextlib
import contextvars
import copy
Expand Down Expand Up @@ -668,6 +669,15 @@ def shutdown_logger() -> None:
_context_state.set({})


def _ensure_shutdown_atexit_registered() -> None:
"""Register ``shutdown_logger`` once so handlers are flushed on process exit."""

if logger_state.get("atexit_registered"):
return
atexit.register(shutdown_logger)
logger_state["atexit_registered"] = True


def get_log_queue() -> mp.Queue:
"""Return the active multiprocessing queue used for logging."""

Expand Down Expand Up @@ -721,6 +731,7 @@ def init_logger(
) -> Optional[str]:
"""Initialize stdlib logging with optional Rich rendering and queue support."""

_ensure_shutdown_atexit_registered()
shutdown_logger()

env_rich_handler = parse_bool_env("LOGURICH_RICH")
Expand Down
2 changes: 2 additions & 0 deletions src/logurich/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
"queue": None,
"listener": None,
"final_handlers": (),
"env_extra": {},
"atexit_registered": False,
}
15 changes: 15 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
logger as exported_logger,
)
from logurich.console import rich_configure_console
from logurich.struct import logger_state


@pytest.mark.parametrize(
Expand All @@ -33,6 +34,20 @@ def test_level_info(logger, buffer):
assert "Debug, world!" not in output


def test_init_logger_registers_atexit_shutdown_once(monkeypatch):
registered: list[object] = []

monkeypatch.setitem(logger_state, "atexit_registered", False)
monkeypatch.setattr("logurich.core.atexit.register", registered.append)

init_logger("INFO", enqueue=False)
shutdown_logger()
init_logger("INFO", enqueue=False)
shutdown_logger()

assert registered == [shutdown_logger]


@pytest.mark.parametrize(
"logger",
[{"level": "DEBUG", "enqueue": False}, {"level": "DEBUG", "enqueue": True}],
Expand Down
19 changes: 19 additions & 0 deletions tests/test_opt_click.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

click = pytest.importorskip("click")

from logurich import shutdown_logger # noqa: E402
from logurich.opt_click import click_logger_init # noqa: E402
from logurich.struct import logger_state # noqa: E402


def test_click_logger_init_registers_atexit_shutdown(monkeypatch):
registered: list[object] = []

monkeypatch.setitem(logger_state, "atexit_registered", False)
monkeypatch.setattr("logurich.core.atexit.register", registered.append)

click_logger_init("INFO", 0, None, (), False)
shutdown_logger()

assert registered == [shutdown_logger]
Loading