feat: 重构接口日志页面与接口监控看板,支持HTTP方法过滤和实时轮询

This commit is contained in:
liumangmang
2026-07-01 15:04:32 +08:00
parent 397a14c978
commit 8abfa4c5ef
19 changed files with 2463 additions and 37 deletions
+64 -15
View File
@@ -1,13 +1,16 @@
from __future__ import annotations
import logging
import sys
import time
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from typing import Any
from urllib.parse import quote
from urllib.parse import quote, urlparse
import httpx
from app.utils.number import fixed_decimal_number, fixed_decimal_string
from app.services.external_api_logger import log_external_api_call
logger = logging.getLogger(__name__)
@@ -136,12 +139,16 @@ class Sub2ApiWebsiteClient:
auth_type: str,
auth_config: dict[str, Any],
timeout: float = 30.0,
target_id: int | None = None,
target_name: str | None = None,
) -> None:
self.base_url = base_url.rstrip("/")
self.api_prefix = api_prefix.strip("/")
self.auth_type = auth_type
self.auth_config = auth_config
self.timeout = timeout
self.target_id = target_id
self.target_name = target_name
self._client = httpx.Client(timeout=timeout)
def close(self) -> None:
@@ -171,21 +178,63 @@ class Sub2ApiWebsiteClient:
return headers
def _request(self, method: str, path: str, body: Any = None) -> Any:
url = self._url(path)
started = time.monotonic()
status_code: int | None = None
error_type: str | None = None
error_msg: str | None = None
try:
resp = self._client.request(method, self._url(path), json=body, headers=self._headers())
resp.raise_for_status()
except httpx.HTTPStatusError as exc:
raise WebsiteError(_friendly_http_error(exc)) from exc
except httpx.TimeoutException as exc:
raise WebsiteError(_friendly_connection_error(exc)) from exc
except httpx.ConnectError as exc:
raise WebsiteError(_friendly_connection_error(exc)) from exc
if not resp.content:
return None
text = resp.text
if "application/json" not in resp.headers.get("content-type", "") and text.lstrip().startswith("<"):
raise WebsiteError(f"{method} {path} 返回了 HTML,请检查接口地址是否正确")
return resp.json()
try:
resp = self._client.request(method, url, json=body, headers=self._headers())
except httpx.TimeoutException as exc:
error_type, error_msg = type(exc).__name__, str(exc)[:500]
raise WebsiteError(_friendly_connection_error(exc)) from exc
except httpx.ConnectError as exc:
error_type, error_msg = type(exc).__name__, str(exc)[:500]
raise WebsiteError(_friendly_connection_error(exc)) from exc
except httpx.HTTPStatusError as exc:
error_type, error_msg = type(exc).__name__, str(exc)[:500]
status_code = exc.response.status_code
raise WebsiteError(_friendly_http_error(exc)) from exc
status_code = getattr(resp, "status_code", None)
try:
resp.raise_for_status()
except httpx.HTTPStatusError as exc:
error_type, error_msg = type(exc).__name__, str(exc)[:500]
status_code = exc.response.status_code
raise WebsiteError(_friendly_http_error(exc)) from exc
if not resp.content:
return None
text = resp.text
if "application/json" not in resp.headers.get("content-type", "") and text.lstrip().startswith("<"):
error_type = "WebsiteError"
error_msg = f"{method} {path} returned HTML"
raise WebsiteError(f"{method} {path} 返回了 HTML,请检查接口地址是否正确")
return resp.json()
except WebsiteError:
if error_type is None:
error_type = "WebsiteError"
e = sys.exc_info()[1]
if e:
error_msg = str(e)[:500]
raise
finally:
elapsed = int((time.monotonic() - started) * 1000)
parsed = urlparse(url)
log_external_api_call(
direction="website",
target_type="website",
method=method,
path=parsed.path or path,
url_host=parsed.hostname or "",
duration_ms=elapsed,
status_code=status_code,
success=error_type is None,
target_id=self.target_id,
target_name=self.target_name,
error_type=error_type,
error_message=error_msg,
)
def get_groups(self, endpoint: str = "/groups") -> list[dict[str, Any]]:
"""拉取分组列表,尝试 endpoint 和 fallback /groups/all。"""