feat: логирование вебхука до БД + файловый лог с ротацией
All checks were successful
CI / test (push) Successful in 47s
All checks were successful
CI / test (push) Successful in 47s
- Каждый входящий POST /ingress/grafana: INFO-строка (status, кол-во алертов, первые лейблы) и DEBUG-блок с полным JSON телом (до 8КБ) — видно даже если БД упала с 500 - LOG_FILE в .env / env: RotatingFileHandler 10MB×5 файлов - LOG_LEVEL=debug теперь показывает полные тела вебхуков - basicConfig уровень DEBUG (uvicorn.access / asyncio приглушены) Made-with: Cursor
This commit is contained in:
@ -3,6 +3,11 @@
|
||||
HTTP_ADDR=0.0.0.0:8080
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Запись логов в файл с авто-ротацией (10 МБ × 5 файлов = ~50 МБ).
|
||||
# Пусто = не писать в файл (логи только в stdout и страницу /ui/logs).
|
||||
# Пример для docker-compose (volume /logs): LOG_FILE=/logs/onguard24.log
|
||||
# LOG_FILE=
|
||||
|
||||
# Опционально: общий секрет для вебхуков (если у источника в JSON не задан свой webhook_secret)
|
||||
# GRAFANA_WEBHOOK_SECRET=
|
||||
|
||||
|
||||
@ -34,6 +34,9 @@ class Settings(BaseSettings):
|
||||
forgejo_url: str = Field(default="", validation_alias="FORGEJO_URL")
|
||||
forgejo_token: str = Field(default="", validation_alias="FORGEJO_TOKEN")
|
||||
log_level: str = Field(default="info", validation_alias="LOG_LEVEL")
|
||||
# Путь к лог-файлу. Пусто = не писать в файл. Пример: /var/log/onguard24/app.log
|
||||
# Файл ротируется: 10 МБ × 5 штук (~50 МБ суммарно).
|
||||
log_file: str = Field(default="", validation_alias="LOG_FILE")
|
||||
# Устаревшее: автосоздание инцидента на каждый вебхук (без учёта irm_alerts). По умолчанию выкл.
|
||||
auto_incident_from_alert: bool = Field(
|
||||
default=False,
|
||||
|
||||
@ -82,6 +82,42 @@ def service_hint_from_grafana_body(body: dict, header_service: str | None) -> st
|
||||
return None
|
||||
|
||||
|
||||
def _log_incoming_webhook(body: object, raw: bytes, path_slug: str | None) -> None:
|
||||
"""Логирует каждый входящий вебхук: краткое резюме INFO + полное тело DEBUG."""
|
||||
slug_tag = f"[/{path_slug}]" if path_slug else "[/]"
|
||||
raw_len = len(raw)
|
||||
|
||||
if not isinstance(body, dict):
|
||||
logger.info("grafana webhook %s raw=%dB (non-dict body)", slug_tag, raw_len)
|
||||
logger.debug("grafana webhook %s raw body: %s", slug_tag, raw[:4000].decode(errors="replace"))
|
||||
return
|
||||
|
||||
alerts = body.get("alerts") or []
|
||||
n_alerts = len(alerts) if isinstance(alerts, list) else 0
|
||||
status = body.get("status", "?")
|
||||
title = str(body.get("title") or body.get("ruleName") or "")[:120]
|
||||
first_labels: dict = {}
|
||||
if isinstance(alerts, list) and alerts and isinstance(alerts[0], dict):
|
||||
first_labels = alerts[0].get("labels") or {}
|
||||
|
||||
logger.info(
|
||||
"grafana webhook %s status=%s alerts=%d title=%r labels=%s raw=%dB",
|
||||
slug_tag,
|
||||
status,
|
||||
n_alerts,
|
||||
title,
|
||||
json.dumps(first_labels, ensure_ascii=False)[:300],
|
||||
raw_len,
|
||||
)
|
||||
try:
|
||||
pretty = json.dumps(body, ensure_ascii=False, indent=2)
|
||||
if len(pretty) > 8000:
|
||||
pretty = pretty[:8000] + "\n…(обрезано)"
|
||||
except Exception:
|
||||
pretty = raw[:8000].decode(errors="replace")
|
||||
logger.debug("grafana webhook %s full body:\n%s", slug_tag, pretty)
|
||||
|
||||
|
||||
async def _grafana_webhook_impl(
|
||||
request: Request,
|
||||
pool,
|
||||
@ -98,6 +134,10 @@ async def _grafana_webhook_impl(
|
||||
body = json.loads(raw.decode() or "{}")
|
||||
except json.JSONDecodeError:
|
||||
body = {}
|
||||
|
||||
# Логируем входящий вебхук ДО любой обработки — чтобы видеть при любой ошибке
|
||||
_log_incoming_webhook(body, raw, path_slug)
|
||||
|
||||
if not isinstance(body, dict):
|
||||
body = {}
|
||||
else:
|
||||
|
||||
@ -78,14 +78,42 @@ class RingBufferHandler(logging.Handler):
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def install_log_handler(loop: asyncio.AbstractEventLoop) -> None:
|
||||
def install_log_handler(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
log_file: str = "",
|
||||
) -> None:
|
||||
"""Вызывается один раз при старте: регистрирует handler на корневом логгере."""
|
||||
set_event_loop(loop)
|
||||
handler = RingBufferHandler()
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(name)s %(message)s")
|
||||
fmt = logging.Formatter(
|
||||
"%(asctime)s %(levelname)-8s %(name)s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
root = logging.getLogger()
|
||||
|
||||
# Кольцевой буфер (SSE-страница логов)
|
||||
if not any(isinstance(h, RingBufferHandler) for h in root.handlers):
|
||||
root.addHandler(handler)
|
||||
ring_h = RingBufferHandler()
|
||||
ring_h.setFormatter(logging.Formatter("%(name)s %(message)s"))
|
||||
ring_h.setLevel(logging.DEBUG)
|
||||
root.addHandler(ring_h)
|
||||
|
||||
# Файл с ротацией (если задан LOG_FILE)
|
||||
if log_file.strip():
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
log_path = log_file.strip()
|
||||
os.makedirs(os.path.dirname(log_path) if os.path.dirname(log_path) else ".", exist_ok=True)
|
||||
if not any(isinstance(h, RotatingFileHandler) for h in root.handlers):
|
||||
file_h = RotatingFileHandler(
|
||||
log_path,
|
||||
maxBytes=10 * 1024 * 1024, # 10 МБ
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_h.setFormatter(fmt)
|
||||
file_h.setLevel(logging.DEBUG)
|
||||
root.addHandler(file_h)
|
||||
logging.getLogger("onguard24").info(
|
||||
"file logging enabled: %s (rotate 10MB×5)", log_path
|
||||
)
|
||||
|
||||
@ -17,8 +17,10 @@ from onguard24.root_html import render_root_page
|
||||
from onguard24.status_snapshot import build as build_status
|
||||
from onguard24.ui_logs import router as logs_router
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
||||
log = logging.getLogger("onguard24")
|
||||
|
||||
|
||||
@ -34,8 +36,8 @@ def parse_addr(http_addr: str) -> tuple[str, int]:
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
install_log_handler(asyncio.get_event_loop())
|
||||
settings = get_settings()
|
||||
install_log_handler(asyncio.get_event_loop(), log_file=settings.log_file)
|
||||
pool = await create_pool(settings)
|
||||
bus = InMemoryEventBus()
|
||||
register_module_events(bus, pool)
|
||||
|
||||
Reference in New Issue
Block a user