File size: 3,506 Bytes
fa1140b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""统一日志初始化:控制台 + 可选 ``logs/`` 轮转文件。

由 :func:`setup_logging` 在 lifespan 启动时调用。``[logging]`` 段控制:
是否落盘、目录、文件名、级别、单文件大小、保留份数。
"""
from __future__ import annotations

import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any

_FILE_HANDLER_NAME = "twoapi_rotating_file"
_CONSOLE_HANDLER_NAME = "twoapi_console"
_CONFIGURED = False


def setup_logging(settings: Any | None = None) -> None:
    """幂等配置 root logger。

    - 始终挂控制台 handler(uvicorn 已有则复用 root,只设级别)。
    - ``settings.log_enabled`` 为真时,额外挂 ``RotatingFileHandler`` 到
      ``{log_dir}/{log_filename}``。
    """
    global _CONFIGURED
    level_name = "INFO"
    log_enabled = True
    log_dir = "logs"
    log_filename = "gateway.log"
    max_bytes = 10 * 1024 * 1024
    backup_count = 5

    if settings is not None:
        level_name = (getattr(settings, "log_level", None) or "INFO").upper()
        log_enabled = bool(getattr(settings, "log_enabled", True))
        log_dir = str(getattr(settings, "log_dir", None) or "logs")
        log_filename = str(getattr(settings, "log_filename", None) or "gateway.log")
        max_bytes = int(getattr(settings, "log_max_bytes", None) or max_bytes)
        backup_count = int(getattr(settings, "log_backup_count", None) or backup_count)

    level = getattr(logging, level_name, logging.INFO)
    fmt = logging.Formatter(
        fmt="%(asctime)s %(levelname)s [%(name)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    root = logging.getLogger()
    root.setLevel(level)

    # 控制台:仅当 root 尚无 StreamHandler 时添加,避免与 uvicorn 重复刷屏
    has_console = any(
        isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
        for h in root.handlers
    )
    if not has_console:
        console = logging.StreamHandler(sys.stderr)
        console.set_name(_CONSOLE_HANDLER_NAME)
        console.setFormatter(fmt)
        console.setLevel(level)
        root.addHandler(console)

    # 文件:先移除旧的同名 handler,再按 enabled 决定是否挂上
    for h in list(root.handlers):
        if getattr(h, "name", None) == _FILE_HANDLER_NAME:
            root.removeHandler(h)
            h.close()

    if log_enabled:
        path = Path(log_dir)
        path.mkdir(parents=True, exist_ok=True)
        file_path = path / log_filename
        fh = RotatingFileHandler(
            file_path,
            maxBytes=max(1024, max_bytes),
            backupCount=max(0, backup_count),
            encoding="utf-8",
        )
        fh.set_name(_FILE_HANDLER_NAME)
        fh.setFormatter(fmt)
        fh.setLevel(level)
        root.addHandler(fh)

    for name in (
        "app",
        "app.upstream",
        "app.adapters",
        "app.deps",
        "app.orchestrator",
        "app.http_log",
        "app.auto_register",
    ):
        logging.getLogger(name).setLevel(level)

    _CONFIGURED = True


def reset_logging_for_tests() -> None:
    """测试用:移除本模块添加的 handler,重置状态。"""
    global _CONFIGURED
    root = logging.getLogger()
    for h in list(root.handlers):
        if getattr(h, "name", None) in (_FILE_HANDLER_NAME, _CONSOLE_HANDLER_NAME):
            root.removeHandler(h)
            h.close()
    _CONFIGURED = False