Files
SmartUp/backend/app/routers/external_api_logs.py
T

197 lines
6.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),
offset: int = Query(0, ge=0),
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).
- limit: max rows returned (default 50, max 200).
- offset: pagination offset (default 0).
"""
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.offset(offset).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
]
@router.get("/summary/count")
def count_external_api_log_summary(
slow_ms: Optional[int] = Query(None),
hours: int = Query(24, ge=1, le=720),
db: Session = Depends(get_db),
_=Depends(get_current_user),
):
"""Return the number of distinct aggregated summary groups.
Uses the same grouping as /summary, returns the count of groups
(not the count of raw log rows).
"""
q = db.query(
ExternalApiLog.direction,
ExternalApiLog.target_type,
ExternalApiLog.target_id,
ExternalApiLog.target_name,
ExternalApiLog.method,
ExternalApiLog.path,
)
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,
)
return {"total": q.count()}
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