268 lines
9.4 KiB
Python
268 lines
9.4 KiB
Python
"""Маршруты административного API для просмотра логов."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Security
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database.crud.ticket import TicketCRUD
|
|
from app.services.monitoring_service import monitoring_service
|
|
|
|
from ..dependencies import get_db_session, require_api_token
|
|
from ..schemas.logs import (
|
|
MonitoringLogEntry,
|
|
MonitoringLogListResponse,
|
|
MonitoringLogTypeListResponse,
|
|
SupportAuditActionsResponse,
|
|
SupportAuditLogEntry,
|
|
SupportAuditLogListResponse,
|
|
SystemLogPreviewResponse,
|
|
SystemLogFullResponse,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
SYSTEM_LOG_PREVIEW_LIMIT_DEFAULT = 4000
|
|
SYSTEM_LOG_PREVIEW_LIMIT_MAX = 20000
|
|
|
|
|
|
def _resolve_system_log_path() -> Path:
|
|
path = Path(settings.LOG_FILE)
|
|
if not path.is_absolute():
|
|
path = Path.cwd() / path
|
|
return path
|
|
|
|
|
|
async def _read_system_log(path: Path) -> tuple[str, int, Optional[float]]:
|
|
def _read() -> tuple[str, int, float]:
|
|
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
stats = path.stat()
|
|
return content, stats.st_size, stats.st_mtime
|
|
|
|
return await run_in_threadpool(_read)
|
|
|
|
|
|
def _format_timestamp(timestamp: Optional[float]) -> Optional[datetime]:
|
|
if timestamp is None:
|
|
return None
|
|
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
|
|
|
|
@router.get("/system", response_model=SystemLogPreviewResponse)
|
|
async def get_system_log_preview(
|
|
_: Any = Security(require_api_token),
|
|
preview_limit: int = Query(
|
|
SYSTEM_LOG_PREVIEW_LIMIT_DEFAULT,
|
|
ge=500,
|
|
le=SYSTEM_LOG_PREVIEW_LIMIT_MAX,
|
|
description="Количество символов предпросмотра от конца файла",
|
|
),
|
|
) -> SystemLogPreviewResponse:
|
|
"""Получить предпросмотр системного лог-файла бота."""
|
|
|
|
log_path = _resolve_system_log_path()
|
|
|
|
if not log_path.exists() or not log_path.is_file():
|
|
return SystemLogPreviewResponse(
|
|
path=str(log_path),
|
|
exists=False,
|
|
updated_at=None,
|
|
size_bytes=0,
|
|
size_chars=0,
|
|
preview="",
|
|
preview_chars=0,
|
|
preview_truncated=False,
|
|
download_url="/logs/system/download",
|
|
)
|
|
|
|
try:
|
|
content, size_bytes, mtime = await _read_system_log(log_path)
|
|
except FileNotFoundError:
|
|
logger.warning("Лог-файл %s исчез во время чтения", log_path)
|
|
return SystemLogPreviewResponse(
|
|
path=str(log_path),
|
|
exists=False,
|
|
updated_at=None,
|
|
size_bytes=0,
|
|
size_chars=0,
|
|
preview="",
|
|
preview_chars=0,
|
|
preview_truncated=False,
|
|
download_url="/logs/system/download",
|
|
)
|
|
except Exception as error: # pragma: no cover - защита от неожиданных ошибок чтения
|
|
logger.error("Ошибка чтения лог-файла %s: %s", log_path, error)
|
|
raise HTTPException(status_code=500, detail="Не удалось прочитать лог-файл") from error
|
|
|
|
preview_text = content[-preview_limit:] if preview_limit > 0 else ""
|
|
truncated = len(content) > len(preview_text)
|
|
|
|
return SystemLogPreviewResponse(
|
|
path=str(log_path),
|
|
exists=True,
|
|
updated_at=_format_timestamp(mtime),
|
|
size_bytes=size_bytes,
|
|
size_chars=len(content),
|
|
preview=preview_text,
|
|
preview_chars=len(preview_text),
|
|
preview_truncated=truncated,
|
|
download_url="/logs/system/download",
|
|
)
|
|
|
|
|
|
@router.get("/system/download")
|
|
async def download_system_log(
|
|
_: Any = Security(require_api_token),
|
|
) -> FileResponse:
|
|
"""Скачать полный лог-файл бота."""
|
|
|
|
log_path = _resolve_system_log_path()
|
|
|
|
if not log_path.exists() or not log_path.is_file():
|
|
raise HTTPException(status_code=404, detail="Лог-файл не найден")
|
|
|
|
try:
|
|
return FileResponse(
|
|
log_path,
|
|
media_type="text/plain",
|
|
filename=log_path.name,
|
|
)
|
|
except Exception as error: # pragma: no cover - защита от неожиданных ошибок отдачи файла
|
|
logger.error("Ошибка отправки лог-файла %s: %s", log_path, error)
|
|
raise HTTPException(status_code=500, detail="Не удалось отправить лог-файл") from error
|
|
|
|
|
|
@router.get("/system/full", response_model=SystemLogFullResponse)
|
|
async def get_system_log_full(
|
|
_: Any = Security(require_api_token),
|
|
) -> SystemLogFullResponse:
|
|
"""Получить полный системный лог-файл бота."""
|
|
|
|
log_path = _resolve_system_log_path()
|
|
|
|
if not log_path.exists() or not log_path.is_file():
|
|
raise HTTPException(status_code=404, detail="Лог-файл не найден")
|
|
|
|
try:
|
|
content, size_bytes, mtime = await _read_system_log(log_path)
|
|
except Exception as error: # pragma: no cover - защита от неожиданных ошибок чтения
|
|
logger.error("Ошибка чтения лог-файла %s: %s", log_path, error)
|
|
raise HTTPException(status_code=500, detail="Не удалось прочитать лог-файл") from error
|
|
|
|
return SystemLogFullResponse(
|
|
path=str(log_path),
|
|
exists=True,
|
|
updated_at=_format_timestamp(mtime),
|
|
size_bytes=size_bytes,
|
|
size_chars=len(content),
|
|
content=content,
|
|
)
|
|
|
|
|
|
@router.get("/monitoring", response_model=MonitoringLogListResponse)
|
|
async def list_monitoring_logs(
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
limit: int = Query(50, ge=1, le=200, description="Количество записей на странице"),
|
|
offset: int = Query(0, ge=0, description="Смещение от начала списка"),
|
|
event_type: Optional[str] = Query(
|
|
default=None,
|
|
max_length=100,
|
|
description="Фильтр по типу события",
|
|
),
|
|
) -> MonitoringLogListResponse:
|
|
"""Получить список логов мониторинга с пагинацией."""
|
|
|
|
per_page = limit
|
|
page = (offset // per_page) + 1
|
|
|
|
raw_logs = await monitoring_service.get_monitoring_logs(
|
|
db,
|
|
event_type=event_type,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
total = await monitoring_service.get_monitoring_logs_count(db, event_type=event_type)
|
|
|
|
return MonitoringLogListResponse(
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
items=[MonitoringLogEntry(**entry) for entry in raw_logs],
|
|
)
|
|
|
|
|
|
@router.get("/monitoring/event-types", response_model=MonitoringLogTypeListResponse)
|
|
async def list_monitoring_event_types(
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> MonitoringLogTypeListResponse:
|
|
"""Получить список доступных типов событий мониторинга."""
|
|
|
|
event_types = await monitoring_service.get_monitoring_event_types(db)
|
|
return MonitoringLogTypeListResponse(items=event_types)
|
|
|
|
|
|
@router.get("/support", response_model=SupportAuditLogListResponse)
|
|
async def list_support_audit_logs(
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
limit: int = Query(50, ge=1, le=200, description="Количество записей на странице"),
|
|
offset: int = Query(0, ge=0, description="Смещение от начала списка"),
|
|
action: Optional[str] = Query(
|
|
default=None,
|
|
max_length=50,
|
|
description="Фильтр по типу действия модератора",
|
|
),
|
|
) -> SupportAuditLogListResponse:
|
|
"""Получить список аудита действий модераторов поддержки."""
|
|
|
|
logs = await TicketCRUD.list_support_audit(
|
|
db,
|
|
limit=limit,
|
|
offset=offset,
|
|
action=action,
|
|
)
|
|
total = await TicketCRUD.count_support_audit(db, action=action)
|
|
|
|
return SupportAuditLogListResponse(
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
items=[
|
|
SupportAuditLogEntry(
|
|
id=log.id,
|
|
actor_user_id=log.actor_user_id,
|
|
actor_telegram_id=log.actor_telegram_id,
|
|
is_moderator=log.is_moderator,
|
|
action=log.action,
|
|
ticket_id=log.ticket_id,
|
|
target_user_id=log.target_user_id,
|
|
details=log.details,
|
|
created_at=log.created_at,
|
|
)
|
|
for log in logs
|
|
],
|
|
)
|
|
|
|
|
|
@router.get("/support/actions", response_model=SupportAuditActionsResponse)
|
|
async def list_support_audit_actions(
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> SupportAuditActionsResponse:
|
|
"""Получить список действий, доступных в аудите поддержки."""
|
|
|
|
actions = await TicketCRUD.list_support_audit_actions(db)
|
|
return SupportAuditActionsResponse(items=actions)
|