kitty-logger 0.2.0.dev1__tar.gz → 0.2.0.dev2__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.dev2}/PKG-INFO +18 -17
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/README.md +17 -16
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/pyproject.toml +1 -1
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger/__init__.py +6 -1
- kitty_logger-0.2.0.dev2/src/kitty_logger/_client.py +112 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger/_server.py +22 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger/_setup.py +48 -26
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2/src/kitty_logger.egg-info}/PKG-INFO +18 -17
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/tests/test_kitty_logger.py +126 -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.dev2}/LICENSE +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/setup.cfg +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger/_env.py +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger/_formatters.py +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger.egg-info/SOURCES.txt +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger.egg-info/dependency_links.txt +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger.egg-info/requires.txt +0 -0
- {kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/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.dev2
|
|
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,20 @@ 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
|
|
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
58
|
启动日志服务子进程(始终使用 `spawn`)。幂等。已通过 `atexit` 注册清理。
|
|
59
59
|
`port=0` 让操作系统挑选空闲端口;返回真实绑定到的 `(host, port)`。
|
|
60
60
|
**`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
|
|
62
|
+
`SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
|
|
63
|
+
在 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op,仅返回
|
|
64
|
+
从父进程继承的 `(host, port)`,因此可以直接放在模块顶层而不必用
|
|
65
|
+
`if __name__ == "__main__":` 包裹。
|
|
66
|
+
- `getLogger(name=None) -> logging.Logger`
|
|
67
|
+
返回挂在 `kitty` 命名空间下的 logger:`getLogger("foo")` 实际拿到的是
|
|
68
|
+
`logging.getLogger("kitty.foo")`,`getLogger()` 拿到的是 `kitty` 本身。
|
|
69
|
+
整个进程内只有 `kitty` 持有 `SocketHandler`,子 logger 通过标准库的祖先链
|
|
70
|
+
冒泡上来,无需重复挂载。
|
|
69
71
|
- `shutdown_logging()` — 显式停止日志服务子进程。
|
|
70
72
|
|
|
71
73
|
## 为什么只支持 spawn
|
|
@@ -80,13 +82,12 @@ if __name__ == "__main__":
|
|
|
80
82
|
|
|
81
83
|
## 注意事项
|
|
82
84
|
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
双倍输出。要么让 kitty_logger 做唯一入口,要么保持默认 `False` 自行管理 root。
|
|
85
|
+
- **所有 logger 名都带 `kitty.` 前缀**,这是有意保留的标记:日志记录里
|
|
86
|
+
`%(name)s` 字段一眼可以和第三方库(urllib3、httpx 等)的输出区分。
|
|
87
|
+
`getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
|
|
88
|
+
- `kitty` 子树**不向 root 冒泡**:第三方库挂在 root 上的 handler 不会收到
|
|
89
|
+
kitty_logger 的日志,反之 root 上输出的日志也不会被 kitty_logger 转发。
|
|
90
|
+
两条输出渠道互不干扰。
|
|
90
91
|
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
|
|
91
92
|
`SocketHandler`,并清理 `KITTY_LOGGER_*` 环境变量,确保进程状态与
|
|
92
93
|
`setup_logging` 对称。如果你额外挂了别的 handler,仍由你自己负责清理。
|
|
@@ -41,18 +41,20 @@ 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
|
|
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
45
|
启动日志服务子进程(始终使用 `spawn`)。幂等。已通过 `atexit` 注册清理。
|
|
46
46
|
`port=0` 让操作系统挑选空闲端口;返回真实绑定到的 `(host, port)`。
|
|
47
47
|
**`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
|
|
49
|
+
`SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
|
|
50
|
+
在 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op,仅返回
|
|
51
|
+
从父进程继承的 `(host, port)`,因此可以直接放在模块顶层而不必用
|
|
52
|
+
`if __name__ == "__main__":` 包裹。
|
|
53
|
+
- `getLogger(name=None) -> logging.Logger`
|
|
54
|
+
返回挂在 `kitty` 命名空间下的 logger:`getLogger("foo")` 实际拿到的是
|
|
55
|
+
`logging.getLogger("kitty.foo")`,`getLogger()` 拿到的是 `kitty` 本身。
|
|
56
|
+
整个进程内只有 `kitty` 持有 `SocketHandler`,子 logger 通过标准库的祖先链
|
|
57
|
+
冒泡上来,无需重复挂载。
|
|
56
58
|
- `shutdown_logging()` — 显式停止日志服务子进程。
|
|
57
59
|
|
|
58
60
|
## 为什么只支持 spawn
|
|
@@ -67,13 +69,12 @@ if __name__ == "__main__":
|
|
|
67
69
|
|
|
68
70
|
## 注意事项
|
|
69
71
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
双倍输出。要么让 kitty_logger 做唯一入口,要么保持默认 `False` 自行管理 root。
|
|
72
|
+
- **所有 logger 名都带 `kitty.` 前缀**,这是有意保留的标记:日志记录里
|
|
73
|
+
`%(name)s` 字段一眼可以和第三方库(urllib3、httpx 等)的输出区分。
|
|
74
|
+
`getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
|
|
75
|
+
- `kitty` 子树**不向 root 冒泡**:第三方库挂在 root 上的 handler 不会收到
|
|
76
|
+
kitty_logger 的日志,反之 root 上输出的日志也不会被 kitty_logger 转发。
|
|
77
|
+
两条输出渠道互不干扰。
|
|
77
78
|
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
|
|
78
79
|
`SocketHandler`,并清理 `KITTY_LOGGER_*` 环境变量,确保进程状态与
|
|
79
80
|
`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.dev2"
|
|
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.dev2"
|
|
@@ -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)
|
|
@@ -169,6 +169,28 @@ def run_server(
|
|
|
169
169
|
root.addHandler(h)
|
|
170
170
|
root.setLevel(level)
|
|
171
171
|
|
|
172
|
+
# 防御 ``spawn`` bootstrap:服务子进程在 ``_fixup_main_from_path`` 重新
|
|
173
|
+
# import 用户主脚本时,主脚本顶层 import 的第三方库可能在模块级调用
|
|
174
|
+
# ``kitty_logger.getLogger(...)``。由于父进程已经把 KITTY_LOGGER_HOST/
|
|
175
|
+
# PORT/LEVEL 写进 ENV,本服务子进程里的 ``_ensure_kitty_handler()`` 会
|
|
176
|
+
# 顺利地往 ``kitty`` logger 上挂一个指向本服务自己监听端口的
|
|
177
|
+
# :class:`SocketHandler`,并把 ``propagate`` 关掉。结果是:本服务接收
|
|
178
|
+
# 到记录后调 ``logging.getLogger(record.name).handle(record)``,记录
|
|
179
|
+
# 沿祖先链冒泡到 ``kitty`` 时被这个 handler 捞走,原样发回给本服务
|
|
180
|
+
# 自身(形成网络回环),同时因为 ``propagate=False`` 而不会再到达
|
|
181
|
+
# root 上配置好的落地 handler,外部表现为日志凭空消失。
|
|
182
|
+
# 因此在开始 serve 之前,强制把 ``kitty`` logger 还原成"无 handler、
|
|
183
|
+
# propagate=True、level=NOTSET",让记录稳稳地冒泡到 root。
|
|
184
|
+
kitty = logging.getLogger("kitty")
|
|
185
|
+
for h in list(kitty.handlers):
|
|
186
|
+
kitty.removeHandler(h)
|
|
187
|
+
try:
|
|
188
|
+
h.close()
|
|
189
|
+
except OSError:
|
|
190
|
+
pass
|
|
191
|
+
kitty.propagate = True
|
|
192
|
+
kitty.setLevel(logging.NOTSET)
|
|
193
|
+
|
|
172
194
|
server = _LogRecordSocketReceiver(listening_sock)
|
|
173
195
|
|
|
174
196
|
def _wait_shutdown() -> None:
|
|
@@ -128,7 +128,6 @@ def setup_logging(
|
|
|
128
128
|
console_fmt: str = DEFAULT_CONSOLE_FMT,
|
|
129
129
|
file_fmt: str = DEFAULT_FILE_FMT,
|
|
130
130
|
datefmt: str | None = None,
|
|
131
|
-
attach_main_logger: bool = False,
|
|
132
131
|
) -> tuple[str, int]:
|
|
133
132
|
"""在主进程里启动跨进程日志服务,返回服务真实绑定到的 ``(host, port)``。
|
|
134
133
|
|
|
@@ -142,9 +141,20 @@ def setup_logging(
|
|
|
142
141
|
无颜色的 :class:`FileFormatter`,两者均使用毫秒级时间戳。
|
|
143
142
|
|
|
144
143
|
幂等:重复调用直接返回已记录的 ``(host, port)``,不会重复启动子进程。
|
|
144
|
+
在 ``multiprocessing`` 启动的子进程里调用本函数时,会自动判断当前
|
|
145
|
+
不是原始主进程并直接返回从父进程继承的 ``(host, port)``,本身不再
|
|
146
|
+
启动新的日志服务——这意味着用户可以把 ``setup_logging()`` 直接放在
|
|
147
|
+
模块顶层而不必用 ``if __name__ == "__main__":`` 包裹。
|
|
148
|
+
|
|
149
|
+
本函数同时把主进程内 ``logging.getLogger("kitty")`` 这一个 logger
|
|
150
|
+
初始化好——挂上指向日志服务的 :class:`SocketHandler`、设置 level、
|
|
151
|
+
并把 ``propagate`` 关掉。所有通过 :func:`kitty_logger.getLogger`
|
|
152
|
+
取到的子 logger 都挂在 ``kitty`` 这棵子树下,借标准库的祖先链冒泡到
|
|
153
|
+
``kitty`` 的这个 handler 上,无需自己额外配置。
|
|
145
154
|
|
|
146
155
|
:param log_file: 日志文件路径;``None`` 表示不落盘。
|
|
147
|
-
:param level: 服务端 root logger
|
|
156
|
+
:param level: 服务端 root logger 的级别,同时也是主进程 ``kitty``
|
|
157
|
+
logger 的初始 level(仅在它仍是 ``NOTSET`` 时设置)。
|
|
148
158
|
:param host: 监听地址,默认 ``127.0.0.1``。**必须是 loopback**:
|
|
149
159
|
绑定非 loopback 地址会直接 :class:`ValueError`。
|
|
150
160
|
:param port: 监听端口;``0`` 让 OS 自动选择空闲端口。
|
|
@@ -152,10 +162,6 @@ def setup_logging(
|
|
|
152
162
|
:param console_fmt: 控制台 handler 的 format 字符串。
|
|
153
163
|
:param file_fmt: 文件 handler 的 format 字符串。
|
|
154
164
|
:param datefmt: 时间戳的 ``strftime`` 格式;``None`` 走默认值。
|
|
155
|
-
:param attach_main_logger: 是否给主进程的 root logger 也挂一个
|
|
156
|
-
:class:`SocketHandler`,让主进程自身的日志走同一个服务。
|
|
157
|
-
默认 ``False``——主进程 root 保持不动;想让某个 logger 走
|
|
158
|
-
kitty_logger,请显式调用 :func:`kitty_logger.getLogger`。
|
|
159
165
|
:return: 服务真实绑定到的 ``(host, port)``。
|
|
160
166
|
|
|
161
167
|
.. note::
|
|
@@ -171,6 +177,22 @@ def setup_logging(
|
|
|
171
177
|
请使用 ``multiprocessing.get_context("spawn")``,或在顶层用
|
|
172
178
|
``if __name__ == "__main__":`` 守卫配合默认 ``spawn`` 行为。
|
|
173
179
|
"""
|
|
180
|
+
# ``multiprocessing`` 启动出来的子进程里再次执行 setup_logging(典型
|
|
181
|
+
# 场景:spawn bootstrap 通过 ``_fixup_main_from_path`` 重新 import 主
|
|
182
|
+
# 脚本,从而触发主脚本顶层那行 ``kitty_logger.setup_logging(...)``)
|
|
183
|
+
# 一律走 no-op:日志服务只该在原始主进程里跑一份;子进程通过继承的
|
|
184
|
+
# ENV 找服务地址,``getLogger`` 第一次被调用时再按需挂 SocketHandler。
|
|
185
|
+
if mp.parent_process() is not None:
|
|
186
|
+
host_env = os.environ.get(ENV_HOST)
|
|
187
|
+
port_env = os.environ.get(ENV_PORT)
|
|
188
|
+
if not host_env or not port_env:
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
"kitty_logger 在子进程里被调用 setup_logging,但没有从父进程"
|
|
191
|
+
"继承到 KITTY_LOGGER_HOST / KITTY_LOGGER_PORT 环境变量。"
|
|
192
|
+
"请确保父进程已经先调用 setup_logging() 后再 spawn 本进程。"
|
|
193
|
+
)
|
|
194
|
+
return host_env, int(port_env)
|
|
195
|
+
|
|
174
196
|
global _state
|
|
175
197
|
if _is_running(state=_state):
|
|
176
198
|
return _state["host"], _state["port"]
|
|
@@ -185,6 +207,16 @@ def setup_logging(
|
|
|
185
207
|
# 被 ``kill -9`` 也会让内核 close 它的 fd,子进程在另一端会读到 EOF。
|
|
186
208
|
parent_alive_send, parent_alive_recv = socket.socketpair()
|
|
187
209
|
|
|
210
|
+
# ENV 必须在 ``proc.start()`` 之前写入:spawn 出的服务子进程虽然通过
|
|
211
|
+
# kwargs 拿到了所有运行参数,但它在 bootstrap 阶段会用
|
|
212
|
+
# ``_fixup_main_from_path`` 重新 import 主脚本,主脚本顶层若间接触发
|
|
213
|
+
# ``kitty_logger.getLogger(...)``(例如某个被顶层 import 的库在模块级
|
|
214
|
+
# 调用了 getLogger),此时子进程必须已经能从 ENV 读到服务地址,否则
|
|
215
|
+
# 子进程会在 import 阶段抛 RuntimeError 而异常退出。
|
|
216
|
+
os.environ[ENV_HOST] = bound_host
|
|
217
|
+
os.environ[ENV_PORT] = str(bound_port)
|
|
218
|
+
os.environ[ENV_LEVEL] = str(level)
|
|
219
|
+
|
|
188
220
|
ctx = mp.get_context("spawn")
|
|
189
221
|
shutdown_event = ctx.Event()
|
|
190
222
|
proc = ctx.Process(
|
|
@@ -211,13 +243,6 @@ def setup_logging(
|
|
|
211
243
|
listening_sock.close()
|
|
212
244
|
parent_alive_recv.close()
|
|
213
245
|
|
|
214
|
-
# ENV 是给"日后由本进程 spawn 出来的工作子进程"用的——它们靠 ENV
|
|
215
|
-
# 找到服务地址。服务子进程本身已经通过 kwargs 拿到所有参数,因此在
|
|
216
|
-
# ``proc.start()`` 之后再写 ENV 没有竞态问题。
|
|
217
|
-
os.environ[ENV_HOST] = bound_host
|
|
218
|
-
os.environ[ENV_PORT] = str(bound_port)
|
|
219
|
-
os.environ[ENV_LEVEL] = str(level)
|
|
220
|
-
|
|
221
246
|
new_state: _RunningState = {
|
|
222
247
|
"running": True,
|
|
223
248
|
"host": bound_host,
|
|
@@ -228,13 +253,12 @@ def setup_logging(
|
|
|
228
253
|
}
|
|
229
254
|
_state = new_state
|
|
230
255
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
256
|
+
# 主进程也要把 ``kitty`` logger 初始化好——挂上 SocketHandler,让
|
|
257
|
+
# 主进程里 ``kitty_logger.getLogger(...)`` 拿到的子 logger 直接可用,
|
|
258
|
+
# 不需要再额外配置。
|
|
259
|
+
from ._client import _ensure_kitty_handler
|
|
234
260
|
|
|
235
|
-
|
|
236
|
-
logging.getLogger(), bound_host, bound_port, level, propagate=False
|
|
237
|
-
)
|
|
261
|
+
_ensure_kitty_handler()
|
|
238
262
|
|
|
239
263
|
_ = atexit.register(shutdown_logging)
|
|
240
264
|
return bound_host, bound_port
|
|
@@ -248,6 +272,10 @@ def _detach_all_socket_handlers(host: str, port: int) -> None:
|
|
|
248
272
|
(:mod:`logging` 静默吞掉 socket 错误,外部表现为日志凭空消失)。
|
|
249
273
|
在 shutdown 时主动卸载,保证状态对称——``setup_logging`` 装上去什么,
|
|
250
274
|
``shutdown_logging`` 全部取下来。
|
|
275
|
+
|
|
276
|
+
新架构下整个进程理论上只有 ``kitty`` logger 持有 SocketHandler,但
|
|
277
|
+
仍按"扫所有 logger"的方式做兜底,便于早期版本里残留的 handler 也
|
|
278
|
+
能在 shutdown 时被清理掉。
|
|
251
279
|
"""
|
|
252
280
|
from logging.handlers import SocketHandler
|
|
253
281
|
|
|
@@ -313,12 +341,6 @@ def shutdown_logging(timeout: float = 5.0) -> None:
|
|
|
313
341
|
# 否则后续 logging 调用会向一个失效端口发送,触发静默丢日志。
|
|
314
342
|
_detach_all_socket_handlers(host, port)
|
|
315
343
|
|
|
316
|
-
#
|
|
317
|
-
# 干净状态。
|
|
318
|
-
from . import _client
|
|
319
|
-
|
|
320
|
-
with _client._lock:
|
|
321
|
-
_client._configured_loggers.clear()
|
|
322
|
-
|
|
344
|
+
# 清掉环境变量,让"二次 setup"能真正回到干净状态。
|
|
323
345
|
for k in (ENV_HOST, ENV_PORT, ENV_LEVEL):
|
|
324
346
|
os.environ.pop(k, None)
|
|
@@ -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.dev2
|
|
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,20 @@ 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
|
|
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
58
|
启动日志服务子进程(始终使用 `spawn`)。幂等。已通过 `atexit` 注册清理。
|
|
59
59
|
`port=0` 让操作系统挑选空闲端口;返回真实绑定到的 `(host, port)`。
|
|
60
60
|
**`host` 必须是 loopback**——绑定非 loopback 地址会直接 `ValueError`。
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
同时把主进程内 `logging.getLogger("kitty")` 这一个 logger 初始化好——挂上
|
|
62
|
+
`SocketHandler`、设置 level、关闭 `propagate`。**不动 root logger**。
|
|
63
|
+
在 `multiprocessing` 启动的子进程里再次调用本函数会自动 no-op,仅返回
|
|
64
|
+
从父进程继承的 `(host, port)`,因此可以直接放在模块顶层而不必用
|
|
65
|
+
`if __name__ == "__main__":` 包裹。
|
|
66
|
+
- `getLogger(name=None) -> logging.Logger`
|
|
67
|
+
返回挂在 `kitty` 命名空间下的 logger:`getLogger("foo")` 实际拿到的是
|
|
68
|
+
`logging.getLogger("kitty.foo")`,`getLogger()` 拿到的是 `kitty` 本身。
|
|
69
|
+
整个进程内只有 `kitty` 持有 `SocketHandler`,子 logger 通过标准库的祖先链
|
|
70
|
+
冒泡上来,无需重复挂载。
|
|
69
71
|
- `shutdown_logging()` — 显式停止日志服务子进程。
|
|
70
72
|
|
|
71
73
|
## 为什么只支持 spawn
|
|
@@ -80,13 +82,12 @@ if __name__ == "__main__":
|
|
|
80
82
|
|
|
81
83
|
## 注意事项
|
|
82
84
|
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
双倍输出。要么让 kitty_logger 做唯一入口,要么保持默认 `False` 自行管理 root。
|
|
85
|
+
- **所有 logger 名都带 `kitty.` 前缀**,这是有意保留的标记:日志记录里
|
|
86
|
+
`%(name)s` 字段一眼可以和第三方库(urllib3、httpx 等)的输出区分。
|
|
87
|
+
`getLogger(__name__)` 在模块 `myapp.svc` 下实际得到的是 `kitty.myapp.svc`。
|
|
88
|
+
- `kitty` 子树**不向 root 冒泡**:第三方库挂在 root 上的 handler 不会收到
|
|
89
|
+
kitty_logger 的日志,反之 root 上输出的日志也不会被 kitty_logger 转发。
|
|
90
|
+
两条输出渠道互不干扰。
|
|
90
91
|
- `shutdown_logging()` 会卸载本进程内 kitty_logger 自己挂的
|
|
91
92
|
`SocketHandler`,并清理 `KITTY_LOGGER_*` 环境变量,确保进程状态与
|
|
92
93
|
`setup_logging` 对称。如果你额外挂了别的 handler,仍由你自己负责清理。
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- 半包 / 中途断连不会导致服务端死循环或丢失后续记录。
|
|
5
5
|
- 重复调用 ``setup_logging`` / ``shutdown_logging`` 是幂等的。
|
|
6
6
|
- ``setup_logging`` 绑定非 loopback 地址会直接 ``ValueError``。
|
|
7
|
+
- 新架构:``kitty`` 子树的 ``propagate`` 行为、子 logger 自动冒泡。
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -43,15 +44,14 @@ def _spawn_worker(idx: int) -> None:
|
|
|
43
44
|
@pytest.fixture(autouse=True)
|
|
44
45
|
def _isolate_state(monkeypatch):
|
|
45
46
|
"""确保每个用例都从干净的全局状态出发。"""
|
|
46
|
-
# setup_logging 是模块级单例,跑完后必须 shutdown 干净。
|
|
47
47
|
yield
|
|
48
48
|
try:
|
|
49
49
|
kitty_logger.shutdown_logging()
|
|
50
50
|
except Exception:
|
|
51
51
|
pass
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
#
|
|
52
|
+
# 清掉测试期间挂上去的 SocketHandler,并把 ``kitty`` 子树相关 logger
|
|
53
|
+
# 的状态(level / propagate / handlers)复位到默认值——避免前一个用例
|
|
54
|
+
# 残留的 ``propagate=False`` 把后一个用例的日志吞掉。
|
|
55
55
|
for lg in [logging.getLogger()] + [
|
|
56
56
|
obj
|
|
57
57
|
for obj in logging.Logger.manager.loggerDict.values()
|
|
@@ -60,6 +60,9 @@ def _isolate_state(monkeypatch):
|
|
|
60
60
|
for h in list(lg.handlers):
|
|
61
61
|
if isinstance(h, logging.handlers.SocketHandler):
|
|
62
62
|
lg.removeHandler(h)
|
|
63
|
+
if lg.name == "kitty" or lg.name.startswith("kitty."):
|
|
64
|
+
lg.setLevel(logging.NOTSET)
|
|
65
|
+
lg.propagate = True
|
|
63
66
|
# 清环境变量。
|
|
64
67
|
for k in ("KITTY_LOGGER_HOST", "KITTY_LOGGER_PORT", "KITTY_LOGGER_LEVEL"):
|
|
65
68
|
os.environ.pop(k, None)
|
|
@@ -89,6 +92,9 @@ def test_end_to_end_spawn(tmp_path: Path):
|
|
|
89
92
|
)
|
|
90
93
|
# 进程名应当出现在每行里,便于跨进程定位。
|
|
91
94
|
assert "MainProcess" in text and "SpawnProcess" in text
|
|
95
|
+
# logger 名都带 ``kitty.`` 前缀。
|
|
96
|
+
assert "kitty.main" in text
|
|
97
|
+
assert "kitty.worker.0" in text
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
def test_setup_logging_idempotent(tmp_path: Path):
|
|
@@ -215,34 +221,46 @@ def test_level_env_accepts_name(tmp_path: Path):
|
|
|
215
221
|
assert _client._read_level() == logging.INFO
|
|
216
222
|
|
|
217
223
|
|
|
218
|
-
def
|
|
219
|
-
"""
|
|
220
|
-
|
|
224
|
+
def test_setup_attaches_handler_to_kitty_only(tmp_path: Path):
|
|
225
|
+
"""setup_logging 只动 ``kitty`` logger,不去碰 root。"""
|
|
226
|
+
root = logging.getLogger()
|
|
227
|
+
before = list(root.handlers)
|
|
221
228
|
kitty_logger.setup_logging(
|
|
222
|
-
log_file=str(
|
|
229
|
+
log_file=str(tmp_path / "app.log"),
|
|
230
|
+
stream=False,
|
|
223
231
|
)
|
|
232
|
+
after = list(root.handlers)
|
|
233
|
+
assert after == before, "setup_logging 不应改动 root logger 的 handlers"
|
|
234
|
+
|
|
235
|
+
kitty = logging.getLogger("kitty")
|
|
236
|
+
sock_handlers = [
|
|
237
|
+
h for h in kitty.handlers if isinstance(h, logging.handlers.SocketHandler)
|
|
238
|
+
]
|
|
239
|
+
assert len(sock_handlers) == 1, "kitty logger 应当恰好挂一个 SocketHandler"
|
|
240
|
+
assert kitty.propagate is False, "kitty 不应向 root 冒泡"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_shutdown_cleans_state(tmp_path: Path):
|
|
244
|
+
"""shutdown 后:kitty 上的 SocketHandler 被卸载、ENV 被清。"""
|
|
245
|
+
log_path = tmp_path / "app.log"
|
|
246
|
+
kitty_logger.setup_logging(log_file=str(log_path), stream=False)
|
|
224
247
|
_ = kitty_logger.getLogger("svc.a")
|
|
225
248
|
|
|
249
|
+
kitty = logging.getLogger("kitty")
|
|
226
250
|
assert os.environ.get("KITTY_LOGGER_HOST")
|
|
227
|
-
assert any(
|
|
228
|
-
isinstance(h, logging.handlers.SocketHandler)
|
|
229
|
-
for h in logging.getLogger().handlers
|
|
230
|
-
)
|
|
231
|
-
assert _client._configured_loggers # 至少包含 "svc.a"
|
|
251
|
+
assert any(isinstance(h, logging.handlers.SocketHandler) for h in kitty.handlers)
|
|
232
252
|
|
|
233
253
|
kitty_logger.shutdown_logging()
|
|
234
254
|
|
|
235
255
|
assert not any(
|
|
236
|
-
isinstance(h, logging.handlers.SocketHandler)
|
|
237
|
-
for h in logging.getLogger().handlers
|
|
256
|
+
isinstance(h, logging.handlers.SocketHandler) for h in kitty.handlers
|
|
238
257
|
)
|
|
239
258
|
for k in ("KITTY_LOGGER_HOST", "KITTY_LOGGER_PORT", "KITTY_LOGGER_LEVEL"):
|
|
240
259
|
assert k not in os.environ
|
|
241
|
-
assert not _client._configured_loggers
|
|
242
260
|
|
|
243
261
|
|
|
244
262
|
def test_setup_after_shutdown_uses_new_port(tmp_path: Path):
|
|
245
|
-
"""shutdown 之后再 setup
|
|
263
|
+
"""shutdown 之后再 setup,新端口必须真正生效。"""
|
|
246
264
|
log_path = tmp_path / "app.log"
|
|
247
265
|
h1, p1 = kitty_logger.setup_logging(log_file=str(log_path), stream=False)
|
|
248
266
|
_ = kitty_logger.getLogger("svc.b")
|
|
@@ -253,37 +271,109 @@ def test_setup_after_shutdown_uses_new_port(tmp_path: Path):
|
|
|
253
271
|
log = kitty_logger.getLogger("svc.b")
|
|
254
272
|
log.info("after-restart")
|
|
255
273
|
|
|
256
|
-
#
|
|
274
|
+
# ``kitty`` 上的 SocketHandler 必须指向新端口。
|
|
275
|
+
kitty = logging.getLogger("kitty")
|
|
257
276
|
sock_handlers = [
|
|
258
|
-
h for h in
|
|
277
|
+
h for h in kitty.handlers if isinstance(h, logging.handlers.SocketHandler)
|
|
259
278
|
]
|
|
260
|
-
assert sock_handlers, "expected a SocketHandler attached to
|
|
279
|
+
assert sock_handlers, "expected a SocketHandler attached to kitty after restart"
|
|
261
280
|
assert all(h.port == p2 for h in sock_handlers)
|
|
262
281
|
assert p1 != p2 or h1 == h2 # 端口可能被 OS 复用,只要 handler 指向当前端口即可
|
|
263
282
|
|
|
264
283
|
_wait_for_lines(log_path2, lambda t: "after-restart" in t)
|
|
265
284
|
|
|
266
285
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
|
|
286
|
+
def test_attach_does_not_override_explicit_level(tmp_path: Path):
|
|
287
|
+
"""用户已显式给 ``kitty`` 设置过 level,setup_logging 不应悄悄覆盖。"""
|
|
288
|
+
kitty = logging.getLogger("kitty")
|
|
289
|
+
kitty.setLevel(logging.DEBUG)
|
|
270
290
|
kitty_logger.setup_logging(
|
|
271
291
|
log_file=str(tmp_path / "app.log"),
|
|
292
|
+
level=logging.WARNING,
|
|
272
293
|
stream=False,
|
|
273
|
-
attach_main_logger=False,
|
|
274
294
|
)
|
|
275
|
-
|
|
276
|
-
assert after == before, "attach_main_logger=False 不应改动 root logger 的 handlers"
|
|
295
|
+
assert kitty.level == logging.DEBUG
|
|
277
296
|
|
|
278
297
|
|
|
279
|
-
def
|
|
280
|
-
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
def test_child_logger_propagates_to_kitty(tmp_path: Path):
|
|
299
|
+
"""``getLogger("foo")`` 返回的 logger 名是 ``kitty.foo``,且会冒泡到 ``kitty``。"""
|
|
300
|
+
log_path = tmp_path / "app.log"
|
|
301
|
+
kitty_logger.setup_logging(log_file=str(log_path), stream=False)
|
|
302
|
+
|
|
303
|
+
log = kitty_logger.getLogger("foo.bar")
|
|
304
|
+
assert log.name == "kitty.foo.bar"
|
|
305
|
+
# 子 logger 自身没挂 SocketHandler——它靠 propagate 冒到 kitty。
|
|
306
|
+
assert not any(isinstance(h, logging.handlers.SocketHandler) for h in log.handlers)
|
|
307
|
+
assert log.propagate is True
|
|
308
|
+
|
|
309
|
+
log.info("from-foo-bar")
|
|
310
|
+
_wait_for_lines(log_path, lambda t: "from-foo-bar" in t and "kitty.foo.bar" in t)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_server_resilient_to_module_level_getlogger_in_user_script(tmp_path: Path):
|
|
314
|
+
"""回归:用户主脚本顶层 import 的库在模块级调用 kitty_logger.getLogger(...)
|
|
315
|
+
时,服务子进程在 ``spawn`` bootstrap 阶段也会触发该模块级调用,从而把
|
|
316
|
+
服务自身的 ``kitty`` logger 挂上一个指向本服务监听端口的 SocketHandler,
|
|
317
|
+
使得接收到的 record 顺着祖先链被发回服务自身、永远不落地。
|
|
318
|
+
``run_server`` 必须在开始 serve 之前把 ``kitty`` logger 还原成无 handler、
|
|
319
|
+
propagate=True 的状态。
|
|
320
|
+
"""
|
|
321
|
+
import subprocess
|
|
322
|
+
import sys as _sys
|
|
323
|
+
|
|
324
|
+
fake_lib = tmp_path / "fake_lib.py"
|
|
325
|
+
fake_lib.write_text(
|
|
326
|
+
"import kitty_logger\n" "log = kitty_logger.getLogger(__name__)\n",
|
|
327
|
+
encoding="utf-8",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
log_file = tmp_path / "app.log"
|
|
331
|
+
main_py = tmp_path / "main.py"
|
|
332
|
+
main_py.write_text(
|
|
333
|
+
"import sys, logging, time\n"
|
|
334
|
+
f"sys.path.insert(0, {str(tmp_path)!r})\n"
|
|
335
|
+
"from kitty_logger import setup_logging, getLogger\n"
|
|
336
|
+
"if __name__ == '__main__':\n"
|
|
337
|
+
f" setup_logging(log_file={str(log_file)!r}, stream=False, level=logging.DEBUG)\n"
|
|
338
|
+
"import fake_lib # noqa: E402,F401 -- 模块级 getLogger 触发回归路径\n"
|
|
339
|
+
"if __name__ == '__main__':\n"
|
|
340
|
+
" getLogger('user').info('real-user-log')\n"
|
|
341
|
+
" time.sleep(1)\n",
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
[_sys.executable, str(main_py)],
|
|
347
|
+
capture_output=True,
|
|
348
|
+
text=True,
|
|
349
|
+
timeout=20,
|
|
350
|
+
)
|
|
351
|
+
assert result.returncode == 0, f"stderr={result.stderr}"
|
|
352
|
+
|
|
353
|
+
text = _wait_for_lines(log_file, lambda t: "real-user-log" in t)
|
|
354
|
+
assert "kitty.user" in text
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _spawn_worker_calling_setup(idx: int, log_file: str) -> None:
|
|
358
|
+
"""子进程里也调一次 setup_logging:应该是 no-op,仅返回从 ENV 继承的端点。"""
|
|
359
|
+
h, p = kitty_logger.setup_logging(log_file=log_file, stream=False)
|
|
360
|
+
log = kitty_logger.getLogger(f"child.{idx}")
|
|
361
|
+
log.info("child says host=%s port=%s", h, p)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_setup_logging_in_child_is_noop(tmp_path: Path):
|
|
365
|
+
"""子进程里调用 setup_logging() 不应再起监听器,仅返回从 ENV 继承的端点。"""
|
|
366
|
+
log_path = tmp_path / "app.log"
|
|
367
|
+
host, port = kitty_logger.setup_logging(log_file=str(log_path), stream=False)
|
|
368
|
+
|
|
369
|
+
ctx = mp.get_context("spawn")
|
|
370
|
+
p = ctx.Process(target=_spawn_worker_calling_setup, args=(0, str(log_path)))
|
|
371
|
+
p.start()
|
|
372
|
+
p.join(timeout=10)
|
|
373
|
+
assert p.exitcode == 0
|
|
374
|
+
|
|
375
|
+
text = _wait_for_lines(
|
|
376
|
+
log_path,
|
|
377
|
+
lambda t: f"host={host} port={port}" in t and "kitty.child.0" in t,
|
|
287
378
|
)
|
|
288
|
-
|
|
289
|
-
assert custom.level == logging.DEBUG
|
|
379
|
+
assert "SpawnProcess" in text
|
|
@@ -1,108 +0,0 @@
|
|
|
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
|
-
import logging
|
|
10
|
-
import logging.handlers
|
|
11
|
-
import os
|
|
12
|
-
import threading
|
|
13
|
-
|
|
14
|
-
from ._env import ENV_HOST, ENV_LEVEL, ENV_PORT
|
|
15
|
-
|
|
16
|
-
# 保护下面这个模块级集合的并发访问。
|
|
17
|
-
_lock = threading.Lock()
|
|
18
|
-
|
|
19
|
-
# 已挂过 SocketHandler 的 logger 名集合,用于在同一进程内做去重。
|
|
20
|
-
# root logger 在集合里以空字符串 ``""`` 作为 key。
|
|
21
|
-
_configured_loggers: set[str] = set()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _read_endpoint() -> tuple[str, int]:
|
|
25
|
-
"""从环境变量读出 ``(host, port)``;未设置则抛出友好错误。"""
|
|
26
|
-
host = os.environ.get(ENV_HOST)
|
|
27
|
-
port = os.environ.get(ENV_PORT)
|
|
28
|
-
if not host or not port:
|
|
29
|
-
raise RuntimeError(
|
|
30
|
-
"kitty_logger 尚未在当前进程树中初始化。"
|
|
31
|
-
+ "请先在主进程调用 kitty_logger.setup_logging(),"
|
|
32
|
-
+ "再创建(spawn)子进程或调用 getLogger()。"
|
|
33
|
-
)
|
|
34
|
-
return host, int(port)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _read_level() -> int:
|
|
38
|
-
"""从环境变量解析默认 level。优先按整数解析,失败则按级别名解析。"""
|
|
39
|
-
raw = os.environ.get(ENV_LEVEL)
|
|
40
|
-
if raw is None:
|
|
41
|
-
return logging.INFO
|
|
42
|
-
try:
|
|
43
|
-
return int(raw)
|
|
44
|
-
except ValueError:
|
|
45
|
-
pass
|
|
46
|
-
return logging.getLevelNamesMapping().get(raw.upper(), logging.INFO)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _attach_socket_handler(
|
|
50
|
-
logger: logging.Logger,
|
|
51
|
-
host: str,
|
|
52
|
-
port: int,
|
|
53
|
-
level: int,
|
|
54
|
-
propagate: bool,
|
|
55
|
-
) -> None:
|
|
56
|
-
"""给 ``logger`` 挂上指向日志服务的 :class:`SocketHandler`。
|
|
57
|
-
|
|
58
|
-
.. note::
|
|
59
|
-
:class:`logging.handlers.SocketHandler` 直接 pickle ``LogRecord``
|
|
60
|
-
发送,由接收端格式化;在这里给 handler 设置 ``formatter`` 没有
|
|
61
|
-
任何效果,会被服务端忽略。
|
|
62
|
-
"""
|
|
63
|
-
SocketHandler = logging.handlers.SocketHandler
|
|
64
|
-
|
|
65
|
-
# 同一 logger 上避免重复挂指向相同端点的 SocketHandler——例如模块被
|
|
66
|
-
# 重新 import、或测试用例反复 setup 的场景。
|
|
67
|
-
for h in logger.handlers:
|
|
68
|
-
if isinstance(h, SocketHandler) and (h.host, h.port) == (host, port):
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
logger.addHandler(SocketHandler(host, port))
|
|
72
|
-
|
|
73
|
-
# 仅在 logger 仍是默认 ``NOTSET`` 时才设置 level,避免悄悄覆盖
|
|
74
|
-
# 用户已经显式设置过的级别。
|
|
75
|
-
if logger.level == logging.NOTSET:
|
|
76
|
-
logger.setLevel(level)
|
|
77
|
-
|
|
78
|
-
# 默认关闭向上传播:每个 logger 独立挂 SocketHandler,互不干扰,
|
|
79
|
-
# 也避免与第三方库共用 root handler 时的串扰。root 自身没有父级,
|
|
80
|
-
# 跳过此项。需要让子 logger 通过祖先链冒泡上来时显式 propagate=True。
|
|
81
|
-
if logger is not logging.getLogger():
|
|
82
|
-
logger.propagate = propagate
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def getLogger(name: str | None = None, *, propagate: bool = False) -> logging.Logger:
|
|
86
|
-
"""返回一个会把日志通过 :class:`SocketHandler` 发送到日志服务的 logger。
|
|
87
|
-
|
|
88
|
-
必须在父进程已经调用过 :func:`kitty_logger.setup_logging` 之后才能
|
|
89
|
-
使用。本库只支持 ``spawn`` 启动方式;spawn 出来的子进程模块级状态
|
|
90
|
-
天然为空,因此不需要处理"继承自父进程的 SocketHandler"问题。
|
|
91
|
-
|
|
92
|
-
:param name: logger 名;``None`` 表示 root logger。
|
|
93
|
-
:param propagate: 是否允许该 logger 把记录继续向上传播给父级 logger。
|
|
94
|
-
默认 ``False``——每个 logger 独立挂 SocketHandler,互不干扰;
|
|
95
|
-
想让子 logger 通过祖先链冒泡到这个 logger 时设为 ``True``。
|
|
96
|
-
对 root logger 该参数没有意义(root 没有父级),会被忽略。
|
|
97
|
-
:raises RuntimeError: 当前进程树尚未调用 ``setup_logging``。
|
|
98
|
-
"""
|
|
99
|
-
host, port = _read_endpoint()
|
|
100
|
-
level = _read_level()
|
|
101
|
-
logger = logging.getLogger(name)
|
|
102
|
-
|
|
103
|
-
with _lock:
|
|
104
|
-
key = name or ""
|
|
105
|
-
if key not in _configured_loggers:
|
|
106
|
-
_attach_socket_handler(logger, host, port, level, propagate)
|
|
107
|
-
_configured_loggers.add(key)
|
|
108
|
-
return logger
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kitty_logger-0.2.0.dev1 → kitty_logger-0.2.0.dev2}/src/kitty_logger.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|