M26: harden clean observability acceptance
This commit is contained in:
@@ -2594,3 +2594,20 @@ evidence yet."
|
|||||||
prior Starlette/httpx warning is confirmed absent in the rebuilt environment.
|
prior Starlette/httpx warning is confirmed absent in the rebuilt environment.
|
||||||
- Exact next action: run complete clean acceptance, push all five milestone commits,
|
- Exact next action: run complete clean acceptance, push all five milestone commits,
|
||||||
create a verified live backup, redeploy and execute live browser acceptance.
|
create a verified live backup, redeploy and execute live browser acceptance.
|
||||||
|
|
||||||
|
## M26 — clean acceptance hardening (2026-08-10)
|
||||||
|
|
||||||
|
- The clean full-suite run exposed and fixed two observability regressions that targeted
|
||||||
|
runs against a stale image had missed: readiness now returns immediately with 503 only
|
||||||
|
on database failure and reaches the healthy 200 branch correctly; Prometheus outbox
|
||||||
|
series now use the documented label order.
|
||||||
|
- All synthetic/operational and pending/delivering/succeeded/failed outbox combinations
|
||||||
|
are initialized to zero before persisted counts are applied. Fresh installs, restores
|
||||||
|
and demo resets therefore produce stable zero-valued metrics instead of misleading
|
||||||
|
`no data` panels and absent alert inputs.
|
||||||
|
- Evidence from the final rebuilt isolated stack: complete backend **239 passed with zero
|
||||||
|
warnings**, ruff passed and mypy passed across 58 source files. Frontend TypeScript
|
||||||
|
lint and production build pass; full and production-only npm audits both report zero
|
||||||
|
vulnerabilities.
|
||||||
|
- Exact next action: commit/push, take a verified pre-deployment backup, redeploy to
|
||||||
|
Unraid, then run migration, health, browser and full Playwright acceptance.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from sqlalchemy import func, select
|
|||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.core.db import SessionLocal
|
from app.core.db import SessionLocal
|
||||||
from app.core.observability import OUTBOX_EVENTS
|
from app.core.observability import OUTBOX_EVENTS
|
||||||
from app.models.outbox import DEMO_SCENARIO_ERROR_CODE, OutboxEvent
|
from app.models.outbox import DELIVERY_STATUSES, DEMO_SCENARIO_ERROR_CODE, OutboxEvent
|
||||||
|
|
||||||
router = APIRouter(tags=["observability"])
|
router = APIRouter(tags=["observability"])
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
@@ -26,8 +26,14 @@ def _refresh_database_metrics() -> None:
|
|||||||
).group_by(OutboxEvent.delivery_status, "demo")
|
).group_by(OutboxEvent.delivery_status, "demo")
|
||||||
).all()
|
).all()
|
||||||
OUTBOX_EVENTS.clear()
|
OUTBOX_EVENTS.clear()
|
||||||
|
# Keep every time series present even when a state currently contains no rows.
|
||||||
|
# Stable zero-valued series make dashboards and alerts deterministic after resets,
|
||||||
|
# restores and fresh installations instead of turning "zero" into "no data".
|
||||||
|
for scenario in ("synthetic", "operational"):
|
||||||
|
for status in DELIVERY_STATUSES:
|
||||||
|
OUTBOX_EVENTS.labels(scenario, status).set(0)
|
||||||
for status, is_demo, count in rows:
|
for status, is_demo, count in rows:
|
||||||
OUTBOX_EVENTS.labels(str(status), "synthetic" if is_demo else "operational").set(count)
|
OUTBOX_EVENTS.labels("synthetic" if is_demo else "operational", str(status)).set(count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/metrics", include_in_schema=False)
|
@router.get("/metrics", include_in_schema=False)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ HTTP_IN_PROGRESS = Gauge(
|
|||||||
OUTBOX_EVENTS = Gauge(
|
OUTBOX_EVENTS = Gauge(
|
||||||
"mobilityops_outbox_events",
|
"mobilityops_outbox_events",
|
||||||
"Persisted outbox events by state and scenario type.",
|
"Persisted outbox events by state and scenario type.",
|
||||||
("status", "scenario"),
|
("scenario", "status"),
|
||||||
)
|
)
|
||||||
DATABASE_READY = Gauge(
|
DATABASE_READY = Gauge(
|
||||||
"mobilityops_database_ready",
|
"mobilityops_database_ready",
|
||||||
|
|||||||
+4
-4
@@ -148,11 +148,11 @@ def readiness() -> JSONResponse:
|
|||||||
db.execute(text("SELECT 1"))
|
db.execute(text("SELECT 1"))
|
||||||
except Exception: # noqa: BLE001 -- readiness must convert infrastructure errors to 503
|
except Exception: # noqa: BLE001 -- readiness must convert infrastructure errors to 503
|
||||||
DATABASE_READY.set(0)
|
DATABASE_READY.set(0)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=503,
|
||||||
|
content={"status": "not_ready", "service": "mobilityops-api", "database": "down"},
|
||||||
|
)
|
||||||
DATABASE_READY.set(1)
|
DATABASE_READY.set(1)
|
||||||
return JSONResponse(
|
|
||||||
status_code=503,
|
|
||||||
content={"status": "not_ready", "service": "mobilityops-api", "database": "down"},
|
|
||||||
)
|
|
||||||
return JSONResponse(content={"status": "ready", "service": "mobilityops-api", "database": "up"})
|
return JSONResponse(content={"status": "ready", "service": "mobilityops-api", "database": "up"})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user