17f6233fd7
- job_journal: collect_recent_journal_entries_page_sync(limit, offset) - GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages - journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили - app/docs/api_routes.md: описание query и пример ответа - Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""Чтение журнала завершённых заданий из ``clusters/<имя>/journal/jobs_history.json``.
|
|
|
|
Автор: Сергей Антропов
|
|
Сайт: 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 models.schemas import ClusterJournalResponse, JournalEntryModel, 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
|
|
|
|
|
|
@router.get(
|
|
"/journal/recent",
|
|
response_model=JournalRecentResponse,
|
|
summary="Страница журнала по всем кластерам (пагинация)",
|
|
)
|
|
async def get_journal_recent(
|
|
limit: int = Query(default=30, ge=1, le=100, description="Записей на страницу"),
|
|
offset: int = Query(default=0, ge=0, description="Смещение (0 = первая страница)"),
|
|
) -> JournalRecentResponse:
|
|
"""Сборка из ``clusters/*/journal/jobs_history.json``, новые первыми; ``total`` и ``total_pages`` для UI."""
|
|
rows, total = await asyncio.to_thread(
|
|
collect_recent_journal_entries_page_sync,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
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(
|
|
"/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)
|