kitty-logger 0.2.0.dev1__tar.gz → 0.2.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kitty_logger-0.2.0.dev1/src/kitty_logger.egg-info → kitty_logger-0.2.0.dev3}/PKG-INFO +22 -19
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/README.md +21 -18
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/pyproject.toml +1 -1
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger/__init__.py +6 -1
- kitty_logger-0.2.0.dev3/src/kitty_logger/_client.py +112 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger/_server.py +31 -84
- kitty_logger-0.2.0.dev3/src/kitty_logger/_server_main.py +129 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger/_setup.py +178 -98
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3/src/kitty_logger.egg-info}/PKG-INFO +22 -19
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger.egg-info/SOURCES.txt +1 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/tests/test_kitty_logger.py +82 -36
- kitty_logger-0.2.0.dev1/src/kitty_logger/_client.py +0 -108
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/LICENSE +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/setup.cfg +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger/_env.py +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger/_formatters.py +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger.egg-info/dependency_links.txt +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger.egg-info/requires.txt +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev3}/src/kitty_logger.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kitty_logger
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev3
|
|
4
4
|
Summary: Cross-process logging via a dedicated log server process and SocketHandler.
|
|
5
5
|
Author: Kitty
|
|
6
6
|
License: MIT
|
|
@@ -54,18 +54,22 @@ if __name__ == "__main__":
|
|
|
54
54
|
|
|
55
55
|
## API
|
|
56
56
|
|
|
57
|
-
- `setup_logging(log_file=None, level=logging.INFO, host="127.0.0.1", port=0, stream=True, console_fmt=..., file_fmt=..., datefmt=None
|
|
58
|
-
|
|
59
|
-
`
|
|
57
|
+
- `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)`
|
|
58
|
+
通过 `subprocess.Popen` 启动一个独立的日志服务子进程(不依赖
|
|
59
|
+
`multiprocessing`,因此**不会回头执行用户主脚本的任何顶层代码**)。
|
|
60
|
+
幂等。已通过 `atexit` 注册清理。`port=0` 让操作系统挑选空闲端口;返回
|
|
61
|
+
真实绑定到的 `(host, port)`。
|
|
60
62
|
**`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
|
|
64
|
+
`SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
|
|
65
|
+
在用户用 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op,
|
|
66
|
+
仅返回从父进程继承的 `(host, port)`,因此可以直接放在模块顶层而不必用
|
|
67
|
+
`if __name__ == "__main__":` 包裹。
|
|
68
|
+
- `getLogger(name=None) -> logging.Logger`
|
|
69
|
+
返回挂在 `kitty` 命名空间下的 logger:`getLogger("foo")` 实际拿到的是
|
|
70
|
+
`logging.getLogger("kitty.foo")`,`getLogger()` 拿到的是 `kitty` 本身。
|
|
71
|
+
整个进程内只有 `kitty` 持有 `SocketHandler`,子 logger 通过标准库的祖先链
|
|
72
|
+
冒泡上来,无需重复挂载。
|
|
69
73
|
- `shutdown_logging()` — 显式停止日志服务子进程。
|
|
70
74
|
|
|
71
75
|
## 为什么只支持 spawn
|
|
@@ -80,13 +84,12 @@ if __name__ == "__main__":
|
|
|
80
84
|
|
|
81
85
|
## 注意事项
|
|
82
86
|
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
双倍输出。要么让 kitty_logger 做唯一入口,要么保持默认 `False` 自行管理 root。
|
|
87
|
+
- **所有 logger 名都带 `kitty.` 前缀**,这是有意保留的标记:日志记录里
|
|
88
|
+
`%(name)s` 字段一眼可以和第三方库(urllib3、httpx 等)的输出区分。
|
|
89
|
+
`getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
|
|
90
|
+
- `kitty` 子树**不向 root 冒泡**:第三方库挂在 root 上的 handler 不会收到
|
|
91
|
+
kitty_logger 的日志,反之 root 上输出的日志也不会被 kitty_logger 转发。
|
|
92
|
+
两条输出渠道互不干扰。
|
|
90
93
|
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
|
|
91
94
|
`SocketHandler`,并清理 `KITTY_LOGGER_*` 环境变量,确保进程状态与
|
|
92
95
|
`setup_logging` 对称。如果你额外挂了别的 handler,仍由你自己负责清理。
|
|
@@ -41,18 +41,22 @@ if __name__ == "__main__":
|
|
|
41
41
|
|
|
42
42
|
## API
|
|
43
43
|
|
|
44
|
-
- `setup_logging(log_file=None, level=logging.INFO, host="127.0.0.1", port=0, stream=True, console_fmt=..., file_fmt=..., datefmt=None
|
|
45
|
-
|
|
46
|
-
`
|
|
44
|
+
- `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)`
|
|
45
|
+
通过 `subprocess.Popen` 启动一个独立的日志服务子进程(不依赖
|
|
46
|
+
`multiprocessing`,因此**不会回头执行用户主脚本的任何顶层代码**)。
|
|
47
|
+
幂等。已通过 `atexit` 注册清理。`port=0` 让操作系统挑选空闲端口;返回
|
|
48
|
+
真实绑定到的 `(host, port)`。
|
|
47
49
|
**`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
|
|
51
|
+
`SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
|
|
52
|
+
在用户用 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op,
|
|
53
|
+
仅返回从父进程继承的 `(host, port)`,因此可以直接放在模块顶层而不必用
|
|
54
|
+
`if __name__ == "__main__":` 包裹。
|
|
55
|
+
- `getLogger(name=None) -> logging.Logger`
|
|
56
|
+
返回挂在 `kitty` 命名空间下的 logger:`getLogger("foo")` 实际拿到的是
|
|
57
|
+
`logging.getLogger("kitty.foo")`,`getLogger()` 拿到的是 `kitty` 本身。
|
|
58
|
+
整个进程内只有 `kitty` 持有 `SocketHandler`,子 logger 通过标准库的祖先链
|
|
59
|
+
冒泡上来,无需重复挂载。
|
|
56
60
|
- `shutdown_logging()` — 显式停止日志服务子进程。
|
|
57
61
|
|
|
58
62
|
## 为什么只支持 spawn
|
|
@@ -67,13 +71,12 @@ if __name__ == "__main__":
|
|
|
67
71
|
|
|
68
72
|
## 注意事项
|
|
69
73
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
双倍输出。要么让 kitty_logger 做唯一入口,要么保持默认 `False` 自行管理 root。
|
|
74
|
+
- **所有 logger 名都带 `kitty.` 前缀**,这是有意保留的标记:日志记录里
|
|
75
|
+
`%(name)s` 字段一眼可以和第三方库(urllib3、httpx 等)的输出区分。
|
|
76
|
+
`getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
|
|
77
|
+
- `kitty` 子树**不向 root 冒泡**:第三方库挂在 root 上的 handler 不会收到
|
|
78
|
+
kitty_logger 的日志,反之 root 上输出的日志也不会被 kitty_logger 转发。
|
|
79
|
+
两条输出渠道互不干扰。
|
|
77
80
|
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
|
|
78
81
|
`SocketHandler`,并清理 `KITTY_LOGGER_*` 环境变量,确保进程状态与
|
|
79
82
|
`setup_logging` 对称。如果你额外挂了别的 handler,仍由你自己负责清理。
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kitty_logger"
|
|
7
|
-
version = "0.2.0.
|
|
7
|
+
version = "0.2.0.dev3"
|
|
8
8
|
description = "Cross-process logging via a dedicated log server process and SocketHandler."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
日志收敛到主进程指定的文件 / ``stderr`` 中,不会因多进程并发写文件而
|
|
7
7
|
错行或丢失。
|
|
8
8
|
|
|
9
|
+
命名空间:所有由本库返回的 logger 都挂在 ``kitty`` 这棵子树下。
|
|
10
|
+
``getLogger("foo")`` 实际拿到的是 ``logging.getLogger("kitty.foo")``,
|
|
11
|
+
日志记录里 ``%(name)s`` 会带 ``kitty.`` 前缀,便于和第三方库的日志区分。
|
|
12
|
+
``kitty`` 子树独立运行,不会向 root 冒泡,也不会被 root 上的 handler 干扰。
|
|
13
|
+
|
|
9
14
|
典型用法::
|
|
10
15
|
|
|
11
16
|
# main.py
|
|
@@ -28,4 +33,4 @@ from ._client import getLogger
|
|
|
28
33
|
from ._setup import setup_logging, shutdown_logging
|
|
29
34
|
|
|
30
35
|
__all__ = ["setup_logging", "shutdown_logging", "getLogger"]
|
|
31
|
-
__version__ = "0.2.0.
|
|
36
|
+
__version__ = "0.2.0.dev3"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""子进程侧 API::func:`getLogger`。
|
|
2
|
+
|
|
3
|
+
子进程通过环境变量
|
|
4
|
+
``KITTY_LOGGER_HOST`` / ``KITTY_LOGGER_PORT`` / ``KITTY_LOGGER_LEVEL``
|
|
5
|
+
找到日志服务地址;这些环境变量是父进程在 :func:`setup_logging` 时写入的,
|
|
6
|
+
``spawn`` 出来的后代进程会自动继承。
|
|
7
|
+
|
|
8
|
+
命名空间约定
|
|
9
|
+
------------
|
|
10
|
+
|
|
11
|
+
所有由本库返回的 logger 都挂在 ``kitty`` 这棵子树下:
|
|
12
|
+
|
|
13
|
+
- ``getLogger()`` → ``logging.getLogger("kitty")``
|
|
14
|
+
- ``getLogger("foo.bar")`` → ``logging.getLogger("kitty.foo.bar")``
|
|
15
|
+
|
|
16
|
+
整个进程内只有 ``kitty`` 这一个 logger 持有指向日志服务的
|
|
17
|
+
:class:`SocketHandler`,子 logger 通过标准库的祖先链冒泡上来。
|
|
18
|
+
``kitty`` 自身的 ``propagate`` 被关闭,与 root 上的第三方库日志互不干扰。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import logging.handlers
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
|
|
26
|
+
from ._env import ENV_HOST, ENV_LEVEL, ENV_PORT
|
|
27
|
+
|
|
28
|
+
# 本库使用的 logger 命名空间根。所有用户拿到的 logger 都在这棵子树下。
|
|
29
|
+
KITTY_NAMESPACE = "kitty"
|
|
30
|
+
|
|
31
|
+
# 保护 ``kitty`` logger 上的 handler 初始化。
|
|
32
|
+
_lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_endpoint() -> tuple[str, int]:
|
|
36
|
+
"""从环境变量读出 ``(host, port)``;未设置则抛出友好错误。"""
|
|
37
|
+
host = os.environ.get(ENV_HOST)
|
|
38
|
+
port = os.environ.get(ENV_PORT)
|
|
39
|
+
if not host or not port:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"kitty_logger 尚未在当前进程树中初始化。"
|
|
42
|
+
+ "请先在主进程调用 kitty_logger.setup_logging(),"
|
|
43
|
+
+ "再创建(spawn)子进程或调用 getLogger()。"
|
|
44
|
+
)
|
|
45
|
+
return host, int(port)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_level() -> int:
|
|
49
|
+
"""从环境变量解析默认 level。优先按整数解析,失败则按级别名解析。"""
|
|
50
|
+
raw = os.environ.get(ENV_LEVEL)
|
|
51
|
+
if raw is None:
|
|
52
|
+
return logging.INFO
|
|
53
|
+
try:
|
|
54
|
+
return int(raw)
|
|
55
|
+
except ValueError:
|
|
56
|
+
pass
|
|
57
|
+
return logging.getLevelNamesMapping().get(raw.upper(), logging.INFO)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_kitty_handler() -> logging.Logger:
|
|
61
|
+
"""保证本进程内 ``kitty`` logger 已挂上指向日志服务的 SocketHandler。
|
|
62
|
+
|
|
63
|
+
幂等:已挂过则直接返回;``spawn`` 出来的全新解释器里第一次调用时
|
|
64
|
+
才会真正去读 ENV、建 SocketHandler。
|
|
65
|
+
|
|
66
|
+
.. note::
|
|
67
|
+
:class:`logging.handlers.SocketHandler` 直接 pickle ``LogRecord``
|
|
68
|
+
发送,由接收端格式化;在这里给 handler 设置 ``formatter`` 没有
|
|
69
|
+
任何效果,会被服务端忽略。
|
|
70
|
+
"""
|
|
71
|
+
SocketHandler = logging.handlers.SocketHandler
|
|
72
|
+
kitty = logging.getLogger(KITTY_NAMESPACE)
|
|
73
|
+
|
|
74
|
+
with _lock:
|
|
75
|
+
for h in kitty.handlers:
|
|
76
|
+
if isinstance(h, SocketHandler):
|
|
77
|
+
return kitty
|
|
78
|
+
|
|
79
|
+
host, port = _read_endpoint()
|
|
80
|
+
level = _read_level()
|
|
81
|
+
|
|
82
|
+
kitty.addHandler(SocketHandler(host, port))
|
|
83
|
+
|
|
84
|
+
# 仅在仍是默认 ``NOTSET`` 时才设置 level,避免悄悄覆盖
|
|
85
|
+
# 用户已经显式设置过的级别。
|
|
86
|
+
if kitty.level == logging.NOTSET:
|
|
87
|
+
kitty.setLevel(level)
|
|
88
|
+
|
|
89
|
+
# ``kitty`` 子树自成一系,不向 root 冒泡:这样和第三方库挂在 root
|
|
90
|
+
# 上的 handler 互不串扰;用户控制 root 的输出渠道时也不会无意中
|
|
91
|
+
# 把 kitty_logger 的日志带过去(反之亦然)。
|
|
92
|
+
kitty.propagate = False
|
|
93
|
+
|
|
94
|
+
return kitty
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def getLogger(name: str | None = None) -> logging.Logger:
|
|
98
|
+
"""返回挂在 ``kitty`` 命名空间下的 logger,自动连到日志服务。
|
|
99
|
+
|
|
100
|
+
必须在父进程已经调用过 :func:`kitty_logger.setup_logging` 之后才能
|
|
101
|
+
使用。本库只支持 ``spawn`` 启动方式;spawn 出来的子进程模块级状态
|
|
102
|
+
天然为空,第一次调用时会按需完成初始化。
|
|
103
|
+
|
|
104
|
+
:param name: 业务名;``None`` 表示直接返回 ``kitty`` 本身。``"foo"``
|
|
105
|
+
会被转换为 ``logging.getLogger("kitty.foo")``——日志记录里
|
|
106
|
+
``%(name)s`` 字段会带 ``kitty.`` 前缀,这是有意保留的标记,方便
|
|
107
|
+
和第三方库日志区分。
|
|
108
|
+
:raises RuntimeError: 当前进程树尚未调用 ``setup_logging``。
|
|
109
|
+
"""
|
|
110
|
+
_ensure_kitty_handler()
|
|
111
|
+
full = KITTY_NAMESPACE if not name else f"{KITTY_NAMESPACE}.{name}"
|
|
112
|
+
return logging.getLogger(full)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""日志服务子进程的核心组件(与启动方式无关)。
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
服务进程通过 ``python -m kitty_logger._server_main`` 由 :func:`kitty_logger.setup_logging`
|
|
4
|
+
间接拉起,本模块只提供与"具体如何起进程"无关的纯逻辑:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
``LogRecord`` 字节流,再用本进程里配置的 handler(FileHandler /
|
|
10
|
-
StreamHandler)落地。
|
|
6
|
+
- :func:`_bind_loopback`:在 loopback 地址上绑定一个 ``AF_INET`` 监听 socket。
|
|
7
|
+
- :class:`_LogRecordSocketReceiver`:基于 :class:`socketserver.ThreadingTCPServer`
|
|
8
|
+
的接收器,复用外部已 ``bind`` 好的 socket。
|
|
9
|
+
- :func:`_build_handlers`:根据用户参数组装落地用的 handler 列表。
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
12
|
import logging
|
|
@@ -16,19 +15,39 @@ import socket
|
|
|
16
15
|
import socketserver
|
|
17
16
|
import struct
|
|
18
17
|
import sys
|
|
19
|
-
import threading
|
|
20
18
|
from logging.handlers import TimedRotatingFileHandler
|
|
21
|
-
from multiprocessing.synchronize import Event as MpEvent
|
|
22
19
|
from typing import cast, final, override
|
|
23
20
|
|
|
24
21
|
from ._formatters import ColorFormatter, FileFormatter
|
|
25
22
|
|
|
23
|
+
# 这三个符号由同包内的 ``_server_main`` 模块导入复用;以下划线开头是因为它们
|
|
24
|
+
# 不属于库的公共 API(仅 kitty_logger 内部使用)。pyright 对下划线名默认按
|
|
25
|
+
# "模块内私有"处理,看不到跨模块引用,因此显式通过 ``__all__`` 声明为本模块
|
|
26
|
+
# 的对外导出,避免 reportUnusedFunction / reportUnusedClass 误报。
|
|
27
|
+
__all__ = ["_bind_loopback", "_LogRecordSocketReceiver", "_build_handlers"]
|
|
28
|
+
|
|
26
29
|
# 单条 LogRecord 序列化后字节数上限。任何正常 LogRecord 都远小于此值;
|
|
27
30
|
# 超过即视为协议错位或异常流量,直接断开连接,避免一次性分配巨大缓冲区
|
|
28
31
|
# 触发 OOM。
|
|
29
32
|
_MAX_RECORD_BYTES = 16 * 1024 * 1024
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
def _bind_loopback(
|
|
36
|
+
host: str, port: int, backlog: int = 128
|
|
37
|
+
) -> tuple[socket.socket, str, int]:
|
|
38
|
+
"""创建并绑定一个 ``AF_INET`` TCP 监听 socket,返回 ``(sock, bound_host, bound_port)``。
|
|
39
|
+
|
|
40
|
+
把"AF_INET 的 ``getsockname`` 一定返回 ``tuple[str, int]``"这个静态
|
|
41
|
+
事实集中收敛在这里,调用方不必再写 ``cast``。
|
|
42
|
+
"""
|
|
43
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
44
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
45
|
+
sock.bind((host, port))
|
|
46
|
+
sock.listen(backlog)
|
|
47
|
+
bound_host, bound_port = cast(tuple[str, int], sock.getsockname())
|
|
48
|
+
return sock, bound_host, bound_port
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
def _recv_exact(conn: socket.socket, n: int) -> bytes | None:
|
|
33
52
|
"""从 ``conn`` 上读满 ``n`` 字节;对端已关闭则返回 ``None``。
|
|
34
53
|
|
|
@@ -84,7 +103,7 @@ class _LogRecordStreamHandler(socketserver.StreamRequestHandler):
|
|
|
84
103
|
class _LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
|
|
85
104
|
"""基于 :class:`socketserver.ThreadingTCPServer` 的接收器。
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
复用外部已 ``bind`` 好的监听 socket,本类不再自己 ``bind``/``listen``。
|
|
88
107
|
"""
|
|
89
108
|
|
|
90
109
|
allow_reuse_address: bool = True
|
|
@@ -95,7 +114,7 @@ class _LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
|
|
|
95
114
|
addr = cast(tuple[str, int], listening_sock.getsockname()[:2])
|
|
96
115
|
# 走父类正常流程以建立 BaseServer 内部状态(shutdown 标志等),
|
|
97
116
|
# 但通过 ``bind_and_activate=False`` 跳过它自己的 bind/listen——
|
|
98
|
-
# 这个 socket
|
|
117
|
+
# 这个 socket 已经由调用方绑好。
|
|
99
118
|
super().__init__(addr, _LogRecordStreamHandler, bind_and_activate=False)
|
|
100
119
|
# 父类构造时仍会 ``self.socket = socket.socket(...)`` 占坑,
|
|
101
120
|
# 这里替换成真正复用的 socket,并把临时占位关闭掉。
|
|
@@ -132,75 +151,3 @@ def _build_handlers(
|
|
|
132
151
|
sh.setFormatter(ColorFormatter(fmt=console_fmt, datefmt=datefmt))
|
|
133
152
|
handlers.append(sh)
|
|
134
153
|
return handlers
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def run_server(
|
|
138
|
-
listening_sock: socket.socket,
|
|
139
|
-
shutdown_event: MpEvent,
|
|
140
|
-
parent_alive_recv: socket.socket,
|
|
141
|
-
level: int,
|
|
142
|
-
log_file: str | None,
|
|
143
|
-
stream: bool,
|
|
144
|
-
console_fmt: str,
|
|
145
|
-
file_fmt: str,
|
|
146
|
-
datefmt: str | None,
|
|
147
|
-
) -> None:
|
|
148
|
-
"""日志服务子进程入口函数。
|
|
149
|
-
|
|
150
|
-
通过 :mod:`multiprocessing` 在 ``spawn`` 出来的全新解释器里运行;
|
|
151
|
-
所有依赖参数显式从 ``kwargs`` 进入,不依赖父进程的全局状态。
|
|
152
|
-
|
|
153
|
-
:param listening_sock: 已经由父进程 ``bind`` + ``listen`` 好的 socket。
|
|
154
|
-
:param shutdown_event: 父进程通知子进程正常退出用的事件。
|
|
155
|
-
:param parent_alive_recv: ``socketpair`` 的接收端,用来侦测父进程死亡。
|
|
156
|
-
:param level: 服务端 root logger 级别。
|
|
157
|
-
:param log_file: 日志文件路径;``None`` 表示不落盘。
|
|
158
|
-
:param stream: 是否同时输出到 ``stderr``。
|
|
159
|
-
:param console_fmt: 控制台 handler 的 format 字符串。
|
|
160
|
-
:param file_fmt: 文件 handler 的 format 字符串。
|
|
161
|
-
:param datefmt: 时间戳的 ``strftime`` 格式;``None`` 走默认值。
|
|
162
|
-
"""
|
|
163
|
-
# 子进程是 ``spawn`` 出来的全新解释器,root logger 默认无 handler;
|
|
164
|
-
# 但仍保险地清掉一遍以便重复初始化场景下的幂等性。
|
|
165
|
-
root = logging.getLogger()
|
|
166
|
-
for h in list(root.handlers):
|
|
167
|
-
root.removeHandler(h)
|
|
168
|
-
for h in _build_handlers(log_file, stream, console_fmt, file_fmt, datefmt):
|
|
169
|
-
root.addHandler(h)
|
|
170
|
-
root.setLevel(level)
|
|
171
|
-
|
|
172
|
-
server = _LogRecordSocketReceiver(listening_sock)
|
|
173
|
-
|
|
174
|
-
def _wait_shutdown() -> None:
|
|
175
|
-
_ = shutdown_event.wait()
|
|
176
|
-
server.shutdown()
|
|
177
|
-
|
|
178
|
-
def _watch_parent_alive() -> None:
|
|
179
|
-
# 父进程持有 socketpair 的发送端;任意原因(正常退出 / SIGKILL /
|
|
180
|
-
# 段错误)使父进程消失,内核都会关闭它的 fd,本端 ``recv`` 会立刻
|
|
181
|
-
# 返回空字节(EOF)。借此触发自我退出,避免变成孤儿进程。
|
|
182
|
-
try:
|
|
183
|
-
while True:
|
|
184
|
-
data = parent_alive_recv.recv(64)
|
|
185
|
-
if not data:
|
|
186
|
-
break
|
|
187
|
-
except OSError:
|
|
188
|
-
pass
|
|
189
|
-
finally:
|
|
190
|
-
try:
|
|
191
|
-
parent_alive_recv.close()
|
|
192
|
-
except OSError:
|
|
193
|
-
pass
|
|
194
|
-
shutdown_event.set()
|
|
195
|
-
|
|
196
|
-
threading.Thread(target=_wait_shutdown, daemon=True).start()
|
|
197
|
-
threading.Thread(target=_watch_parent_alive, daemon=True).start()
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
server.serve_forever()
|
|
201
|
-
finally:
|
|
202
|
-
try:
|
|
203
|
-
server.server_close()
|
|
204
|
-
except OSError:
|
|
205
|
-
pass
|
|
206
|
-
logging.shutdown()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""kitty_logger 服务子进程入口。
|
|
2
|
+
|
|
3
|
+
通过 ``python -m kitty_logger._server_main`` 启动,由
|
|
4
|
+
:func:`kitty_logger.setup_logging` 内部用 :class:`subprocess.Popen` 拉起。
|
|
5
|
+
本模块不是给最终用户的接口。
|
|
6
|
+
|
|
7
|
+
握手 / 生命周期协议
|
|
8
|
+
--------------------
|
|
9
|
+
|
|
10
|
+
整套机制只用三条标准管道,不需要 ``pass_fds``,所以天然跨平台:
|
|
11
|
+
|
|
12
|
+
* ``stdin`` — 父→子,**parent-alive**:父进程保持 write 端打开但不写。
|
|
13
|
+
父进程任何原因退出(``exit`` / ``kill -9`` / 段错误),内核会关掉它持有
|
|
14
|
+
的写端 fd,子进程在本端 ``read()`` 立刻返回 ``b""`` → 触发优雅退出。
|
|
15
|
+
* ``stdout`` — 子→父,**启动握手**:子进程 ``bind+listen`` 成功后写出
|
|
16
|
+
``"PORT <port>\\n"`` 然后 ``close``,握手即结束;之后不再使用 stdout
|
|
17
|
+
以避免后续误用 ``print`` 污染父进程的读端。
|
|
18
|
+
* ``stderr`` — 子→父,**日志落地**:直接继承父进程的 stderr/tty。
|
|
19
|
+
``--stream`` 控制是否给 root logger 挂上 :class:`logging.StreamHandler`。
|
|
20
|
+
|
|
21
|
+
命令行参数对应 :func:`kitty_logger.setup_logging` 的同名参数。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
import threading
|
|
28
|
+
|
|
29
|
+
from ._server import _LogRecordSocketReceiver, _bind_loopback, _build_handlers
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_args() -> argparse.Namespace:
|
|
33
|
+
ap = argparse.ArgumentParser(prog="kitty_logger._server_main", add_help=False)
|
|
34
|
+
ap.add_argument("--host", required=True)
|
|
35
|
+
ap.add_argument("--port", type=int, required=True)
|
|
36
|
+
ap.add_argument("--level", type=int, required=True)
|
|
37
|
+
ap.add_argument("--log-file", default=None)
|
|
38
|
+
ap.add_argument("--stream", action="store_true")
|
|
39
|
+
ap.add_argument("--console-fmt", required=True)
|
|
40
|
+
ap.add_argument("--file-fmt", required=True)
|
|
41
|
+
ap.add_argument("--datefmt", default=None)
|
|
42
|
+
return ap.parse_args()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _emit_handshake(port: int) -> None:
|
|
46
|
+
"""把 ``"PORT <port>\\n"`` 写到 stdout 并主动 close。
|
|
47
|
+
|
|
48
|
+
主动 close 等于显式宣告"握手结束"——之后任何对 ``sys.stdout`` 的
|
|
49
|
+
误用都不会再污染父进程的读端,父进程的 ``readline()`` 也只会看到
|
|
50
|
+
我们刚写的这一行。
|
|
51
|
+
"""
|
|
52
|
+
sys.stdout.write(f"PORT {port}\n")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
try:
|
|
55
|
+
sys.stdout.close()
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _emit_handshake_error(msg: str) -> None:
|
|
61
|
+
"""启动失败时通过 stdout 报错,让父端 ``readline()`` 看到非 ``PORT`` 行。"""
|
|
62
|
+
try:
|
|
63
|
+
sys.stdout.write(f"ERROR {msg}\n")
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
sys.stdout.close()
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> None:
|
|
71
|
+
args = _parse_args()
|
|
72
|
+
|
|
73
|
+
# 先 bind+listen,这一步任何失败都通过 stdout 上报给父进程,让 setup_logging
|
|
74
|
+
# 能在主进程线程中以 ``RuntimeError`` 形式抛出,而不是父子双方各自卡住。
|
|
75
|
+
try:
|
|
76
|
+
sock, _bound_host, bound_port = _bind_loopback(args.host, args.port)
|
|
77
|
+
except OSError as e:
|
|
78
|
+
_emit_handshake_error(f"bind failed: {e}")
|
|
79
|
+
sys.exit(2)
|
|
80
|
+
|
|
81
|
+
_emit_handshake(bound_port)
|
|
82
|
+
|
|
83
|
+
# spawn 出来的全新解释器,root logger 默认无 handler;保险起见仍清一遍
|
|
84
|
+
# 以便重复初始化场景下保持幂等。
|
|
85
|
+
root = logging.getLogger()
|
|
86
|
+
for h in list(root.handlers):
|
|
87
|
+
root.removeHandler(h)
|
|
88
|
+
for h in _build_handlers(
|
|
89
|
+
args.log_file, args.stream, args.console_fmt, args.file_fmt, args.datefmt
|
|
90
|
+
):
|
|
91
|
+
root.addHandler(h)
|
|
92
|
+
root.setLevel(args.level)
|
|
93
|
+
|
|
94
|
+
server = _LogRecordSocketReceiver(sock)
|
|
95
|
+
|
|
96
|
+
def _watch_parent_alive() -> None:
|
|
97
|
+
"""父进程持有 stdin 的写端;任何原因(正常退出 / SIGKILL / 段错误)
|
|
98
|
+
让父进程消失,内核都会关掉它的 fd,本端 ``read()`` 会立刻返回
|
|
99
|
+
``b""``(EOF)。借此触发自我退出,避免变成孤儿进程。
|
|
100
|
+
|
|
101
|
+
正常生命周期下父进程也通过 ``proc.stdin.close()`` 主动触发同一
|
|
102
|
+
条 EOF 路径,把"父退出 / 父主动 shutdown" 收敛为同一种处理。
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
while True:
|
|
106
|
+
data = sys.stdin.buffer.read(64)
|
|
107
|
+
if not data:
|
|
108
|
+
break
|
|
109
|
+
except (OSError, ValueError):
|
|
110
|
+
pass
|
|
111
|
+
finally:
|
|
112
|
+
server.shutdown()
|
|
113
|
+
|
|
114
|
+
threading.Thread(
|
|
115
|
+
target=_watch_parent_alive, name="kitty-logger-parent-watch", daemon=True
|
|
116
|
+
).start()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
server.serve_forever()
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
server.server_close()
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
logging.shutdown()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|