162 lines
5.9 KiB
Python
162 lines
5.9 KiB
Python
"""External API logs listing with filters and summary."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import cast, case, func, Integer
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import get_db
|
|
from app.models.external_api_log import ExternalApiLog
|
|
from app.schemas.external_api_log import ExternalApiLogResponse, ExternalApiLogSummaryItem
|
|
from app.utils.auth import get_current_user
|
|
from app.config import get_settings
|
|
|
|
router = APIRouter(prefix="/api/external-api-logs", tags=["external_api_logs"])
|
|
|
|
|
|
@router.get("", response_model=List[ExternalApiLogResponse])
|
|
def list_external_api_logs(
|
|
direction: Optional[str] = Query(None),
|
|
target_type: Optional[str] = Query(None),
|
|
target_id: Optional[int] = Query(None),
|
|
method: Optional[str] = Query(None),
|
|
success: Optional[bool] = Query(None),
|
|
status_code: Optional[int] = Query(None),
|
|
path: Optional[str] = Query(None),
|
|
limit: int = Query(100, le=500),
|
|
offset: int = Query(0),
|
|
db: Session = Depends(get_db),
|
|
_=Depends(get_current_user),
|
|
):
|
|
q = db.query(ExternalApiLog)
|
|
if direction:
|
|
q = q.filter(ExternalApiLog.direction == direction)
|
|
if target_type:
|
|
q = q.filter(ExternalApiLog.target_type == target_type)
|
|
if target_id is not None:
|
|
q = q.filter(ExternalApiLog.target_id == target_id)
|
|
if method and isinstance(method, str):
|
|
q = q.filter(ExternalApiLog.method == method.upper())
|
|
if success is not None:
|
|
q = q.filter(ExternalApiLog.success == success)
|
|
if status_code is not None:
|
|
q = q.filter(ExternalApiLog.status_code == status_code)
|
|
if path:
|
|
q = q.filter(ExternalApiLog.path.like(f"%{path}%"))
|
|
logs = q.order_by(ExternalApiLog.created_at.desc()).offset(offset).limit(limit).all()
|
|
return logs
|
|
|
|
|
|
@router.get("/count")
|
|
def count_external_api_logs(
|
|
direction: Optional[str] = Query(None),
|
|
target_type: Optional[str] = Query(None),
|
|
target_id: Optional[int] = Query(None),
|
|
method: Optional[str] = Query(None),
|
|
success: Optional[bool] = Query(None),
|
|
status_code: Optional[int] = Query(None),
|
|
path: Optional[str] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
_=Depends(get_current_user),
|
|
):
|
|
q = db.query(ExternalApiLog)
|
|
if direction:
|
|
q = q.filter(ExternalApiLog.direction == direction)
|
|
if target_type:
|
|
q = q.filter(ExternalApiLog.target_type == target_type)
|
|
if target_id is not None:
|
|
q = q.filter(ExternalApiLog.target_id == target_id)
|
|
if method and isinstance(method, str):
|
|
q = q.filter(ExternalApiLog.method == method.upper())
|
|
if success is not None:
|
|
q = q.filter(ExternalApiLog.success == success)
|
|
if status_code is not None:
|
|
q = q.filter(ExternalApiLog.status_code == status_code)
|
|
if path:
|
|
q = q.filter(ExternalApiLog.path.like(f"%{path}%"))
|
|
return {"total": q.count()}
|
|
|
|
|
|
@router.get("/summary", response_model=List[ExternalApiLogSummaryItem])
|
|
def get_external_api_log_summary(
|
|
slow_ms: Optional[int] = Query(None),
|
|
hours: int = Query(24, ge=1, le=720),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
db: Session = Depends(get_db),
|
|
_=Depends(get_current_user),
|
|
):
|
|
"""Return aggregated summary grouped by direction + target + method + path.
|
|
|
|
- hours: time window (default 24, max 720). None means no time filter.
|
|
- limit: max rows returned (default 50, max 200).
|
|
"""
|
|
settings = get_settings()
|
|
slow_threshold = slow_ms if slow_ms is not None else settings.external_api_log_slow_ms
|
|
|
|
q = db.query(*_summary_cols(slow_threshold))
|
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
q = q.filter(ExternalApiLog.created_at >= cutoff)
|
|
q = q.group_by(
|
|
ExternalApiLog.direction,
|
|
ExternalApiLog.target_type,
|
|
ExternalApiLog.target_id,
|
|
ExternalApiLog.target_name,
|
|
ExternalApiLog.method,
|
|
ExternalApiLog.path,
|
|
).order_by(func.sum(ExternalApiLog.duration_ms).desc())
|
|
q = q.limit(limit)
|
|
rows = q.all()
|
|
|
|
return [
|
|
ExternalApiLogSummaryItem(
|
|
direction=row.direction,
|
|
target_type=row.target_type,
|
|
target_id=row.target_id,
|
|
target_name=row.target_name,
|
|
method=row.method,
|
|
path=row.path,
|
|
call_count=row.call_count,
|
|
success_count=row.success_count or 0,
|
|
fail_count=row.fail_count or 0,
|
|
avg_duration_ms=round(float(row.avg_duration_ms or 0), 1),
|
|
max_duration_ms=row.max_duration_ms or 0,
|
|
slow_count=row.slow_count or 0,
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def _summary_cols(slow_threshold: int) -> list:
|
|
return [
|
|
ExternalApiLog.direction,
|
|
ExternalApiLog.target_type,
|
|
ExternalApiLog.target_id,
|
|
ExternalApiLog.target_name,
|
|
ExternalApiLog.method,
|
|
ExternalApiLog.path,
|
|
func.count(ExternalApiLog.id).label("call_count"),
|
|
func.sum(cast(ExternalApiLog.success, Integer)).label("success_count"),
|
|
func.sum(cast(1 - cast(ExternalApiLog.success, Integer), Integer)).label("fail_count"),
|
|
func.avg(ExternalApiLog.duration_ms).label("avg_duration_ms"),
|
|
func.max(ExternalApiLog.duration_ms).label("max_duration_ms"),
|
|
func.sum(case((ExternalApiLog.duration_ms >= slow_threshold, 1), else_=0)).label("slow_count"),
|
|
]
|
|
|
|
|
|
def clean_expired_logs(db: Session) -> int:
|
|
"""Delete external API logs older than the configured retention period.
|
|
|
|
Returns the number of deleted rows.
|
|
"""
|
|
settings = get_settings()
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=settings.external_api_log_retention_days)
|
|
deleted = db.query(ExternalApiLog).filter(
|
|
ExternalApiLog.created_at < cutoff
|
|
).delete(synchronize_session=False)
|
|
if deleted:
|
|
db.commit()
|
|
return deleted
|