""" 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 }