"""Раздел «Логи»: события и дневные счётчики сообщений из 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, }, )