Skip to content

Commit 36d2794

Browse files
committed
fix(ci): 修复 GitHub Actions CI 全部失败问题(Ruff Lint/Format + 测试隔离)
- Ruff 格式化:server.py、logging/__init__.py 自动格式修复 - Ruff Lint:formatters.py UP017(timezone.utc → datetime.UTC)、 test_logging_dual_write.py I001/F401/E741×2(import 排序、未使用导入、歧义变量名) - 测试隔离:TestDualWriteIntegration 集成测试调用 dictConfig() 后未重置日志状态, 导致 coding.proxy 的 propagate=False 污染后续 caplog 测试,18 个测试全量运行时失败; 新增 _reset_logging_state() 在 finally 块中彻底恢复 propagate/handlers/level 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
1 parent c9f1596 commit 36d2794

4 files changed

Lines changed: 56 additions & 28 deletions

File tree

‎src/coding/proxy/config/server.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class LoggingConfig(BaseModel):
2626
max_bytes: 单个日志文件最大字节数(触发轮转)。默认 5 MB。
2727
backup_count: 保留的已压缩备份文件数。默认 5。
2828
"""
29+
2930
level: str = "INFO"
3031
file: str | None = None
3132
max_bytes: int = 5 * 1024 * 1024

‎src/coding/proxy/logging/__init__.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def build_log_config(
9898
},
9999
"access": {
100100
"()": "uvicorn.logging.AccessFormatter",
101-
'fmt': '%(asctime)s %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
101+
"fmt": '%(asctime)s %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
102102
"datefmt": "%Y-%m-%d %H:%M:%S",
103103
},
104104
},
@@ -157,7 +157,12 @@ def build_log_config(
157157

158158
# 为每个 logger 添加 file handler
159159
# 注意:uvicorn.error 无 handlers 键(通过 propagate 继承 uvicorn 的 handler)
160-
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access", "coding.proxy"):
160+
for logger_name in (
161+
"uvicorn",
162+
"uvicorn.error",
163+
"uvicorn.access",
164+
"coding.proxy",
165+
):
161166
logger_cfg = config["loggers"][logger_name]
162167
handlers = logger_cfg.get("handlers", [])
163168
if isinstance(handlers, list):

‎src/coding/proxy/logging/formatters.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import json
1010
import logging
11-
from datetime import datetime, timezone
11+
from datetime import UTC, datetime
1212

1313

1414
class JsonFormatter(logging.Formatter):
@@ -37,9 +37,7 @@ def format(self, record: logging.LogRecord) -> str:
3737
exception = self.formatException(record.exc_info)
3838

3939
log_entry: dict[str, object] = {
40-
"timestamp": datetime.fromtimestamp(
41-
record.created, tz=timezone.utc
42-
).isoformat(),
40+
"timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
4341
"level": record.levelname,
4442
"logger": record.name,
4543
"message": message,

‎tests/test_logging_dual_write.py‎

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@
1616
import logging
1717
import logging.config
1818

19-
import pytest
20-
19+
from coding.proxy.config.server import LoggingConfig
2120
from coding.proxy.logging import (
2221
JsonFormatter,
2322
_gzip_namer,
2423
_gzip_rotator,
2524
build_log_config,
2625
)
27-
from coding.proxy.config.server import LoggingConfig
28-
2926

3027
# ── JsonFormatter 测试 ──────────────────────────────────────────
3128

@@ -279,37 +276,64 @@ def test_file_can_be_disabled(self):
279276

280277

281278
class TestDualWriteIntegration:
279+
@staticmethod
280+
def _reset_logging_state() -> None:
281+
"""彻底重置 logging 模块全局状态,消除 dictConfig 的副作用.
282+
283+
``logging.config.dictConfig()`` 会修改管理器内部状态:
284+
- 移除已有 handler 并注入新的(StreamHandler + RotatingFileHandler)
285+
- 将 logger 级别设为 DEBUG
286+
- **将 propagate 设为 False**(这是导致后续 caplog 测试失败的关键原因)
287+
288+
若不重置,``coding.proxy`` 及其子 logger 的消息无法传播到 root logger,
289+
而 pytest 的 caplog fixture 恰好通过 root logger 捕获记录。
290+
"""
291+
manager = logging.root.manager
292+
for name, lg in list(manager.loggerDict.items()):
293+
if isinstance(lg, logging.Logger):
294+
lg.handlers.clear()
295+
lg.level = logging.NOTSET
296+
lg.propagate = True
297+
root = logging.getLogger()
298+
root.setLevel(logging.WARNING)
299+
282300
def test_debug_visible_in_file_not_console(self, tmp_path, capsys):
283301
"""验证 DEBUG 级别消息写入文件但不显示在控制台."""
284302
log_file = tmp_path / "integration.log"
285303
config = build_log_config(level="INFO", file_path=str(log_file))
286304

287-
logging.config.dictConfig(config)
305+
try:
306+
logging.config.dictConfig(config)
288307

289-
logger = logging.getLogger("coding.proxy.test_integration")
290-
logger.debug("debug_only_message")
291-
logger.info("info_message")
308+
logger = logging.getLogger("coding.proxy.test_integration")
309+
logger.debug("debug_only_message")
310+
logger.info("info_message")
292311

293-
# 验证文件包含两条记录
294-
log_content = log_file.read_text()
295-
lines = [l for l in log_content.strip().split("\n") if l]
296-
assert len(lines) == 2
312+
# 验证文件包含两条记录
313+
log_content = log_file.read_text()
314+
lines = [line for line in log_content.strip().split("\n") if line]
315+
assert len(lines) == 2
297316

298-
messages = [json.loads(l)["message"] for l in lines]
299-
assert "debug_only_message" in messages
300-
assert "info_message" in messages
317+
messages = [json.loads(line)["message"] for line in lines]
318+
assert "debug_only_message" in messages
319+
assert "info_message" in messages
320+
finally:
321+
self._reset_logging_state()
301322

302323
def test_console_only_shows_info_and_above(self, tmp_path, capsys):
303324
"""验证控制台只输出 INFO+ 级别的消息."""
304325
log_file = tmp_path / "console_filter.log"
305326
config = build_log_config(level="INFO", file_path=str(log_file))
306327

307-
logging.config.dictConfig(config)
328+
try:
329+
logging.config.dictConfig(config)
308330

309-
logger = logging.getLogger("coding.proxy.test_console")
310-
logger.debug("should_not_appear")
311-
logger.info("should_appear")
331+
logger = logging.getLogger("coding.proxy.test_console")
332+
logger.debug("should_not_appear")
333+
logger.info("should_appear")
312334

313-
captured = capsys.readouterr()
314-
assert "should_not_appear" not in captured.err
315-
assert "should_appear" in captured.err
335+
captured = capsys.readouterr()
336+
assert "should_not_appear" not in captured.err
337+
assert "should_appear" in captured.err
338+
finally:
339+
self._reset_logging_state()

0 commit comments

Comments
 (0)