SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%

monitoring: sources vides légitimes ≠ pannes + stale seulement si la dernière synchro observée est malsaine

Revue du parc 2026-08-23 : les « brisés » niddamour, courtemanche,
place_florimay, atlas_immo étaient des sources sans inventaire en ligne
(vérifiées à la main) qui synchronisaient ok à 0 résultat.

- un 0 résultat n est suspect que si la médiane historique de la source
  est ≥ 1 ; sinon c est un vide légitime (streak remis à zéro)
- une source dont la dernière synchro observée est saine n est plus
  marquée stale même si son dernier succès avec résultats est ancien
- tests mis à jour + 2 nouveaux cas (53 verts)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent eda3f1b

2 changed files +51 −4

modified src/monitoring/connector_health.py +21 −2
@@ -148,9 +148,17 @@ def assess_source(
148 148 new_entries = [
149 149 e for e in entries if prev_checked_ts is None or e.ts > prev_checked_ts
150 150 ]
151 + # Un « 0 résultat » n'est suspect que si la source a normalement des
152 + # résultats (médiane historique ≥ 1). Une source vide légitimement
153 + # (agence sans inventaire en ligne, employeur sans poste ouvert…) qui
154 + # synchronise ok à 0 n'est PAS en panne — revue du 2026-08-23 : les
155 + # « brisés » niddamour, courtemanche, place_florimay, atlas_immo étaient
156 + # tous des vides légitimes vérifiés à la main.
157 + legit_empty_history = (prev.median_found or 0) < 1
151 158 consecutive_failures = prev.consecutive_failures
152 159 for entry in reversed(new_entries):
153 − if not entry.ok or entry.found <= 0:
160 + suspicious_zero = entry.ok and entry.found <= 0 and not legit_empty_history
161 + if not entry.ok or suspicious_zero:
154 162 consecutive_failures += 1
155 163 else:
156 164 consecutive_failures = 0
@@ -175,6 +183,13 @@ def assess_source(
175 183
176 184 last_message = next((e.message for e in entries if e.message), "")
177 185
186 + # La dernière synchro observée est-elle saine ? (ok avec des résultats,
187 + # ou ok à 0 pour une source vide légitimement) — si oui, la source n'est
188 + # pas « stale » même si son dernier succès AVEC résultats est ancien.
189 + latest_healthy = bool(entries) and entries[0].ok and (
190 + entries[0].found > 0 or legit_empty_history
191 + )
192 +
178 193 if consecutive_failures >= BROKEN_THRESHOLD:
179 194 message = (
180 195 f"{consecutive_failures} synchros consécutives en échec ou à 0 résultat"
@@ -182,7 +197,11 @@ def assess_source(
182 197 if last_message and last_message != "ok":
183 198 message += f" (dernier message : {last_message})"
184 199 status = STATUS_BROKEN
185 − elif last_success is not None and (now - last_success.timestamp()) > stale_after_seconds:
200 + elif (
201 + not latest_healthy
202 + and last_success is not None
203 + and (now - last_success.timestamp()) > stale_after_seconds
204 + ):
186 205 hours = (now - last_success.timestamp()) / 3600
187 206 message = (
188 207 f"aucun sync réussi depuis {hours:.1f} h "
modified tests/test_connector_health.py +30 −2
@@ -69,13 +69,41 @@ def test_assess_broken_apres_trois_echecs():
69 69
70 70
71 71 def test_assess_broken_apres_trois_zero_resultats():
72 − # Syncs "réussis" mais vides ×3 → broken.
72 + # Syncs "réussis" mais vides ×3 ALORS QUE la source a normalement des
73 + # résultats (médiane historique ≥ 1) → broken.
73 74 entries = [_entry(h, found=0, ok=True) for h in (0.5, 1.5, 2.5)]
74 − result = assess_source(entries, SourceState(), NOW, stale_after_seconds=100 * HOUR)
75 + prev = SourceState(median_found=40.0)
76 + result = assess_source(entries, prev, NOW, stale_after_seconds=100 * HOUR)
75 77 assert result.status == "broken"
76 78 assert result.consecutive_failures == 3
77 79
78 80
81 +def test_assess_zero_legitime_pas_broken():
82 + # Source vide légitimement (médiane historique < 1) : des syncs ok à
83 + # 0 résultat ne sont PAS des pannes (revue 2026-08-23 : niddamour,
84 + # courtemanche, place_florimay, atlas_immo — vérifiés à la main).
85 + entries = [_entry(h, found=0, ok=True) for h in (0.5, 1.5, 2.5)]
86 + result = assess_source(
87 + entries, SourceState(median_found=0.0), NOW, stale_after_seconds=100 * HOUR
88 + )
89 + assert result.status != "broken"
90 + assert result.consecutive_failures == 0
91 +
92 +
93 +def test_assess_zero_legitime_pas_stale():
94 + # Une source vide légitime qui synchronise ok n'est pas « stale » même
95 + # si son dernier succès AVEC résultats est ancien.
96 + prev = SourceState(
97 + median_found=0.0,
98 + last_success=datetime.datetime.fromtimestamp(
99 + NOW - 500 * HOUR, tz=datetime.UTC
100 + ),
101 + )
102 + entries = [_entry(0.5, found=0, ok=True)]
103 + result = assess_source(entries, prev, NOW, stale_after_seconds=100 * HOUR)
104 + assert result.status not in ("stale", "broken")
105 +
106 +
79 107 def test_assess_streak_cumule_avec_etat_precedent():
80 108 prev = SourceState(
81 109 status="degraded",
82 110