Files
orion/app/api/v1/admin/logs.py
Samir Boulahtit cad862f469 refactor(api): introduce UserContext schema for API dependency injection
Replace direct User database model imports in API endpoints with UserContext
schema, following the architecture principle that API routes should not import
database models directly.

Changes:
- Create UserContext schema in models/schema/auth.py with from_user() factory
- Update app/api/deps.py to return UserContext from all auth dependencies
- Add _get_user_model() helper for functions needing User model access
- Update 58 API endpoint files to use UserContext instead of User
- Add noqa comments for 4 legitimate edge cases (enums, internal helpers)

Architecture validation: 0 errors (down from 61), 11 warnings remain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:47:33 +01:00

343 lines
10 KiB
Python

# app/api/v1/admin/logs.py
"""
Log management endpoints for admin.
Provides endpoints for:
- Viewing database logs with filters
- Reading file logs
- Log statistics
- Log settings management
- Log cleanup operations
"""
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.core.logging import reload_log_level
from app.exceptions import ConfirmationRequiredException, ResourceNotFoundException
from app.services.admin_audit_service import admin_audit_service
from app.services.admin_settings_service import admin_settings_service
from app.services.log_service import log_service
from models.schema.auth import UserContext
from models.schema.admin import (
ApplicationLogFilters,
ApplicationLogListResponse,
FileLogResponse,
LogCleanupResponse,
LogDeleteResponse,
LogFileListResponse,
LogSettingsResponse,
LogSettingsUpdate,
LogSettingsUpdateResponse,
LogStatistics,
)
router = APIRouter(prefix="/logs")
logger = logging.getLogger(__name__)
# ============================================================================
# DATABASE LOGS ENDPOINTS
# ============================================================================
@router.get("/database", response_model=ApplicationLogListResponse)
def get_database_logs(
level: str | None = Query(None, description="Filter by log level"),
logger_name: str | None = Query(None, description="Filter by logger name"),
module: str | None = Query(None, description="Filter by module"),
user_id: int | None = Query(None, description="Filter by user ID"),
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
search: str | None = Query(None, description="Search in message"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get logs from database with filtering.
Supports filtering by level, logger, module, user, vendor, and date range.
Returns paginated results.
"""
filters = ApplicationLogFilters(
level=level,
logger_name=logger_name,
module=module,
user_id=user_id,
vendor_id=vendor_id,
search=search,
skip=skip,
limit=limit,
)
return log_service.get_database_logs(db, filters)
@router.get("/statistics", response_model=LogStatistics)
def get_log_statistics(
days: int = Query(7, ge=1, le=90, description="Number of days to analyze"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get log statistics for the last N days.
Returns counts by level, module, and recent critical errors.
"""
return log_service.get_log_statistics(db, days)
@router.delete("/database/cleanup", response_model=LogCleanupResponse)
def cleanup_old_logs(
retention_days: int = Query(30, ge=1, le=365),
confirm: bool = Query(False, description="Must be true to confirm cleanup"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Delete logs older than retention period.
Requires confirmation parameter.
"""
if not confirm:
raise ConfirmationRequiredException(operation="cleanup_logs")
deleted_count = log_service.cleanup_old_logs(db, retention_days)
# Log action
admin_audit_service.log_action(
db=db,
admin_user_id=current_admin.id,
action="cleanup_logs",
target_type="application_logs",
target_id="bulk",
details={"retention_days": retention_days, "deleted_count": deleted_count},
)
return LogCleanupResponse(
message=f"Deleted {deleted_count} log entries older than {retention_days} days",
deleted_count=deleted_count,
)
@router.delete("/database/{log_id}", response_model=LogDeleteResponse)
def delete_log(
log_id: int,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Delete a specific log entry."""
message = log_service.delete_log(db, log_id)
# Log action
admin_audit_service.log_action(
db=db,
admin_user_id=current_admin.id,
action="delete_log",
target_type="application_log",
target_id=str(log_id),
details={},
)
return LogDeleteResponse(message=message)
# ============================================================================
# FILE LOGS ENDPOINTS
# ============================================================================
@router.get("/files", response_model=LogFileListResponse)
def list_log_files(
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
List all available log files.
Returns list of log files with size and modification date.
"""
return LogFileListResponse(files=log_service.list_log_files())
@router.get("/files/{filename}", response_model=FileLogResponse)
def get_file_log(
filename: str,
lines: int = Query(500, ge=1, le=10000, description="Number of lines to read"),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Read log file content.
Returns the last N lines from the specified log file.
"""
return log_service.get_file_logs(filename, lines)
@router.get("/files/{filename}/download")
def download_log_file(
filename: str,
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Download log file.
Returns the entire log file for download.
"""
from pathlib import Path
from fastapi.responses import FileResponse
from app.core.config import settings
# Determine log file path
log_file_path = settings.log_file
if log_file_path:
log_file = Path(log_file_path).parent / filename
else:
log_file = Path("logs") / filename
if not log_file.exists():
raise ResourceNotFoundException(resource_type="LogFile", identifier=filename)
# Log action
from app.core.database import get_db
db_gen = get_db()
db = next(db_gen)
try:
admin_audit_service.log_action(
db=db,
admin_user_id=current_admin.id,
action="download_log_file",
target_type="log_file",
target_id=filename,
details={"size_bytes": log_file.stat().st_size},
)
finally:
db.close()
return FileResponse(
log_file,
media_type="text/plain",
filename=filename,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
# ============================================================================
# LOG SETTINGS ENDPOINTS
# ============================================================================
@router.get("/settings", response_model=LogSettingsResponse)
def get_log_settings(
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Get current log configuration settings."""
log_level = admin_settings_service.get_setting_value(db, "log_level", "INFO")
max_size_mb = admin_settings_service.get_setting_value(
db, "log_file_max_size_mb", 10
)
backup_count = admin_settings_service.get_setting_value(
db, "log_file_backup_count", 5
)
retention_days = admin_settings_service.get_setting_value(
db, "db_log_retention_days", 30
)
file_enabled = admin_settings_service.get_setting_value(
db, "file_logging_enabled", "true"
)
db_enabled = admin_settings_service.get_setting_value(
db, "db_logging_enabled", "true"
)
return LogSettingsResponse(
log_level=str(log_level),
log_file_max_size_mb=int(max_size_mb),
log_file_backup_count=int(backup_count),
db_log_retention_days=int(retention_days),
file_logging_enabled=str(file_enabled).lower() == "true",
db_logging_enabled=str(db_enabled).lower() == "true",
)
@router.put("/settings", response_model=LogSettingsUpdateResponse)
def update_log_settings(
settings_update: LogSettingsUpdate,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Update log configuration settings.
Changes are applied immediately without restart (for log level).
File rotation settings require restart.
"""
from models.schema.admin import AdminSettingUpdate
updated = []
# Update log level
if settings_update.log_level:
admin_settings_service.update_setting(
db,
"log_level",
AdminSettingUpdate(value=settings_update.log_level),
current_admin.id,
)
updated.append("log_level")
# Reload log level immediately
reload_log_level()
# Update file rotation settings
if settings_update.log_file_max_size_mb:
admin_settings_service.update_setting(
db,
"log_file_max_size_mb",
AdminSettingUpdate(value=str(settings_update.log_file_max_size_mb)),
current_admin.id,
)
updated.append("log_file_max_size_mb")
if settings_update.log_file_backup_count is not None:
admin_settings_service.update_setting(
db,
"log_file_backup_count",
AdminSettingUpdate(value=str(settings_update.log_file_backup_count)),
current_admin.id,
)
updated.append("log_file_backup_count")
# Update retention
if settings_update.db_log_retention_days:
admin_settings_service.update_setting(
db,
"db_log_retention_days",
AdminSettingUpdate(value=str(settings_update.db_log_retention_days)),
current_admin.id,
)
updated.append("db_log_retention_days")
# Log action
admin_audit_service.log_action(
db=db,
admin_user_id=current_admin.id,
action="update_log_settings",
target_type="settings",
target_id="logging",
details={"updated_fields": updated},
)
return LogSettingsUpdateResponse(
message="Log settings updated successfully",
updated_fields=updated,
note="Log level changes are applied immediately. File rotation settings require restart.",
)