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