feat: страница логов /ui/logs с SSE real-time потоком
Some checks failed
CI / test (push) Successful in 39s
Deploy / deploy (push) Failing after 15s

- log_buffer: RingBufferHandler, кольцевой буфер 600 записей, fan-out SSE
- ui_logs: GET /ui/logs (HTML), GET /ui/logs/stream (EventSource)
- main: install_log_handler при старте, подключён router логов
- nav_rail: ссылка Логи, root_html: кнопка-ссылка Логи
- Исправлено: NaN/Inf/NUL в теле вебхука → 500 от PostgreSQL jsonb
- Тесты: test_log_buffer, test_json_sanitize; 51 passed

Made-with: Cursor
This commit is contained in:
Alexandr
2026-04-03 15:56:58 +03:00
parent 18ba48e8d0
commit 80645713a0
12 changed files with 465 additions and 12 deletions

View File

@ -0,0 +1,26 @@
import json
import math
from onguard24.ingress.json_sanitize import sanitize_for_jsonb
def test_sanitize_nan_inf_to_none() -> None:
raw = json.loads('{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}')
out = sanitize_for_jsonb(raw)
assert math.isnan(raw["a"])
assert out["a"] is None
assert out["b"] is None
assert out["c"] is None
assert out["d"] == 1.5
def test_sanitize_strips_nul_in_strings() -> None:
assert sanitize_for_jsonb({"x": "a\x00b"}) == {"x": "ab"}
def test_dumps_after_sanitize_is_valid_json() -> None:
raw = json.loads('{"v": NaN}')
clean = sanitize_for_jsonb(raw)
s = json.dumps(clean, allow_nan=False)
assert "NaN" not in s
assert json.loads(s)["v"] is None

45
tests/test_log_buffer.py Normal file
View File

@ -0,0 +1,45 @@
"""Кольцевой буфер логов и SSE-страница."""
import logging
import pytest
from fastapi.testclient import TestClient
from onguard24 import log_buffer
def test_ring_buffer_captures_log_records() -> None:
log_buffer._ring.clear()
handler = log_buffer.RingBufferHandler()
handler.setFormatter(logging.Formatter("%(name)s %(message)s"))
logger = logging.getLogger("test.ring")
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
try:
logger.info("hello ring")
history = log_buffer.get_history()
assert any("hello ring" in e["msg"] for e in history)
finally:
logger.removeHandler(handler)
log_buffer._ring.clear()
def test_logs_page_returns_html(client: TestClient) -> None:
r = client.get("/ui/logs")
assert r.status_code == 200
assert "text/html" in r.headers.get("content-type", "")
assert "Логи" in r.text
assert "log-wrap" in r.text
assert "EventSource" in r.text or "event-stream" in r.text or "ui/logs/stream" in r.text
def test_logs_page_in_nav_rail(client: TestClient) -> None:
r = client.get("/ui/logs")
assert r.status_code == 200
assert "/ui/logs" in r.text
def test_root_has_logs_link(client: TestClient) -> None:
r = client.get("/")
assert r.status_code == 200
assert "/ui/logs" in r.text