Metadata-Version: 2.4
Name: kitty_logger
Version: 0.2.0.dev5
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) -> (host, port)`
  通过 `subprocess.Popen` 启动一个独立的日志服务子进程（不依赖
  `multiprocessing`，因此**不会回头执行用户主脚本的任何顶层代码**）。
  幂等。已通过 `atexit` 注册清理。`port=0` 让操作系统挑选空闲端口；返回
  真实绑定到的 `(host, port)`。
  **`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
  同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
  `SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
  在用户用 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op，
  仅返回从父进程继承的 `(host, port)`，因此可以直接放在模块顶层而不必用
  `if __name__ == "__main__":` 包裹。
- `getLogger(name=None) -> logging.Logger`
  返回挂在 `kitty` 命名空间下的 logger：`getLogger("foo")` 实际拿到的是
  `logging.getLogger("kitty.foo")`，`getLogger()` 拿到的是 `kitty` 本身。
  整个进程内只有 `kitty` 持有 `SocketHandler`，子 logger 通过标准库的祖先链
  冒泡上来，无需重复挂载。
- `shutdown_logging()` — 显式停止日志服务子进程。

## 为什么只支持 spawn

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

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

## 注意事项

- **所有 logger 名都带 `kitty.` 前缀**，这是有意保留的标记：日志记录里
  `%(name)s` 字段一眼可以和第三方库（urllib3、httpx 等）的输出区分。
  `getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
- `kitty` 子树**不向 root 冒泡**：第三方库挂在 root 上的 handler 不会收到
  kitty_logger 的日志，反之 root 上输出的日志也不会被 kitty_logger 转发。
  两条输出渠道互不干扰。
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
  `SocketHandler`，并清理 `KITTY_LOGGER_*` 环境变量，确保进程状态与
  `setup_logging` 对称。如果你额外挂了别的 handler，仍由你自己负责清理。

## 安全性

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