feat: 重构接口日志页面与接口监控看板,支持HTTP方法过滤和实时轮询
This commit is contained in:
@@ -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。"""
|
||||
|
||||
Reference in New Issue
Block a user