feat(server): endpoint GET /api/v1/agents/logs/{machine_id} (push-log-DGX, brique 3)
Route de diagnostic dashboard (read-only) : restitue les logs poussés par un poste, rangés par machine_id. Bearer global ; volontairement sans garde fleet (consultation d'un poste révoqué/en panne). limit=tail pour borner la réponse. 4 tests d'intégration verts ; store inchangé (briques 1-2 figées). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7250,6 +7250,30 @@ async def agents_logs(request: AgentLogsRequest):
|
|||||||
return {"status": "ok", "received": received, "machine_id": machine_id}
|
return {"status": "ok", "received": received, "machine_id": machine_id}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/agents/logs/{machine_id}")
|
||||||
|
async def get_agents_logs(machine_id: str, limit: int = 1000):
|
||||||
|
"""Lecture des logs poussés par un poste (push-log-DGX, brique 3).
|
||||||
|
|
||||||
|
Route de diagnostic dashboard : restitue les logs rangés par machine_id
|
||||||
|
(poste sans AnyDesk). Lecture admin read-only — volontairement SANS garde
|
||||||
|
fleet : on doit pouvoir consulter un poste révoqué ou en panne. Seul le
|
||||||
|
Bearer (dépendance globale `_verify_token`) protège l'accès.
|
||||||
|
|
||||||
|
`limit` borne la réponse aux N entrées les plus récentes (tail) pour éviter
|
||||||
|
de renvoyer plusieurs jours de logs d'un coup.
|
||||||
|
"""
|
||||||
|
entries = agent_logs_store.read(machine_id)
|
||||||
|
total = len(entries)
|
||||||
|
if limit and limit > 0:
|
||||||
|
entries = entries[-limit:]
|
||||||
|
return {
|
||||||
|
"machine_id": machine_id,
|
||||||
|
"total": total,
|
||||||
|
"count": len(entries),
|
||||||
|
"logs": entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# R2 MVP P0 — DialogResolver (catalogue centralisé des modaux runtime)
|
# R2 MVP P0 — DialogResolver (catalogue centralisé des modaux runtime)
|
||||||
# Flag OFF par défaut. Activer en posant RPA_DIALOG_RESOLVER_ENABLED=true.
|
# Flag OFF par défaut. Activer en posant RPA_DIALOG_RESOLVER_ENABLED=true.
|
||||||
|
|||||||
@@ -121,3 +121,72 @@ def test_post_logs_rejects_oversized_batch(logs_client):
|
|||||||
|
|
||||||
assert resp.status_code == 413, resp.text
|
assert resp.status_code == 413, resp.text
|
||||||
assert store.read("lea-flood") == []
|
assert store.read("lea-flood") == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Brique 3 — lecture des logs par machine_id (route dashboard, read-only).
|
||||||
|
# Lecture admin/diagnostic : PAS de garde fleet (on veut justement pouvoir
|
||||||
|
# consulter un poste révoqué ou en panne) ; seul le Bearer protège.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logs_returns_persisted_for_machine(logs_client):
|
||||||
|
"""GET /agents/logs/{machine_id} restitue les logs stockés, dans l'ordre."""
|
||||||
|
client, token, store = logs_client
|
||||||
|
store.append(
|
||||||
|
"lea-emilie-001",
|
||||||
|
[
|
||||||
|
{"ts": "2026-06-26T16:00:00", "level": "INFO", "message": "demarrage"},
|
||||||
|
{"ts": "2026-06-26T16:00:01", "level": "WARNING", "message": "popup"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
"/api/v1/agents/logs/lea-emilie-001", headers=_auth_headers(token)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["machine_id"] == "lea-emilie-001"
|
||||||
|
assert body["count"] == 2
|
||||||
|
assert body["total"] == 2
|
||||||
|
assert body["logs"][0]["message"] == "demarrage"
|
||||||
|
assert body["logs"][1]["level"] == "WARNING"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logs_without_token_returns_401(logs_client):
|
||||||
|
client, _, _ = logs_client
|
||||||
|
resp = client.get("/api/v1/agents/logs/lea-emilie-001")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logs_empty_for_unknown_machine(logs_client):
|
||||||
|
"""Un poste sans log remonte une liste vide (200), pas une erreur."""
|
||||||
|
client, token, _ = logs_client
|
||||||
|
resp = client.get(
|
||||||
|
"/api/v1/agents/logs/lea-inconnu", headers=_auth_headers(token)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["count"] == 0
|
||||||
|
assert body["total"] == 0
|
||||||
|
assert body["logs"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logs_limit_returns_tail(logs_client):
|
||||||
|
"""`limit` borne la réponse aux N entrées les plus récentes (tail)."""
|
||||||
|
client, token, store = logs_client
|
||||||
|
store.append(
|
||||||
|
"lea-tail",
|
||||||
|
[{"level": "INFO", "message": f"m{i}"} for i in range(5)],
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
"/api/v1/agents/logs/lea-tail?limit=2", headers=_auth_headers(token)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["total"] == 5
|
||||||
|
assert body["count"] == 2
|
||||||
|
assert [e["message"] for e in body["logs"]] == ["m3", "m4"]
|
||||||
|
|||||||
Reference in New Issue
Block a user