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.
419 lines
13 KiB
Python
419 lines
13 KiB
Python
"""
|
|
Error 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 case, select, desc, and_, or_, func
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.core.database import get_db
|
|
from app.models.error_log import ErrorLog
|
|
from app.schemas.error_log import (
|
|
ErrorLogResponse,
|
|
ErrorLogListResponse,
|
|
ErrorLogStats,
|
|
ErrorLogUpdate
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
ALLOWED_SORT_FIELDS = {"created_at", "status_code", "response_time_ms"}
|
|
|
|
|
|
@router.get(
|
|
"/logs",
|
|
response_model=ErrorLogListResponse,
|
|
summary="Get error logs",
|
|
description="""
|
|
Retrieve error logs with filtering and pagination options.
|
|
|
|
**Filters:**
|
|
- Date range (start_date, end_date)
|
|
- Error type
|
|
- Status code range
|
|
- Endpoint pattern
|
|
- Resolution status
|
|
|
|
**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_error_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"),
|
|
error_type: Optional[str] = Query(None, description="Filter by error type"),
|
|
status_code: Optional[int] = Query(None, description="Filter by status code"),
|
|
endpoint: Optional[str] = Query(None, description="Filter by endpoint (supports wildcards)"),
|
|
is_resolved: Optional[bool] = Query(None, description="Filter by resolution status"),
|
|
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 error logs with filters"""
|
|
|
|
# Build query
|
|
query = select(ErrorLog)
|
|
|
|
# Apply filters
|
|
filters = []
|
|
|
|
if start_date:
|
|
filters.append(ErrorLog.created_at >= start_date)
|
|
if end_date:
|
|
filters.append(ErrorLog.created_at <= end_date)
|
|
if error_type:
|
|
filters.append(ErrorLog.error_type == error_type)
|
|
if status_code:
|
|
filters.append(ErrorLog.status_code == status_code)
|
|
if endpoint:
|
|
# Support wildcard matching
|
|
if '*' in endpoint:
|
|
pattern = endpoint.replace('*', '%')
|
|
filters.append(ErrorLog.endpoint.like(pattern))
|
|
else:
|
|
filters.append(ErrorLog.endpoint == endpoint)
|
|
if is_resolved is not None:
|
|
filters.append(ErrorLog.is_resolved == is_resolved)
|
|
|
|
if filters:
|
|
query = query.where(and_(*filters))
|
|
|
|
# Apply sorting (validate sort_by against allowed fields)
|
|
if sort_by not in ALLOWED_SORT_FIELDS:
|
|
sort_by = "created_at"
|
|
sort_column = getattr(ErrorLog, sort_by, ErrorLog.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(ErrorLog)
|
|
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)
|
|
error_logs = result.scalars().all()
|
|
|
|
# Calculate pagination info
|
|
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
|
|
|
|
return ErrorLogListResponse(
|
|
items=[log.to_dict() for log in error_logs],
|
|
total=total_count,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=total_pages
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/logs/{log_id}",
|
|
response_model=ErrorLogResponse,
|
|
summary="Get error log by ID",
|
|
description="Retrieve detailed information about a specific error log"
|
|
)
|
|
async def get_error_log(
|
|
log_id: int,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get specific error log by ID"""
|
|
|
|
result = await db.execute(
|
|
select(ErrorLog).where(ErrorLog.id == log_id)
|
|
)
|
|
error_log = result.scalar_one_or_none()
|
|
|
|
if not error_log:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Error log with ID {log_id} not found"
|
|
)
|
|
|
|
return ErrorLogResponse(**error_log.to_dict())
|
|
|
|
|
|
@router.get(
|
|
"/by-request/{request_id}",
|
|
response_model=ErrorLogResponse,
|
|
summary="Get error log by request ID",
|
|
description="Retrieve error log information for a specific request ID"
|
|
)
|
|
async def get_error_by_request_id(
|
|
request_id: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get error log by request ID"""
|
|
|
|
result = await db.execute(
|
|
select(ErrorLog).where(ErrorLog.request_id == request_id).order_by(desc(ErrorLog.created_at))
|
|
)
|
|
error_log = result.scalar_one_or_none()
|
|
|
|
if not error_log:
|
|
# Return 404 but it's ok if no error exists for this request
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"No error log found for request ID {request_id}"
|
|
)
|
|
|
|
return ErrorLogResponse(**error_log.to_dict())
|
|
|
|
|
|
@router.get(
|
|
"/stats",
|
|
response_model=ErrorLogStats,
|
|
summary="Get error statistics",
|
|
description="""
|
|
Get aggregated statistics about errors.
|
|
|
|
**Statistics include:**
|
|
- Total error count
|
|
- Errors by type
|
|
- Errors by status code
|
|
- Errors by endpoint
|
|
- Time-based trends
|
|
- Resolution rate
|
|
"""
|
|
)
|
|
async def get_error_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 error 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_(
|
|
ErrorLog.created_at >= start_date,
|
|
ErrorLog.created_at <= end_date
|
|
)
|
|
|
|
# Get total, resolved count, and avg response time in a single query
|
|
summary_result = await db.execute(
|
|
select(
|
|
func.count().label('total'),
|
|
func.count(case((ErrorLog.is_resolved == True, 1))).label('resolved'),
|
|
func.avg(case((ErrorLog.response_time_ms.isnot(None), ErrorLog.response_time_ms))).label('avg_time'),
|
|
).select_from(ErrorLog).where(date_filter)
|
|
)
|
|
summary_row = summary_result.one()
|
|
total_errors = summary_row.total
|
|
resolved_errors = summary_row.resolved
|
|
avg_response_time = summary_row.avg_time or 0
|
|
|
|
# Get errors by type
|
|
type_result = await db.execute(
|
|
select(
|
|
ErrorLog.error_type,
|
|
func.count().label('count')
|
|
).where(date_filter)
|
|
.group_by(ErrorLog.error_type)
|
|
.order_by(desc('count'))
|
|
.limit(10)
|
|
)
|
|
errors_by_type = {row.error_type: row.count for row in type_result}
|
|
|
|
# Get errors by status code
|
|
status_result = await db.execute(
|
|
select(
|
|
ErrorLog.status_code,
|
|
func.count().label('count')
|
|
).where(date_filter)
|
|
.group_by(ErrorLog.status_code)
|
|
.order_by(desc('count'))
|
|
.limit(10)
|
|
)
|
|
errors_by_status = {str(row.status_code): row.count for row in status_result}
|
|
|
|
# Get errors by endpoint (top 10)
|
|
endpoint_result = await db.execute(
|
|
select(
|
|
ErrorLog.endpoint,
|
|
func.count().label('count')
|
|
).where(date_filter)
|
|
.group_by(ErrorLog.endpoint)
|
|
.order_by(desc('count'))
|
|
.limit(10)
|
|
)
|
|
errors_by_endpoint = {row.endpoint: row.count for row in endpoint_result}
|
|
|
|
# Get hourly trend for last 24 hours if within range
|
|
hourly_trend = {}
|
|
if (end_date - start_date).days <= 1:
|
|
hour_expr = func.to_char(ErrorLog.created_at, 'YYYY-MM-DD HH24:00').label('hour')
|
|
hourly_result = await db.execute(
|
|
select(
|
|
hour_expr,
|
|
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 ErrorLogStats(
|
|
total_errors=total_errors,
|
|
resolved_errors=resolved_errors,
|
|
unresolved_errors=total_errors - resolved_errors,
|
|
resolution_rate=(resolved_errors / total_errors * 100) if total_errors > 0 else 0,
|
|
errors_by_type=errors_by_type,
|
|
errors_by_status_code=errors_by_status,
|
|
errors_by_endpoint=errors_by_endpoint,
|
|
average_response_time_ms=avg_response_time,
|
|
hourly_trend=hourly_trend,
|
|
start_date=start_date.isoformat(),
|
|
end_date=end_date.isoformat()
|
|
)
|
|
|
|
|
|
@router.patch(
|
|
"/logs/{log_id}",
|
|
response_model=ErrorLogResponse,
|
|
summary="Update error log",
|
|
description="Update error log resolution status and notes"
|
|
)
|
|
async def update_error_log(
|
|
log_id: int,
|
|
update_data: ErrorLogUpdate,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update error log (mark as resolved, add notes, etc.)"""
|
|
|
|
result = await db.execute(
|
|
select(ErrorLog).where(ErrorLog.id == log_id)
|
|
)
|
|
error_log = result.scalar_one_or_none()
|
|
|
|
if not error_log:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Error log with ID {log_id} not found"
|
|
)
|
|
|
|
# Update fields
|
|
if update_data.is_resolved is not None:
|
|
error_log.is_resolved = update_data.is_resolved
|
|
if update_data.is_resolved:
|
|
error_log.resolved_at = datetime.now(timezone.utc)
|
|
else:
|
|
error_log.resolved_at = None
|
|
|
|
if update_data.resolution_notes is not None:
|
|
error_log.resolution_notes = update_data.resolution_notes
|
|
|
|
await db.commit()
|
|
await db.refresh(error_log)
|
|
|
|
return ErrorLogResponse(**error_log.to_dict())
|
|
|
|
|
|
@router.delete(
|
|
"/logs/old",
|
|
summary="Delete old error logs",
|
|
description="Delete error logs older than specified days"
|
|
)
|
|
async def delete_old_logs(
|
|
days_old: int = Query(30, ge=1, le=365, description="Delete logs older than this many days"),
|
|
only_resolved: bool = Query(True, description="Only delete resolved errors"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Delete old error logs"""
|
|
|
|
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_old)
|
|
|
|
# Build delete query
|
|
filters = [ErrorLog.created_at < cutoff_date]
|
|
if only_resolved:
|
|
filters.append(ErrorLog.is_resolved == True)
|
|
|
|
# Get count of logs to delete
|
|
count_result = await db.execute(
|
|
select(func.count()).select_from(ErrorLog).where(and_(*filters))
|
|
)
|
|
count = count_result.scalar()
|
|
|
|
# Delete logs
|
|
await db.execute(
|
|
ErrorLog.__table__.delete().where(and_(*filters))
|
|
)
|
|
await db.commit()
|
|
|
|
return {
|
|
"message": f"Deleted {count} error logs older than {days_old} days",
|
|
"deleted_count": count,
|
|
"cutoff_date": cutoff_date.isoformat()
|
|
}
|
|
|
|
|
|
@router.delete(
|
|
"/logs",
|
|
summary="Delete all error logs",
|
|
description="Delete all error logs (use with caution)"
|
|
)
|
|
async def delete_all_error_logs(
|
|
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
|
only_resolved: bool = Query(False, description="Only delete resolved errors"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Delete all error logs"""
|
|
|
|
if not confirm:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Must set confirm=true to delete all logs"
|
|
)
|
|
|
|
# Build delete query
|
|
filters = []
|
|
if only_resolved:
|
|
filters.append(ErrorLog.is_resolved == True)
|
|
|
|
# Get count of logs to delete
|
|
if filters:
|
|
count_result = await db.execute(
|
|
select(func.count()).select_from(ErrorLog).where(and_(*filters))
|
|
)
|
|
# Delete with filters
|
|
await db.execute(
|
|
ErrorLog.__table__.delete().where(and_(*filters))
|
|
)
|
|
else:
|
|
count_result = await db.execute(
|
|
select(func.count()).select_from(ErrorLog)
|
|
)
|
|
# Delete all logs
|
|
await db.execute(ErrorLog.__table__.delete())
|
|
|
|
count = count_result.scalar()
|
|
await db.commit()
|
|
|
|
return {
|
|
"message": f"Deleted all {count} error logs" + (" (resolved only)" if only_resolved else ""),
|
|
"deleted_count": count
|
|
} |