"""Чтение журналов: задания (jobs_history), развёртывание (provision_log), Helm-аддоны (helm_addon_log). Автор: Сергей Антропов Сайт: https://devops.org.ru """ from __future__ import annotations import asyncio import logging from fastapi import APIRouter, HTTPException, Query from core.cluster_lifecycle import validate_cluster_name from core.job_journal import collect_recent_journal_entries_page_sync, read_cluster_journal_sync from core.provision_log import ( HELM_ADDON_LOG_FILENAME, PROVISION_LOG_FILENAME, collect_cluster_dir_logs_page_sync, ) from models.schemas import ( ClusterDirLogEntryModel, ClusterJournalResponse, JournalEntryModel, JournalPagedDirLogsResponse, JournalRecentResponse, ) logger = logging.getLogger("kind_k8s.api.journal") router = APIRouter(tags=["journal"]) def _journal_total_pages(total: int, limit: int) -> int: if total <= 0 or limit <= 0: return 0 return (total + limit - 1) // limit def _build_paged_dir_response( rows: list[dict], *, limit: int, offset: int, total: int, ) -> JournalPagedDirLogsResponse: total_pages = _journal_total_pages(total, limit) if total <= 0: page = 1 else: page = min((offset // limit) + 1, total_pages) page = max(1, page) entries = [ClusterDirLogEntryModel.model_validate(r) for r in rows] return JournalPagedDirLogsResponse( limit=limit, offset=offset, total=total, page=page, total_pages=total_pages, entries=entries, ) @router.get( "/journal/recent", response_model=JournalRecentResponse, summary="Страница журнала заданий (jobs_history), опционально один кластер", ) async def get_journal_recent( limit: int = Query(default=30, ge=1, le=100, description="Записей на страницу"), offset: int = Query(default=0, ge=0, description="Смещение (0 = первая страница)"), cluster: str | None = Query( default=None, description="Имя кластера: только записи из clusters/<имя>/journal/jobs_history.json", ), ) -> JournalRecentResponse: """Сборка из ``journal/jobs_history.json``; при ``cluster`` — только выбранный каталог.""" c = cluster.strip() if cluster else None if c and not validate_cluster_name(c): raise HTTPException(status_code=400, detail="Некорректное имя кластера") rows, total = await asyncio.to_thread( collect_recent_journal_entries_page_sync, limit=limit, offset=offset, cluster_name=c, ) entries = [JournalEntryModel.model_validate(r) for r in rows] total_pages = _journal_total_pages(total, limit) if total <= 0: page = 1 else: page = min((offset // limit) + 1, total_pages) page = max(1, page) return JournalRecentResponse( limit=limit, offset=offset, total=total, page=page, total_pages=total_pages, entries=entries, ) @router.get( "/journal/provision", response_model=JournalPagedDirLogsResponse, summary="Журналы развёртывания по кластерам (provision_log.json)", ) async def get_journal_provision_logs( limit: int = Query(default=30, ge=1, le=100), offset: int = Query(default=0, ge=0), ) -> JournalPagedDirLogsResponse: """По одному последнему ``provision_log.json`` на кластер, новые первыми.""" rows, total = await asyncio.to_thread( collect_cluster_dir_logs_page_sync, filename=PROVISION_LOG_FILENAME, limit=limit, offset=offset, ) return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total) @router.get( "/journal/helm-addons", response_model=JournalPagedDirLogsResponse, summary="Журналы установки Helm-аддонов (helm_addon_log.json)", ) async def get_journal_helm_addon_logs( limit: int = Query(default=30, ge=1, le=100), offset: int = Query(default=0, ge=0), ) -> JournalPagedDirLogsResponse: """Все записи из ``helm_addon_log.json`` по кластерам (история операций), новые первыми.""" rows, total = await asyncio.to_thread( collect_cluster_dir_logs_page_sync, filename=HELM_ADDON_LOG_FILENAME, limit=limit, offset=offset, ) return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total) @router.get( "/clusters/{name}/journal", response_model=ClusterJournalResponse, summary="Журнал заданий кластера (JSON с диска)", ) async def get_cluster_journal(name: str) -> ClusterJournalResponse: n = name.strip() if not validate_cluster_name(n): raise HTTPException(status_code=400, detail="Некорректное имя кластера") raw = await asyncio.to_thread(read_cluster_journal_sync, n) if raw is None: return ClusterJournalResponse(cluster_name=n, file_version=None, entries=[]) fv = raw.get("file_version") if isinstance(raw.get("file_version"), int) else None ent = raw.get("entries") if not isinstance(ent, list): return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=[]) entries: list[JournalEntryModel] = [] for item in ent: if isinstance(item, dict): try: entries.append(JournalEntryModel.model_validate(item)) except Exception: logger.debug("Пропуск некорректной записи журнала кластера «%s»", n) return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=entries)