"""Раздел «Логи»: события из ClickHouse с фильтрами и пагинацией.""" 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: str) -> str: """Отформатировать JSON-payload для просмотра; битый JSON — как есть.""" try: return json.dumps(json.loads(payload), ensure_ascii=False, indent=2) except (TypeError, ValueError): return 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(str(row.get("payload") or "")), } @router.get("", response_class=HTMLResponse) async def logs_page( request: Request, event_type: str = "", user: str = "", date_from: str = "", date_to: str = "", page: int = 1, ) -> HTMLResponse: """Показать события с фильтрами по типу, пользователю и датам.""" settings = request.app.state.settings 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]] = [] total = 0 error: str | None = None try: 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("Не удалось прочитать события из ClickHouse") error = "ClickHouse недоступен — логи временно не читаются." pages = max((total + PAGE_SIZE - 1) // PAGE_SIZE, 1) query_base = urlencode( { key: value for key, value in { "event_type": event_type, "user": user, "date_from": date_from, "date_to": date_to, }.items() if value } ) return request.app.state.templates.TemplateResponse( request=request, name="logs/list.html", context={ "version": __version__, "events": [_decorate(row, settings) for row in events], "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, }, )