Metadata-Version: 2.1
Name: kitty_logger
Version: 0.1.0
Summary: Cross-process logging via a dedicated log server process and SocketHandler.
Author: Kitty
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"

# kitty_logger

Cross-process Python logger. The main process starts a dedicated log-server
subprocess; every other process (forked or spawned) ships `LogRecord`s to it
via `logging.handlers.SocketHandler`, so all log lines end up in one place
with no interleaving or file-locking issues.

## Install

```bash
pip install .
```

## Usage

```python
# main.py
import multiprocessing as mp
import kitty_logger

def worker(i):
    log = kitty_logger.getLogger(f"worker.{i}")
    log.info("hello from worker %d", i)

if __name__ == "__main__":
    kitty_logger.setup_logging(log_file="app.log")
    log = kitty_logger.getLogger("main")
    log.info("starting")

    with mp.get_context("spawn").Pool(4) as pool:
        pool.map(worker, range(4))
```

`setup_logging` sets the env vars `KITTY_LOGGER_HOST` / `KITTY_LOGGER_PORT`,
which child processes inherit and use to connect.

## API

- `setup_logging(log_file=None, level=logging.INFO, host="127.0.0.1", port=0, stream=True, console_fmt=..., file_fmt=..., datefmt=None, attach_main_logger=True) -> (host, port)`
  Starts the log-server subprocess (always uses the `spawn` start method).
  Idempotent. Registered with `atexit`. `port=0` lets the OS pick a free port;
  the actual `(host, port)` is returned.
- `getLogger(name=None) -> logging.Logger`
  Returns a logger with a `SocketHandler` pointing at the server.
- `shutdown_logging()` — stop the server subprocess explicitly.

## Why spawn-only

Child processes started with `fork` inherit the parent's already-connected
`SocketHandler` (and its TCP fd). Parent and child would then interleave
pickled-record bytes on the same connection, which corrupts every record on
the wire. `fork` is also unsafe in multi-threaded parents and unavailable on
Windows. KittyLogger therefore only supports `spawn`. Use
`multiprocessing.get_context("spawn")` (or just rely on Python's defaults
under `if __name__ == "__main__":`).

## Security

The server uses `pickle` to deserialize incoming `LogRecord`s. **Only ever bind
to a loopback address** (the default `127.0.0.1`). Binding to `0.0.0.0` or any
externally reachable interface exposes a remote-code-execution surface to anyone
who can reach the port.

## Caveats

- Do **not** call `logging.basicConfig()` (or otherwise attach a `StreamHandler`
  to the root logger) before `setup_logging(attach_main_logger=True)` — the main
  process would emit each record twice: once via the local root handler and
  once via the log-server. Either let kitty_logger be the only configurer, or
  pass `attach_main_logger=False` and manage main-process handlers yourself.
- After `shutdown_logging()`, kitty_logger removes its own `SocketHandler`s
  from this process's loggers and clears `KITTY_LOGGER_*` env vars, so the
  process state is symmetric with `setup_logging`. If you attach extra handlers
  yourself, you remain responsible for cleaning them up.
