feat: expose monitoring logs through web api

This commit is contained in:
Egor
2025-10-24 09:06:08 +03:00
parent 2abd1fd9df
commit 5f316f85b3
7 changed files with 260 additions and 11 deletions

View File

@@ -333,18 +333,42 @@ class TicketCRUD:
*,
limit: int = 50,
offset: int = 0,
action: Optional[str] = None,
) -> List[SupportAuditLog]:
from sqlalchemy import select, desc
result = await db.execute(
select(SupportAuditLog).order_by(desc(SupportAuditLog.created_at)).offset(offset).limit(limit)
)
query = select(SupportAuditLog).order_by(desc(SupportAuditLog.created_at))
if action:
query = query.where(SupportAuditLog.action == action)
result = await db.execute(query.offset(offset).limit(limit))
return result.scalars().all()
@staticmethod
async def count_support_audit(db: AsyncSession) -> int:
async def count_support_audit(db: AsyncSession, action: Optional[str] = None) -> int:
from sqlalchemy import select, func
result = await db.execute(select(func.count()).select_from(SupportAuditLog))
query = select(func.count()).select_from(SupportAuditLog)
if action:
query = query.where(SupportAuditLog.action == action)
result = await db.execute(query)
return int(result.scalar() or 0)
@staticmethod
async def list_support_audit_actions(db: AsyncSession) -> List[str]:
from sqlalchemy import select
result = await db.execute(
select(SupportAuditLog.action)
.where(SupportAuditLog.action.isnot(None))
.distinct()
.order_by(SupportAuditLog.action)
)
return [row[0] for row in result.fetchall()]
@staticmethod
async def get_open_tickets_count(db: AsyncSession) -> int:

View File

@@ -1759,26 +1759,43 @@ class MonitoringService:
return []
async def get_monitoring_logs_count(
self,
self,
db: AsyncSession,
event_type: Optional[str] = None
) -> int:
try:
from sqlalchemy import select, func
query = select(func.count(MonitoringLog.id))
if event_type:
query = query.where(MonitoringLog.event_type == event_type)
result = await db.execute(query)
count = result.scalar()
return count or 0
except Exception as e:
logger.error(f"Ошибка получения количества логов: {e}")
return 0
async def get_monitoring_event_types(self, db: AsyncSession) -> List[str]:
try:
from sqlalchemy import select
result = await db.execute(
select(MonitoringLog.event_type)
.where(MonitoringLog.event_type.isnot(None))
.distinct()
.order_by(MonitoringLog.event_type)
)
return [row[0] for row in result.fetchall() if row[0]]
except Exception as e:
logger.error(f"Ошибка получения списка типов событий мониторинга: {e}")
return []
async def cleanup_old_logs(self, db: AsyncSession, days: int = 30) -> int:
try:

View File

@@ -25,6 +25,7 @@ from .routes import (
tokens,
transactions,
users,
logs,
)
@@ -69,6 +70,10 @@ OPENAPI_TAGS = [
"name": "promo-offers",
"description": "Управление промо-предложениями, шаблонами и журналом событий.",
},
{
"name": "logs",
"description": "Журналы мониторинга бота и действий модераторов поддержки.",
},
{
"name": "auth",
"description": "Управление токенами доступа к административному API.",
@@ -138,5 +143,6 @@ def create_web_api_app() -> FastAPI:
app.include_router(tokens.router, prefix="/tokens", tags=["auth"])
app.include_router(remnawave.router, prefix="/remnawave", tags=["remnawave"])
app.include_router(miniapp.router, prefix="/miniapp", tags=["miniapp"])
app.include_router(logs.router, prefix="/logs", tags=["logs"])
return app

View File

@@ -13,6 +13,7 @@ from . import (
tokens,
transactions,
users,
logs,
)
__all__ = [
@@ -30,4 +31,5 @@ __all__ = [
"tokens",
"transactions",
"users",
"logs",
]

120
app/webapi/routes/logs.py Normal file
View File

@@ -0,0 +1,120 @@
"""Маршруты административного API для просмотра логов."""
from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, Depends, Query, Security
from sqlalchemy.ext.asyncio import AsyncSession
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,
)
router = APIRouter()
@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)

