feat(per-instance-hook-scoping): scope observability + backup hooks by instance
This commit is contained in:
@@ -129,9 +129,10 @@ def post_backup_start(
|
||||
|
||||
@router.get("/jobs")
|
||||
def get_backup_jobs(
|
||||
service_id: str | None = None,
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> list[dict[str, Any]]:
|
||||
jobs = store.list_backup_jobs()
|
||||
jobs = store.list_backup_jobs(service_id=service_id)
|
||||
return jobs
|
||||
|
||||
|
||||
@@ -155,9 +156,10 @@ def get_backup_runs(
|
||||
job_id: str | None = None,
|
||||
status: str | None = None,
|
||||
limit: int = 50,
|
||||
service_id: str | None = None,
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> list[BackupRunResponse]:
|
||||
runs = store.list_backup_runs(job_id=job_id, status=status, limit=limit)
|
||||
runs = store.list_backup_runs(job_id=job_id, status=status, limit=limit, service_id=service_id)
|
||||
return [BackupRunResponse(**run) for run in runs]
|
||||
|
||||
|
||||
@@ -177,9 +179,12 @@ def get_backup_alerts(
|
||||
job_id: str | None = None,
|
||||
acknowledged: bool | None = None,
|
||||
severity: str | None = None,
|
||||
service_id: str | None = None,
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> list[BackupAlertResponse]:
|
||||
alerts = store.list_backup_alerts(job_id=job_id, acknowledged=acknowledged, severity=severity)
|
||||
alerts = store.list_backup_alerts(
|
||||
job_id=job_id, acknowledged=acknowledged, severity=severity, service_id=service_id,
|
||||
)
|
||||
return [BackupAlertResponse(**alert) for alert in alerts]
|
||||
|
||||
|
||||
|
||||
@@ -1075,10 +1075,16 @@ class SettingsStore:
|
||||
row = conn.execute("SELECT * FROM backup_jobs WHERE id = ?", (job_id,)).fetchone()
|
||||
return self._row_to_job(row) if row else None
|
||||
|
||||
def list_backup_jobs(self) -> list[dict[str, Any]]:
|
||||
def list_backup_jobs(self, service_id: str | None = None) -> list[dict[str, Any]]:
|
||||
self.init_schema()
|
||||
where = ""
|
||||
params: list[Any] = []
|
||||
if service_id:
|
||||
where = "WHERE service_id = ?"
|
||||
params.append(service_id)
|
||||
sql = f"SELECT * FROM backup_jobs {where} ORDER BY created_at DESC"
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute("SELECT * FROM backup_jobs ORDER BY created_at DESC").fetchall()
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
return [self._row_to_job(row) for row in rows]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -1167,6 +1173,7 @@ class SettingsStore:
|
||||
job_id: str | None = None,
|
||||
status: str | None = None,
|
||||
limit: int = 50,
|
||||
service_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
self.init_schema()
|
||||
clauses: list[str] = []
|
||||
@@ -1177,6 +1184,9 @@ class SettingsStore:
|
||||
if status:
|
||||
clauses.append("status = ?")
|
||||
params.append(status)
|
||||
if service_id:
|
||||
clauses.append("job_id IN (SELECT id FROM backup_jobs WHERE service_id = ?)")
|
||||
params.append(service_id)
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
sql = f"SELECT * FROM backup_runs {where} ORDER BY created_at DESC LIMIT ?"
|
||||
params.append(max(1, min(int(limit), 200)))
|
||||
@@ -1257,6 +1267,7 @@ class SettingsStore:
|
||||
job_id: str | None = None,
|
||||
acknowledged: bool | None = None,
|
||||
severity: str | None = None,
|
||||
service_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
self.init_schema()
|
||||
clauses: list[str] = []
|
||||
@@ -1270,6 +1281,9 @@ class SettingsStore:
|
||||
if severity:
|
||||
clauses.append("severity = ?")
|
||||
params.append(severity)
|
||||
if service_id:
|
||||
clauses.append("job_id IN (SELECT id FROM backup_jobs WHERE service_id = ?)")
|
||||
params.append(service_id)
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
sql = f"SELECT * FROM backup_alerts {where} ORDER BY created_at DESC"
|
||||
with self.connect() as conn:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from media_library_viewer_api.main import app
|
||||
@@ -33,6 +34,88 @@ def test_dashboard_backups():
|
||||
auth_module._API_KEY = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-instance service scoping (PI-110, PI-111, PI-119)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path):
|
||||
"""Fresh SettingsStore with schema initialized."""
|
||||
db_path = tmp_path / "test_settings.sqlite"
|
||||
s = SettingsStore(db_path)
|
||||
s.init_schema()
|
||||
return s
|
||||
|
||||
|
||||
class TestBackupServiceScoping:
|
||||
"""Verify service_id filtering on list_backup_jobs/runs/alerts."""
|
||||
|
||||
def test_list_backup_jobs_filtered_by_service(self, store: SettingsStore):
|
||||
store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
assert len(store.list_backup_jobs(service_id="svc-a")) == 1
|
||||
assert len(store.list_backup_jobs(service_id="svc-b")) == 1
|
||||
|
||||
def test_list_backup_jobs_unfiltered_returns_all(self, store: SettingsStore):
|
||||
store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
assert len(store.list_backup_jobs()) == 2
|
||||
assert len(store.list_backup_jobs(service_id="")) == 2
|
||||
|
||||
def test_list_backup_runs_filtered_by_service(self, store: SettingsStore):
|
||||
job_a = store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
job_b = store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
store.create_backup_run({"job_id": job_a["id"], "started_at": 1700000000, "status": "success"})
|
||||
store.create_backup_run({"job_id": job_b["id"], "started_at": 1700000000, "status": "success"})
|
||||
assert len(store.list_backup_runs(service_id="svc-a")) == 1
|
||||
assert len(store.list_backup_runs(service_id="svc-b")) == 1
|
||||
|
||||
def test_list_backup_alerts_filtered_by_service(self, store: SettingsStore):
|
||||
job_a = store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
job_b = store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
store.create_backup_run({"job_id": job_a["id"], "started_at": 1700000000, "status": "success"})
|
||||
store.create_backup_run({"job_id": job_b["id"], "started_at": 1700000000, "status": "success"})
|
||||
store.create_backup_alert(
|
||||
{"job_id": job_a["id"], "alert_type": "test", "severity": "warning"}
|
||||
)
|
||||
store.create_backup_alert(
|
||||
{"job_id": job_b["id"], "alert_type": "test", "severity": "warning"}
|
||||
)
|
||||
assert len(store.list_backup_alerts(service_id="svc-a")) == 1
|
||||
assert len(store.list_backup_alerts(service_id="svc-b")) == 1
|
||||
|
||||
def test_list_backup_runs_unfiltered_returns_all(self, store: SettingsStore):
|
||||
job_a = store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
job_b = store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
store.create_backup_run({"job_id": job_a["id"], "started_at": 1700000000, "status": "success"})
|
||||
store.create_backup_run({"job_id": job_b["id"], "started_at": 1700000000, "status": "success"})
|
||||
assert len(store.list_backup_runs()) == 2
|
||||
|
||||
def test_endpoint_threads_service_id_to_store(self, store: SettingsStore):
|
||||
"""GET /api/backups/jobs?service_id=svc-a filters via the endpoint."""
|
||||
import media_library_viewer_api.auth as auth_module
|
||||
from media_library_viewer_api.services import settings_store
|
||||
|
||||
store.upsert_backup_job({"name": "job-a", "service_id": "svc-a"})
|
||||
store.upsert_backup_job({"name": "job-b", "service_id": "svc-b"})
|
||||
|
||||
original_store = settings_store._store
|
||||
settings_store._store = store
|
||||
auth_module._API_KEY = None
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/backups/jobs", params={"service_id": "svc-a"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["name"] == "job-a"
|
||||
finally:
|
||||
settings_store._store = original_store
|
||||
auth_module._API_KEY = None
|
||||
|
||||
|
||||
def test_post_backup_report():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test_settings.sqlite"
|
||||
|
||||
Reference in New Issue
Block a user