mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #1491 from Fr1ngg/wugnye-bedolaga/expand-bot-api-for-system-logs
Expose system log through admin API
This commit is contained in:
@@ -72,7 +72,9 @@ OPENAPI_TAGS = [
|
||||
},
|
||||
{
|
||||
"name": "logs",
|
||||
"description": "Журналы мониторинга бота и действий модераторов поддержки.",
|
||||
"description": (
|
||||
"Журналы мониторинга бота, действий модераторов поддержки и системный лог-файл."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "auth",
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"""Маршруты административного 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, Query, Security
|
||||
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
|
||||
|
||||
@@ -17,10 +23,123 @@ from ..schemas.logs import (
|
||||
SupportAuditActionsResponse,
|
||||
SupportAuditLogEntry,
|
||||
SupportAuditLogListResponse,
|
||||
SystemLogPreviewResponse,
|
||||
)
|
||||
|
||||
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("/monitoring", response_model=MonitoringLogListResponse)
|
||||
async def list_monitoring_logs(
|
||||
|
||||
@@ -63,3 +63,28 @@ class SupportAuditActionsResponse(BaseModel):
|
||||
"""Ответ со списком доступных действий аудита поддержки."""
|
||||
|
||||
items: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SystemLogPreviewResponse(BaseModel):
|
||||
"""Ответ с превью системного лог-файла бота."""
|
||||
|
||||
path: str = Field(..., description="Абсолютный путь до лог-файла")
|
||||
exists: bool = Field(..., description="Флаг наличия лог-файла")
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
description="Дата и время последнего изменения лог-файла",
|
||||
)
|
||||
size_bytes: int = Field(..., ge=0, description="Размер лог-файла в байтах")
|
||||
size_chars: int = Field(..., ge=0, description="Количество символов в лог-файле")
|
||||
preview: str = Field(
|
||||
default="",
|
||||
description="Фрагмент содержимого лог-файла, возвращаемый для предпросмотра",
|
||||
)
|
||||
preview_chars: int = Field(..., ge=0, description="Размер предпросмотра в символах")
|
||||
preview_truncated: bool = Field(
|
||||
..., description="Флаг усечения предпросмотра относительно полного файла"
|
||||
)
|
||||
download_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Относительный путь до endpoint для скачивания лог-файла",
|
||||
)
|
||||
|
||||
@@ -139,6 +139,8 @@ curl -X POST "http://127.0.0.1:8080/tokens" \
|
||||
| `GET` | `/logs/monitoring/event-types` | Справочник доступных типов событий мониторинга.
|
||||
| `GET` | `/logs/support` | Журнал действий модераторов поддержки (блокировки, закрытия тикетов).
|
||||
| `GET` | `/logs/support/actions` | Справочник возможных действий в аудите поддержки.
|
||||
| `GET` | `/logs/system` | Предпросмотр системного лог-файла бота с метаданными.
|
||||
| `GET` | `/logs/system/download` | Скачивание полного лог-файла бота (`text/plain`).
|
||||
|
||||
> Раздел **promo-offers** в Swagger объединяет работу с персональными предложениями: выдачу скидок/бонусов пользователям, настройку
|
||||
> текстов шаблонов и просмотр журнала операций (активации, автосписания, отключения просроченных акций).
|
||||
@@ -151,6 +153,8 @@ curl -X POST "http://127.0.0.1:8080/tokens" \
|
||||
- Получать справочник доступных типов событий (`GET /logs/monitoring/event-types`). Это удобно для построения фильтров во внешней админке.
|
||||
- Отслеживать действия модераторов поддержки (`GET /logs/support`) с пагинацией и возможностью фильтровать по конкретному действию (`action`).
|
||||
- Запрашивать список возможных действий для UI (`GET /logs/support/actions`).
|
||||
- Просматривать системный лог-файл бота (`GET /logs/system`). Endpoint возвращает метаданные (путь, время изменения, размер в байтах/символах) и фрагмент конца файла, размер которого можно регулировать параметром `preview_limit` (от 500 до 20 000 символов).
|
||||
- Скачивать полный системный лог в текстовом формате (`GET /logs/system/download`).
|
||||
|
||||
Все эндпоинты защищены токеном API и возвращают структуру с общим количеством записей, текущим `limit`/`offset` и массивом объектов. Это упрощает реализацию таблиц и постраничной навигации во внешних административных интерфейсах.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user