Files
chongming-core/test/test_logging_async.py
T

167 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
access 日志采样 + 异步日志队列(QueueHandler + QueueListener)测试
验证:
1. _should_log_access:错误(>=400)全量;成功按采样率(确定性计数);1.0 全量
2. QueueHandler + QueueListener:logger 只挂队列 handler,I/O 由后台监听线程落盘
3. 队列满不抛异常(丢弃而非阻塞/打断业务)
4. 端到端:真实 app 请求 -> access 日志异步落盘(含采样命中)
运行: uv run python test_logging_async.py
"""
import importlib.util
import itertools
import queue
import sys
import tempfile
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "gateway" / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "utils" / "worker" / "src"))
_tmp = tempfile.TemporaryDirectory()
# 预加载真实 logger 模块(不触发 chongming 包链导入),把日志目录重定向到
# 临时目录,之后 import chongming.app 时 get_logger 即用新路径
_logger_path = (
Path(__file__).resolve().parent.parent / "gateway" / "src" / "chongming" / "app" / "core" / "logger.py"
)
_logger_spec = importlib.util.spec_from_file_location("chongming.app.core.logger", _logger_path)
logger_mod = importlib.util.module_from_spec(_logger_spec)
sys.modules["chongming.app.core.logger"] = logger_mod
_logger_spec.loader.exec_module(logger_mod) # type: ignore[union-attr]
logger_mod.log_config["file"] = _tmp.name
import chongming.app as app_mod
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
def _wait_for(pred, timeout=3.0):
deadline = time.time() + timeout
while time.time() < deadline:
if pred():
return True
time.sleep(0.05)
return False
# ---- 1. 采样逻辑 ----
def test_sampling():
app_mod._access_log_counter = itertools.count()
check(
"错误 4xx/5xx 全量",
app_mod._should_log_access(404)
and app_mod._should_log_access(500)
and app_mod._should_log_access(503),
)
app_mod._access_log_counter = itertools.count()
check("rate>=1.0 全量", app_mod._should_log_access(200, 1.0) and app_mod._should_log_access(200, 1.0))
app_mod._access_log_counter = itertools.count()
logged = sum(1 for _ in range(10) if app_mod._should_log_access(200, 0.1))
check("0.1 采样率 10 个成功请求记 1 条", logged == 1, f"logged={logged}")
# ---- 2. 异步队列:logger 只挂队列 handler,监听线程落盘 ----
def test_async_queue_write():
from logging.handlers import QueueHandler
lg = logger_mod.get_logger("test_async")
check("logger 只挂队列 handler", len(lg.handlers) == 1 and isinstance(lg.handlers[0], QueueHandler))
check("监听线程已启动", logger_mod._listener is not None and logger_mod._listener._thread.is_alive())
for i in range(5):
lg.info("async log %d", i) # % 参数化:格式化在监听线程执行
ok = _wait_for(
lambda: any(
"async log 4" in f.read_text(errors="ignore")
for f in Path(_tmp.name).glob("test_async-*.log")
)
)
check("日志异步落盘(监听线程写入)", ok)
# ---- 3. 队列满:丢弃而非抛异常 ----
def test_queue_full_drops():
old = logger_mod._log_queue
logger_mod._log_queue = queue.Queue(maxsize=2) # 换小队列
try:
lg = logger_mod.get_logger("test_full")
try:
for i in range(20):
lg.info("flood %d", i)
check("队列满不抛异常(丢弃新日志)", True)
except Exception as e:
check("队列满不抛异常(丢弃新日志)", False, str(e))
finally:
logger_mod._log_queue = old
# ---- 4. 按名隔离:不同 log_name 的记录不串文件 ----
def test_name_isolation():
logger_mod.get_logger("test_b")
lg_a = logger_mod.get_logger("test_async")
lg_b = logger_mod.get_logger("test_b")
lg_a.info("隔离测试 A 专属 %d", 1)
lg_b.info("隔离测试 B 专属 %d", 2)
ok = _wait_for(
lambda: any(
"隔离测试 B 专属 2" in f.read_text(errors="ignore")
for f in Path(_tmp.name).glob("test_b-*.log")
)
)
check("test_b 记录写入 test_b 文件", ok)
a_text = "".join(f.read_text(errors="ignore") for f in Path(_tmp.name).glob("test_async-*.log"))
b_text = "".join(f.read_text(errors="ignore") for f in Path(_tmp.name).glob("test_b-*.log"))
check("test_async 记录不串入 test_b 文件", "隔离测试 A 专属" not in b_text)
check("test_b 记录不串入 test_async 文件", "隔离测试 B 专属" not in a_text)
# ---- 5. 端到端:中间件采样 + 异步落盘 ----
def test_e2e_middleware():
import asyncio
import httpx
app_mod._access_log_counter = itertools.count() # 重置采样计数
async def main():
transport = httpx.ASGITransport(app=app_mod.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/")
return r.status_code
code = asyncio.run(main())
check("请求返回 200", code == 200, str(code))
ok = _wait_for(
lambda: any(
"GET / status=200" in f.read_text(errors="ignore")
for f in Path(_tmp.name).glob("app-*.log")
)
)
check("access 日志异步落盘(采样命中)", ok)
test_sampling()
test_async_queue_write()
test_queue_full_drops()
test_name_isolation()
test_e2e_middleware()
# 关闭监听线程并释放文件句柄,再清理临时目录
logger_mod.shutdown_logging()
_tmp.cleanup()
print()
if failures:
print(f"共 {len(failures)} 项失败: {failures}")
sys.exit(1)
print("全部通过")