View File

@@ -0,0 +1,65 @@
"""Pydantic-схемы для работы с логами административного API."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
class MonitoringLogEntry(BaseModel):
"""Запись лога мониторинга."""
id: int
event_type: str = Field(..., description="Тип события мониторинга")
message: str = Field(..., description="Краткое описание события")
data: Optional[Dict[str, Any]] = Field(
default=None,
description="Дополнительные данные события",
)
is_success: bool = Field(..., description="Флаг успешности выполнения операции")
created_at: datetime = Field(..., description="Дата и время создания записи")
class MonitoringLogListResponse(BaseModel):
"""Ответ со списком логов мониторинга."""
total: int = Field(..., ge=0)
limit: int = Field(..., ge=1)
offset: int = Field(..., ge=0)
items: List[MonitoringLogEntry]
class MonitoringLogTypeListResponse(BaseModel):
"""Ответ со списком доступных типов событий мониторинга."""
items: List[str] = Field(default_factory=list)
class SupportAuditLogEntry(BaseModel):
"""Запись аудита модераторов поддержки."""
id: int
actor_user_id: Optional[int]
actor_telegram_id: int
is_moderator: bool
action: str
ticket_id: Optional[int]
target_user_id: Optional[int]
details: Optional[Dict[str, Any]] = None
created_at: datetime
class SupportAuditLogListResponse(BaseModel):
"""Ответ со списком аудита поддержки."""
total: int = Field(..., ge=0)
limit: int = Field(..., ge=1)
offset: int = Field(..., ge=0)
items: List[SupportAuditLogEntry]
class SupportAuditActionsResponse(BaseModel):
"""Ответ со списком доступных действий аудита поддержки."""
items: List[str] = Field(default_factory=list)

View File

@@ -135,10 +135,25 @@ curl -X POST "http://127.0.0.1:8080/tokens" \
| `PATCH` | `/promo-offers/templates/{id}` | Обновить текст, кнопки и параметры шаблона.
| `GET` | `/promo-offers/logs` | Журнал операций с промо-предложениями (активации, списания, выключения).
| `GET` | `/tokens` | Управление токенами доступа.
| `GET` | `/logs/monitoring` | Логи мониторинга бота с пагинацией и фильтрами по типу события.
| `GET` | `/logs/monitoring/event-types` | Справочник доступных типов событий мониторинга.
| `GET` | `/logs/support` | Журнал действий модераторов поддержки (блокировки, закрытия тикетов).
| `GET` | `/logs/support/actions` | Справочник возможных действий в аудите поддержки.
> Раздел **promo-offers** в Swagger объединяет работу с персональными предложениями: выдачу скидок/бонусов пользователям, настройку
> текстов шаблонов и просмотр журнала операций (активации, автосписания, отключения просроченных акций).
### Логи бота
В административном API появился раздел **logs**. Он позволяет:
- Просматривать общие логи мониторинга (`GET /logs/monitoring`) с поддержкой пагинации (`limit`, `offset`) и фильтра по типу события (`event_type`).
- Получать справочник доступных типов событий (`GET /logs/monitoring/event-types`). Это удобно для построения фильтров во внешней админке.
- Отслеживать действия модераторов поддержки (`GET /logs/support`) с пагинацией и возможностью фильтровать по конкретному действию (`action`).
- Запрашивать список возможных действий для UI (`GET /logs/support/actions`).
Все эндпоинты защищены токеном API и возвращают структуру с общим количеством записей, текущим `limit`/`offset` и массивом объектов. Это упрощает реализацию таблиц и постраничной навигации во внешних административных интерфейсах.
### RemnaWave интеграция
После включения веб-API в Swagger (`WEB_API_DOCS_ENABLED=true`) появится раздел **remnawave**. Он объединяет эндпоинты для управления панелью RemnaWave и синхронизации данных бота: