# Python Logging Infrastructure

`pytools.logging` is a phased, state-driven logging subsystem.

It is built for:

- single-process scripts and CLIs
- API servers (including Uvicorn workers)
- deterministic cross-process logging behavior

## High-Level Model

The package separates configuration from runtime mutation:

1. `bootstrap(...)` defines baseline `LoggingState` (pure data; no I/O).
2. `resolve_logging_env_path(...)` discovers optional `.env.pytools_logging` path.
3. Mutate helpers update state (`merge_logger_levels`, `set_root_level`, etc.).
4. `apply_state(...)` activates runtime logging via `logging.config.dictConfig`.
5. `export_config(...)` serializes state to a dict for other processes (for example Uvicorn `log_config`).

Only `apply_config(...)` / `apply_state(...)` perform logging side effects.

## Handlers: dictConfig shape and factory callables

`LoggingState.handlers` is a mapping of **handler id → handler definition dict**, in the same sense as the `handlers` key in `logging.config.dictConfig`.

- You do **not** put a constructed `logging.Handler` instance inside `LoggingState`. The pipeline compiles state to a dict and passes it to `dictConfig`, which expects class paths, factory callables identified by import string, or stdlib-supported patterns—not arbitrary live objects.
- For a custom sink, define a **factory**: a callable (typically with no parameters) that **returns** a `Handler`. Reference it with the `"()"` key and a **dotted import path** string, for example `{"default": {"()": "myapp.logging_handlers.build_console_handler"}}`. At apply time, the logging framework imports `build_console_handler` and calls it. Using closures over module-level configuration in that module is fine; what must not be embedded in the state is the handler instance itself.

Default handler when `bootstrap` is called without `handlers`:

- `rich=False`: `pytools.logging.factories.create_colored_level_handler`
- `rich=True`: `pytools.logging.factories.create_rich_console_handler`

When you pass `handlers=...`, `bootstrap` attaches **all** keys in that dict to the root (same as default behavior: root uses every handler id you define).

See [USAGE.md](./USAGE.md) for end-to-end examples (including Typer and custom factories). See [API.md](./API.md) for a full symbol-level inventory.

## Core Types and Modules

- `state.py`: `LoggingState` source of truth
- `bootstrap.py`: pure baseline state definition
- `resolve.py`: path discovery (`explicit > env var > project_root/.env.pytools_logging`)
- `mutate.py`: controlled, copy-safe state overlays
- `export.py`: pure `LoggingState` → `dictConfig` mapping
- `apply.py`: explicit runtime activation boundary
- `factories.py`: stable callable factories referenced by dictConfig `"()"` entries

## Quickstart

```python
import logging

from pytools.logging import bootstrap, merge_logger_levels, set_root_level
from pytools.logging.apply import apply_state

state = bootstrap(root_level=logging.WARNING)
state = merge_logger_levels(state, {"my.module": logging.DEBUG})
state = set_root_level(state, logging.INFO)
apply_state(state)
```

## Path Resolution Behavior

`resolve_logging_env_path(...)` uses:

1. explicit path argument
2. `PYTOOLS_LOGGING_CONFIG_FILE`
3. `{project_root}/.env.pytools_logging`

If none exists, it returns `None` and the app continues safely without file-based overrides.

## Debugging the Logging System

In a consuming application, if you want to debug initialization (resolve, mutate, export, etc.), pass `pytools.logging.*` logger specs into `bootstrap` (or `one_shot`); `.env.pytools_logging` alone does not change bootstrap defaults by itself.

Alternatively, set levels before bootstrap:

```python
import logging

logging.getLogger("pytools.logging").setLevel(logging.DEBUG)
```

## See Also

- [Usage examples and handler factories](./USAGE.md)
- [API inventory](./API.md)
