mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
feat: expose monitoring logs through web api
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
120
app/webapi/routes/logs.py
Normal 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)
|
||||
65
app/webapi/schemas/logs.py
Normal file
65
app/webapi/schemas/logs.py
Normal 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)
|
||||
@@ -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 и синхронизации данных бота:
|
||||
|
||||
Reference in New Issue
Block a user