DispelEvilBot/admin/routes/logs.py

140 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Раздел «Логи»: события и дневные счётчики сообщений из Postgres."""
from __future__ import annotations
import json
import logging
from datetime import UTC, datetime, timedelta
from typing import Any
from urllib.parse import urlencode
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from common import __version__
from common.analytics import ALL_EVENT_TYPES
from common.analytics import read as analytics_read
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/logs", tags=["logs"])
#: Сколько строк показывать на странице.
PAGE_SIZE = 50
def _pretty_payload(payload: Any) -> str:
"""Отформатировать JSON-payload для просмотра; битый JSON — как есть."""
if isinstance(payload, dict):
return json.dumps(payload, ensure_ascii=False, indent=2)
try:
return json.dumps(json.loads(str(payload or "")), ensure_ascii=False, indent=2)
except (TypeError, ValueError):
return str(payload or "")
def _decorate(row: dict[str, Any], settings: Any) -> dict[str, Any]:
"""Добавить локальное время и читаемый payload."""
moment = row.get("event_time")
if isinstance(moment, datetime):
if moment.tzinfo is None:
moment = moment.replace(tzinfo=UTC)
time_local = moment.astimezone(settings.tzinfo).strftime("%d.%m.%Y %H:%M:%S")
else:
time_local = "—"
return {
**row,
"time_local": time_local,
"payload_pretty": _pretty_payload(row.get("payload")),
}
def _decorate_message(row: dict[str, Any]) -> dict[str, Any]:
day = row.get("day")
return {
**row,
"day_local": day.strftime("%d.%m.%Y") if hasattr(day, "strftime") else str(day or "—"),
}
@router.get("", response_class=HTMLResponse)
async def logs_page(
request: Request,
view: str = "events",
event_type: str = "",
user: str = "",
date_from: str = "",
date_to: str = "",
page: int = 1,
) -> HTMLResponse:
"""Показать события или дневные счётчики сообщений с фильтрами."""
settings = request.app.state.settings
view = view if view in {"events", "messages"} else "events"
today = datetime.now(settings.tzinfo).date()
date_from = date_from or (today - timedelta(days=6)).isoformat()
date_to = date_to or today.isoformat()
page = max(page, 1)
events: list[dict[str, Any]] = []
messages: list[dict[str, Any]] = []
total = 0
error: str | None = None
try:
if view == "messages":
messages, total = await analytics_read.fetch_messages(
settings,
user=user,
date_from=date_from,
date_to=date_to,
limit=PAGE_SIZE,
offset=(page - 1) * PAGE_SIZE,
)
else:
events, total = await analytics_read.fetch_events(
settings,
event_type=event_type,
user=user,
date_from=date_from,
date_to=date_to,
limit=PAGE_SIZE,
offset=(page - 1) * PAGE_SIZE,
)
except ValueError:
error = "Даты должны быть в формате ГГГГ-ММ-ДД."
except Exception:
logger.exception("Не удалось прочитать аналитику из Postgres")
error = "Postgres недоступен — логи временно не читаются."
pages = max((total + PAGE_SIZE - 1) // PAGE_SIZE, 1)
query_params = {
"view": view if view != "events" else "",
"event_type": event_type if view == "events" else "",
"user": user,
"date_from": date_from,
"date_to": date_to,
}
query_base = urlencode({key: value for key, value in query_params.items() if value})
return request.app.state.templates.TemplateResponse(
request=request,
name="logs/list.html",
context={
"version": __version__,
"view": view,
"events": [_decorate(row, settings) for row in events],
"messages": [_decorate_message(row) for row in messages],
"event_types": sorted(ALL_EVENT_TYPES),
"event_type": event_type,
"user": user,
"date_from": date_from,
"date_to": date_to,
"page": page,
"pages": pages,
"total": total,
"error": error,
"query_base": query_base,
},
)