You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
11 KiB
Python

"""
Request log API endpoints
"""
from datetime import datetime, timedelta, timezone
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, and_, or_, func, case
from sqlalchemy.orm import selectinload
from app.core.database import get_db
from app.models.request_log import RequestLog
from app.schemas.request_log import (
RequestLogResponse,
RequestLogListResponse,
RequestLogStats
)
router = APIRouter()
@router.get(
"/logs",
response_model=RequestLogListResponse,
summary="Get request logs",
description="""
Retrieve request logs with filtering and pagination options.
**Filters:**
- Date range (start_date, end_date)
- HTTP method
- Status code range
- Endpoint pattern
- Response time range
**Sorting:**
- By date (newest first by default)
- By status code
- By response time
**Pagination:**
- Configurable page size (default: 50, max: 200)
- Page-based navigation
"""
)
async def get_request_logs(
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=200, description="Items per page"),
start_date: Optional[datetime] = Query(None, description="Filter by start date"),
end_date: Optional[datetime] = Query(None, description="Filter by end date"),
method: Optional[str] = Query(None, description="Filter by HTTP method"),
status_code: Optional[int] = Query(None, description="Filter by status code"),
endpoint: Optional[str] = Query(None, description="Filter by endpoint (supports wildcards)"),
min_response_time: Optional[float] = Query(None, description="Minimum response time in ms"),
max_response_time: Optional[float] = Query(None, description="Maximum response time in ms"),
sort_by: str = Query("created_at", description="Sort field: created_at, status_code, response_time_ms"),
sort_order: str = Query("desc", description="Sort order: asc or desc"),
db: AsyncSession = Depends(get_db)
):
"""Get paginated request logs with filters"""
# Build query
query = select(RequestLog)
# Apply filters
filters = []
if start_date:
filters.append(RequestLog.created_at >= start_date)
if end_date:
filters.append(RequestLog.created_at <= end_date)
if method:
filters.append(RequestLog.method == method.upper())
if status_code:
filters.append(RequestLog.status_code == status_code)
if endpoint:
# Support wildcard matching
if '*' in endpoint:
pattern = endpoint.replace('*', '%')
filters.append(RequestLog.endpoint.like(pattern))
else:
filters.append(RequestLog.endpoint == endpoint)
if min_response_time:
filters.append(RequestLog.response_time_ms >= min_response_time)
if max_response_time:
filters.append(RequestLog.response_time_ms <= max_response_time)
if filters:
query = query.where(and_(*filters))
# Apply sorting
sort_column = getattr(RequestLog, sort_by, RequestLog.created_at)
if sort_order.lower() == "desc":
query = query.order_by(desc(sort_column))
else:
query = query.order_by(sort_column)
# Get total count
count_query = select(func.count()).select_from(RequestLog)
if filters:
count_query = count_query.where(and_(*filters))
result = await db.execute(count_query)
total_count = result.scalar()
# Apply pagination
offset = (page - 1) * page_size
query = query.offset(offset).limit(page_size)
# Execute query
result = await db.execute(query)
request_logs = result.scalars().all()
# Calculate pagination info
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
return RequestLogListResponse(
items=[log.to_dict() for log in request_logs],
total=total_count,
page=page,
page_size=page_size,
total_pages=total_pages
)
@router.get(
"/logs/{log_id}",
response_model=RequestLogResponse,
summary="Get request log by ID",
description="Retrieve detailed information about a specific request log"
)
async def get_request_log(
log_id: int,
db: AsyncSession = Depends(get_db)
):
"""Get specific request log by ID"""
result = await db.execute(
select(RequestLog).where(RequestLog.id == log_id)
)
request_log = result.scalar_one_or_none()
if not request_log:
raise HTTPException(
status_code=404,
detail=f"Request log with ID {log_id} not found"
)
return RequestLogResponse(**request_log.to_dict())
@router.get(
"/stats",
response_model=RequestLogStats,
summary="Get request statistics",
description="""
Get aggregated statistics about API requests.
**Statistics include:**
- Total request count
- Success/error rates
- Requests by method
- Requests by status code
- Requests by endpoint
- Time-based trends
- Average response time
"""
)
async def get_request_stats(
start_date: Optional[datetime] = Query(None, description="Start date for statistics"),
end_date: Optional[datetime] = Query(None, description="End date for statistics"),
db: AsyncSession = Depends(get_db)
):
"""Get request statistics"""
# Default to last 7 days if no dates provided
if not end_date:
end_date = datetime.now(timezone.utc)
if not start_date:
start_date = end_date - timedelta(days=7)
# Build base filter
date_filter = and_(
RequestLog.created_at >= start_date,
RequestLog.created_at <= end_date
)
# Single query: total + success + client_error + server_error counts
counts_result = await db.execute(
select(
func.count().label('total'),
func.sum(case((RequestLog.status_code.between(200, 299), 1), else_=0)).label('success'),
func.sum(case((RequestLog.status_code.between(400, 499), 1), else_=0)).label('client_error'),
func.sum(case((RequestLog.status_code.between(500, 599), 1), else_=0)).label('server_error'),
).select_from(RequestLog).where(date_filter)
)
counts = counts_result.one()
total_requests = counts.total or 0
success_requests = counts.success or 0
client_error_requests = counts.client_error or 0
server_error_requests = counts.server_error or 0
# Get requests by method
method_result = await db.execute(
select(
RequestLog.method,
func.count().label('count')
).where(date_filter)
.group_by(RequestLog.method)
.order_by(desc('count'))
)
requests_by_method = {row.method: row.count for row in method_result}
# Get requests by status code
status_result = await db.execute(
select(
RequestLog.status_code,
func.count().label('count')
).where(date_filter)
.group_by(RequestLog.status_code)
.order_by(desc('count'))
.limit(10)
)
requests_by_status_code = {str(row.status_code): row.count for row in status_result}
# Get requests by endpoint (top 10)
endpoint_result = await db.execute(
select(
RequestLog.endpoint,
func.count().label('count')
).where(date_filter)
.group_by(RequestLog.endpoint)
.order_by(desc('count'))
.limit(10)
)
requests_by_endpoint = {row.endpoint: row.count for row in endpoint_result}
# Get average response time
avg_time_result = await db.execute(
select(func.avg(RequestLog.response_time_ms)).where(
and_(date_filter, RequestLog.response_time_ms.isnot(None))
)
)
avg_response_time = avg_time_result.scalar() or 0
# Get hourly trend for last 24 hours if within range
hourly_trend = {}
if (end_date - start_date).days <= 1:
hourly_result = await db.execute(
select(
func.to_char(RequestLog.created_at, 'YYYY-MM-DD HH24:00').label('hour'),
func.count().label('count')
).where(date_filter)
.group_by('hour')
.order_by('hour')
)
hourly_trend = {row.hour: row.count for row in hourly_result}
return RequestLogStats(
total_requests=total_requests,
success_requests=success_requests,
client_error_requests=client_error_requests,
server_error_requests=server_error_requests,
success_rate=(success_requests / total_requests * 100) if total_requests > 0 else 0,
requests_by_method=requests_by_method,
requests_by_status_code=requests_by_status_code,
requests_by_endpoint=requests_by_endpoint,
average_response_time_ms=avg_response_time,
hourly_trend=hourly_trend,
start_date=start_date.isoformat(),
end_date=end_date.isoformat()
)
@router.delete(
"/logs/old",
summary="Delete old request logs",
description="Delete request logs older than specified days"
)
async def delete_old_request_logs(
days_old: int = Query(30, ge=1, le=365, description="Delete logs older than this many days"),
db: AsyncSession = Depends(get_db)
):
"""Delete old request logs"""
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_old)
# Get count of logs to delete
count_result = await db.execute(
select(func.count()).select_from(RequestLog).where(RequestLog.created_at < cutoff_date)
)
count = count_result.scalar()
# Delete logs
await db.execute(
RequestLog.__table__.delete().where(RequestLog.created_at < cutoff_date)
)
await db.commit()
return {
"message": f"Deleted {count} request logs older than {days_old} days",
"deleted_count": count,
"cutoff_date": cutoff_date.isoformat()
}
@router.delete(
"/logs",
summary="Delete all request logs",
description="Delete all request logs (use with caution)"
)
async def delete_all_request_logs(
confirm: bool = Query(False, description="Must be true to confirm deletion"),
db: AsyncSession = Depends(get_db)
):
"""Delete all request logs"""
if not confirm:
raise HTTPException(
status_code=400,
detail="Must set confirm=true to delete all logs"
)
# Get count of logs to delete
count_result = await db.execute(
select(func.count()).select_from(RequestLog)
)
count = count_result.scalar()
# Delete all logs
await db.execute(RequestLog.__table__.delete())
await db.commit()
return {
"message": f"Deleted all {count} request logs",
"deleted_count": count
}