# Logging API Inventory

Technical inventory of the main APIs for the phased `pytools.logging` subsystem.

## Design Contract

- **Source of truth:** `LoggingState` in `state.py`
- **Pure/data stages:** `bootstrap`, `resolve`, `mutate`, `export`
- **Runtime side effects only:** `apply_config`, `apply_state`
- **App-specific precedence logic:** stays outside this package (for example in `ai_server/cli.py`)

## Handlers and dictConfig

`LoggingState.handlers` mirrors the `handlers` section of a `logging.config.dictConfig` configuration.

- Values are **dicts** describing how to build a handler (for example `"class": "..."` with kwargs, or `"()": "import.path.to_callable"` for a **factory** that returns a `Handler`).
- **Do not** store live `logging.Handler` instances in `LoggingState`. They are not part of the dictConfig contract and will not serialize for `export_config` or subprocesses.
- The `"()"` value must be a string **dotted path** to a callable importable in the target process. The stdlib loads it and invokes it (see Python docs: *Logging Configuration*). A factory with no parameters is typical; extra keys in the handler dict can be passed as keyword arguments to that callable per dictConfig behavior.
- **Closures:** a factory function may use module-level state or default arguments populated at import time; what cannot be embedded in exported config is an arbitrary in-memory object graph.

When `bootstrap` is called **without** `handlers`, it seeds a single handler id `default` with:

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

When `handlers` **is** provided, **every** handler id in that dict is registered on the root logger (same rule as the default case: `root["handlers"]` is the list of those keys).

## Core Data Model

### `pytools.logging.state.LoggingState`

Authoritative in-memory logging configuration.

Fields:

- `version: int` (dictConfig version, default `1`)
- `disable_existing_loggers: bool`
- `formatters: dict[str, dict[str, Any]]`
- `handlers: dict[str, dict[str, Any]]` — dictConfig-style handler definitions, not handler instances
- `root: dict[str, Any]` (`level`, `handlers`)
- `loggers: dict[str, dict[str, Any]]`

## Bootstrap APIs

### `pytools.logging.bootstrap.bootstrap(...) -> LoggingState`

Creates baseline logging state without mutating Python runtime.

Parameters:

- `root_level: int = logging.WARNING`
- `logger_levels: dict[str, int] | None = None` — mapped to `loggers` with `{"level": ...}` only (propagation to root)
- `formatters: dict[str, dict[str, Any]] | None = None`
- `handlers: dict[str, dict[str, Any]] | None = None` — if omitted, uses built-in default factory as above; if provided, keys become root handler ids
- `disable_existing_loggers: bool = False`
- `rich: bool = False` — selects built-in Rich vs colored stderr default when `handlers` is omitted

Behavior:

- Seeds one default handler when `handlers` is omitted (colored stderr, or Rich when `rich=True`).
- Sets `root["handlers"]` to all handler keys in use.

Side effects: **none**

## Resolve APIs

### `pytools.logging.resolve.resolve_logging_env_path(explicit_path=None, *, env_var="PYTOOLS_LOGGING_CONFIG_FILE") -> str | None`

Resolves optional config file path using precedence:

1. explicit path argument
2. environment variable (`PYTOOLS_LOGGING_CONFIG_FILE` by default)
3. `{project_root}/.env.pytools_logging`

Side effects: **none** (read-only discovery)

## Mutate APIs

All mutate APIs return a copied `LoggingState` with updates.

### `pytools.logging.mutate.set_root_level(state, level) -> LoggingState`

- updates `state.root["level"]`

### `pytools.logging.mutate.set_root_handlers(state, handlers) -> LoggingState`

- updates `state.root["handlers"]`

### `pytools.logging.mutate.merge_logger_levels(state, levels) -> LoggingState`

- merges `{logger_name: level}` into `state.loggers`
- overwrites existing level for matching logger keys

### `pytools.logging.mutate.merge_logger_specs(state, specs) -> LoggingState`

- shallow-merges dictConfig logger fragments into `state.loggers[name]`

Side effects: **none**

## Export APIs

### `pytools.logging.export.export_config(state) -> dict`

Pure conversion from `LoggingState` to stdlib `dictConfig` mapping:

- deep-copies state payloads
- produces portable config artifact for subprocess runtimes (for example Uvicorn workers)

Side effects: **none** (the implementation may emit log lines; configuration remains pure)

## Apply APIs (Activation Boundary)

### `pytools.logging.apply.apply_config(config: dict) -> None`

Activates logging runtime with `logging.config.dictConfig(config)`.

Side effects: **yes** (mutates Python logging runtime)

### `pytools.logging.apply.apply_state(state: LoggingState) -> None`

Equivalent to:

1. `export_config(state)`
2. `apply_config(config)`

Side effects: **yes**

## Public Re-Exports (`pytools.logging`)

Main symbols exported via `pytools.logging`:

- `LoggingState`
- `bootstrap`
- `resolve_logging_env_path`
- `merge_logger_levels`
- `merge_logger_specs`
- `set_root_level`
- `set_root_handlers`
- `export_config`
- `apply_config`
- `apply_state`

(Additional exports such as `one_shot`, formatters, and helpers appear in `__init__.py`; see source for the full `__all__`.)
