- Add api_logs table (migration 0007) and ApiLog model - Add web/lib/api_logger.py — httpx wrapper that records every outbound call - Wire api_logger into vk_sync, vk_catalog, and connections test endpoints - Add /admin/logs page with filters (service, method, status, time range, URL search) and expandable request/response detail - Add "Логи" nav link for admin users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
2.2 KiB
Python
83 lines
2.2 KiB
Python
"""Thin wrapper around httpx that logs every outbound API call to api_logs."""
|
|
import json
|
|
import time
|
|
import urllib.parse
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from web.database import SessionLocal
|
|
from web.models.connections import ApiLog
|
|
|
|
_MAX_BODY = 8000 # truncate stored bodies beyond this
|
|
|
|
|
|
def _service_from_url(url: str) -> str:
|
|
host = urllib.parse.urlparse(url).netloc
|
|
if "evotor" in host:
|
|
return "evotor"
|
|
if "vk.com" in host:
|
|
return "vk"
|
|
return "other"
|
|
|
|
|
|
def _truncate(text: str | None) -> str | None:
|
|
if text and len(text) > _MAX_BODY:
|
|
return text[:_MAX_BODY] + "…"
|
|
return text
|
|
|
|
|
|
def _record(
|
|
user_id: int | None,
|
|
method: str,
|
|
url: str,
|
|
request_body: str | None,
|
|
response_status: int | None,
|
|
response_body: str | None,
|
|
duration_ms: int,
|
|
) -> None:
|
|
try:
|
|
db = SessionLocal()
|
|
db.add(ApiLog(
|
|
user_id=user_id,
|
|
service=_service_from_url(url),
|
|
method=method.upper(),
|
|
url=url,
|
|
request_body=_truncate(request_body),
|
|
response_status=response_status,
|
|
response_body=_truncate(response_body),
|
|
duration_ms=duration_ms,
|
|
))
|
|
db.commit()
|
|
db.close()
|
|
except Exception:
|
|
pass # never let logging crash the caller
|
|
|
|
|
|
def get(url: str, *, user_id: int | None = None, **kwargs) -> httpx.Response:
|
|
t0 = time.monotonic()
|
|
resp = httpx.get(url, **kwargs)
|
|
ms = int((time.monotonic() - t0) * 1000)
|
|
try:
|
|
body = resp.text
|
|
except Exception:
|
|
body = None
|
|
_record(user_id, "GET", url, None, resp.status_code, body, ms)
|
|
return resp
|
|
|
|
|
|
def post(url: str, *, user_id: int | None = None, data: Any = None, json: Any = None, **kwargs) -> httpx.Response:
|
|
t0 = time.monotonic()
|
|
resp = httpx.post(url, data=data, json=json, **kwargs)
|
|
ms = int((time.monotonic() - t0) * 1000)
|
|
try:
|
|
req_body = resp.request.content.decode("utf-8", errors="replace") if resp.request.content else None
|
|
except Exception:
|
|
req_body = None
|
|
try:
|
|
body = resp.text
|
|
except Exception:
|
|
body = None
|
|
_record(user_id, "POST", url, req_body, resp.status_code, body, ms)
|
|
return resp
|