Metadata-Version: 2.4
Name: kitty_logger
Version: 0.2.0.dev1
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"
Dynamic: license-file

# kitty_logger

单机跨进程的 Python 日志库。主进程启动一个独立的"日志服务"子进程，所有
通过 `spawn` 创建的子进程把 `LogRecord` 经 `logging.handlers.SocketHandler`
发到这里，由它统一落盘 / 输出到 `stderr`，**不会出现多进程并发写文件的
错行或丢失问题**。

**适用范围。** 公司内部研发协作环境：单台主机、本机所有进程都互相信任、
追求"用起来简单"而不是"对抗外部威胁"。**不适用于**生产部署或多租户机器
等不能信任本机其他进程的场景。

## 安装

```bash
pip install kitty-logger
```

## 用法

```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` 会写入环境变量
`KITTY_LOGGER_HOST` / `KITTY_LOGGER_PORT` / `KITTY_LOGGER_LEVEL`；任意层级
`spawn` 出来的后代进程会自动继承，从而能在零配置的情况下连上日志服务。

## 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=False) -> (host, port)`
  启动日志服务子进程（始终使用 `spawn`）。幂等。已通过 `atexit` 注册清理。
  `port=0` 让操作系统挑选空闲端口；返回真实绑定到的 `(host, port)`。
  **`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
  默认**不动**主进程 root logger；想让某个 logger 走 kitty_logger，请显式
  `kitty_logger.getLogger(...)`。如果希望主进程 root 也自动挂上 SocketHandler，
  传 `attach_main_logger=True`。
- `getLogger(name=None, *, propagate=False) -> logging.Logger`
  返回一个挂好 `SocketHandler`、指向日志服务的 logger。
  默认 **`propagate=False`**：每个 logger 独立挂载，互不干扰；想让子 logger
  通过祖先链冒泡到这个 logger 时（`getLogger("myapp")` 接住 `myapp.svc.x` 等），
  传 `propagate=True`。对 root logger 该参数没有意义。
- `shutdown_logging()` — 显式停止日志服务子进程。

## 为什么只支持 spawn

`fork` 出来的子进程会继承父进程已经建立的 `SocketHandler` 及其底层 TCP
连接。父子进程会在同一个 socket 上交叉写 pickle 字节流，导致服务端反
序列化必然失败。此外 `fork` 在多线程父进程中不安全（其他线程持有的锁
会原样留在子进程里，引发死锁），且在 Windows 上不被支持。

请使用 `multiprocessing.get_context("spawn")`，或在顶层用
`if __name__ == "__main__":` 守卫配合 Python 默认行为。

## 注意事项

- 默认每个 logger 都是**显式加入、互不传播**的：业务代码里 `kitty_logger.getLogger(__name__)`
  得到的 logger 自带 `SocketHandler` 且 `propagate=False`，第三方库（urllib3、httpx 等）
  的日志不会被卷入。如果你更喜欢"包级挂一次、子模块走 stdlib + propagate"的传统模式，
  调 `kitty_logger.getLogger("myapp", propagate=True)` 即可。
- 如果传了 `attach_main_logger=True` 让主进程 root 也挂上 SocketHandler，请**不要**
  在此之前调用 `logging.basicConfig()` 或自行给 root 挂 `StreamHandler`，否则会出现
  双倍输出。要么让 kitty_logger 做唯一入口，要么保持默认 `False` 自行管理 root。
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
  `SocketHandler`，并清理 `KITTY_LOGGER_*` 环境变量，确保进程状态与
  `setup_logging` 对称。如果你额外挂了别的 handler，仍由你自己负责清理。

## 安全性

服务端用 `pickle.loads` 反序列化收到的字节流——这等价于在本机上对攻击者
可控的输入做"任意代码执行"。为了让这个能力**永远不会跨出本机**，
`setup_logging` 在打开 socket 之前就会**直接拒绝任何非 loopback 绑定**
（`ValueError`）。监听端口仅本机可达；请把它视为受信任的内部接口，**不要**
在不能信任本机其他进程的多租户机器上运行。
