68 lines
2.0 KiB
Python
68 lines
2.0 KiB
Python
"""HTTP 访问日志与转发内容摘要(可配置是否打出正文预览)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
import time
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
from starlette.middleware.base import BaseHTTPMiddleware
|
||
from starlette.requests import Request
|
||
from starlette.responses import Response
|
||
|
||
access_logger = logging.getLogger("tg_bridge.access")
|
||
|
||
|
||
def log_preview_max_chars() -> int:
|
||
"""环境变量 BRIDGE_LOG_PREVIEW_CHARS:>0 时在业务日志中附带截断后的正文/回复预览;未设置或 0 则只打长度。"""
|
||
raw = os.environ.get("BRIDGE_LOG_PREVIEW_CHARS", "").strip()
|
||
if not raw:
|
||
return 0
|
||
try:
|
||
return max(0, int(raw))
|
||
except ValueError:
|
||
return 0
|
||
|
||
|
||
def preview_for_log(text: str | None, max_chars: int) -> str:
|
||
if not text or max_chars <= 0:
|
||
return ""
|
||
s = text.replace("\r", "").replace("\n", "\\n")
|
||
if len(s) <= max_chars:
|
||
return s
|
||
return s[:max_chars] + "..."
|
||
|
||
|
||
class AccessLogMiddleware(BaseHTTPMiddleware):
|
||
"""记录每条 HTTP 请求的方法、路径、状态码与耗时(不含鉴权头与 Body)。"""
|
||
|
||
async def dispatch(self, request: Request, call_next: Callable[[Request], Any]) -> Response:
|
||
start = time.perf_counter()
|
||
client = request.client.host if request.client else "-"
|
||
path = request.url.path
|
||
method = request.method
|
||
try:
|
||
response = await call_next(request)
|
||
except Exception:
|
||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||
access_logger.exception(
|
||
'%s "%s %s" 未捕获异常 %.1fms',
|
||
client,
|
||
method,
|
||
path,
|
||
elapsed_ms,
|
||
)
|
||
raise
|
||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||
access_logger.info(
|
||
'%s "%s %s" %s %.1fms',
|
||
client,
|
||
method,
|
||
path,
|
||
response.status_code,
|
||
elapsed_ms,
|
||
)
|
||
return response
|