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.
353 lines
11 KiB
Python
353 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
|
|
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
|
|
)
|
|
|
|
# Get total count
|
|
total_result = await db.execute(
|
|
select(func.count()).select_from(RequestLog).where(date_filter)
|
|
)
|
|
total_requests = total_result.scalar()
|
|
|
|
# Get success count (2xx status codes)
|
|
success_result = await db.execute(
|
|
select(func.count()).select_from(RequestLog).where(
|
|
and_(date_filter, RequestLog.status_code.between(200, 299))
|
|
)
|
|
)
|
|
success_requests = success_result.scalar()
|
|
|
|
# Get client error count (4xx status codes)
|
|
client_error_result = await db.execute(
|
|
select(func.count()).select_from(RequestLog).where(
|
|
and_(date_filter, RequestLog.status_code.between(400, 499))
|
|
)
|
|
)
|
|
client_error_requests = client_error_result.scalar()
|
|
|
|
# Get server error count (5xx status codes)
|
|
server_error_result = await db.execute(
|
|
select(func.count()).select_from(RequestLog).where(
|
|
and_(date_filter, RequestLog.status_code.between(500, 599))
|
|
)
|
|
)
|
|
server_error_requests = server_error_result.scalar()
|
|
|
|
# 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
|
|
} |