- Remove |safe from |tojson in HTML attributes (x-data) - quotes must become " for browsers to parse correctly - Update LANG-002 and LANG-003 architecture rules to document correct |tojson usage patterns: - HTML attributes: |tojson (no |safe) - Script blocks: |tojson|safe - Fix validator to warn when |tojson|safe is used in x-data (breaks HTML attribute parsing) - Improve code quality across services, APIs, and tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
343 lines
10 KiB
Python
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.database.user import User
|
|
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: User = 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: User = 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: User = 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: User = 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: User = 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: User = 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: User = 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: User = 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: User = 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.",
|
|
)
|