Skip to content

Module — Supervision (état des services)

Synchronisé avec le code. Mettre à jour à chaque changement structurel.

Rôle

Savoir, sans attendre qu'un utilisateur se plaigne, si les services externes dont Horizon dépend répondent : Visual Planning, Business Central, Saferpay, CembraPay, les envois d'emails, la recherche du site. Complète le monitoring des jobs du worker (/api/health/workers, cf. architecture/overview.md).

Motivation (août 2026) : Visual Planning répondait en ~100 s sans timeout, ce qui plantait l'éditeur de réservation. Le correctif dégrade désormais en silence (cf. resources-visual-planning.md > Timeout et dégradation) — cette page est la contrepartie de ce silence : plus aucune interface ne remonte l'erreur, mais l'état est visible ici et notifié.

Emplacement

  • Sondes et service : src/Application/Services/Health/ (ServiceHealthProbe, ServiceHealthService, HealthServices, Probes/)
  • Entité : src/Domain/Entities/ServiceHealthCheck.cs, enum Domain/Enum/ServiceHealthStatus.cs
  • Page : src/Web/Pages/ServicesStatus/Index.cshtml(.cs) — menu Système > État des services, [Authorize(Roles = "Administrator,Direction")]
  • API : src/Web/Areas/Api/HealthController.csGET /api/health/services
  • Job : BackgroundWorker.DoServicesHealthJob, toutes les 5 min (Workers.ServicesHealth)
  • Tests : tests/Application/Services/ServiceHealthServiceTest.cs

Fonctionnement

Sondes

Une classe par service, dérivée de ServiceHealthProbe, qui expose Service / Category / IsCritical et implémente ProbeAsync. La classe de base mesure le temps de réponse, attrape toute exception et la transforme en Down : une sonde ne peut jamais faire tomber la page ni le job.

ServiceSondeCritique
Base de données (SQL Server)Database.CanConnectAsyncoui
Visual Planningresources?resourceModel=ETAPE&count=1 (le plus petit référentiel VP, ~1.5 Ko)oui
Business Centraljeton Azure AD (cache ~1 h de BcSoapService) + GET du WSDL de WS/SystemService?tenant=…oui
SaferpayTransaction/Inquire sur une transaction inexistante → 402 TRANSACTION_NOT_FOUND = API debout et identifiants acceptés, 401 = identifiants refusésoui
CembraPayjeton Azure AD client-credentials + joignabilité de BaseUrloui
SendGridGET /v3/scopes (droits de la clé, hors quota d'envoi)oui
BetterSearchGET /travels?search=a&size=1, le vrai endpoint de TravelControllernon
Gender-APIjoignabilité seule — chaque appel authentifié consomme le quota du forfaitnon
Google Mapsjoignabilité seule — l'API est facturée à la requêtenon

Codes HTTP vérifiés en vrai (18.08.2026), deux pièges à ne pas réintroduire :

  • Business Central : la racine des web services SOAP (GetBaseWebServiceUrl(), /WS/{Company}) renvoie 500 — ce n'est pas une ressource interrogeable, c'était le premier essai et il affichait BC en panne alors qu'il tournait. WS/SystemService?tenant=… renvoie 200 (WSDL) avec un jeton valide et 401 sans, sans dépendre d'une page publiée : c'est ce qui s'en rapproche le plus d'un endpoint de santé côté BC. BcOptions.GetSystemServiceUrl().
  • Saferpay : une erreur métier sort en 402 ({"ErrorName":"TRANSACTION_NOT_FOUND"}), pas en 400 — d'où un faux « hors service » au premier essai. Des identifiants faux sortent bien en 401, la sonde distingue donc réellement les deux cas.

Aucune sonde n'a d'effet de bord : pas d'email de test, pas de transaction de test, pas d'écriture. C'est la règle à respecter pour toute sonde ajoutée.

Statuts

Up / Degraded (répond mais au-delà de DegradedAboveMs, 3 s par défaut, 1 s pour SQL) / Down / NotConfigured (clé ou URL absente de l'environnement — ni disponible ni en panne, exclu du calcul de disponibilité).

Deux garde-fous de temps

  1. Chaque sonde est plafonnée à 5 s (ServiceHealthProbe.MaxDuration), via un Task.WhenAny contre un Task.Delay — et pas seulement via le CancellationToken, parce qu'une dépendance peut l'ignorer (BcSoapService.GetAccessTokenAsync n'en prend pas). Au-delà, le service est Down et la sonde abandonnée continue dans le vide, exception observée pour ne pas remonter en UnobservedTaskException.
  2. Toutes les sondes tournent en parallèle (Task.WhenAll) : la page répond en ~5 s dans le pire cas, jamais en somme des sondes.

La page charge d'ailleurs son tableau après rendu (handler ?handler=Check en fetch, auto-refresh 60 s) : l'écran s'affiche immédiatement même si un service met 5 s.

Historique et alerting

  • DoServicesHealthJob (worker, 5 min) appelle CheckAllAsync puis RecordAsync : une ligne ServiceHealthCheck par service et par tour, purgée au-delà de 30 jours (par lots de 1000, le worker repasse).
  • GetHistoryAsync(days = 7) calcule le taux de disponibilité et regroupe les relevés Down consécutifs en une seule coupure (affichée « du … au … (n relevés) »).
  • Notification Administrator sur bascule uniquement — quand un service critique tombe, et quand il revient. In-app uniquement, jamais par mail (choix Buchard, août 2026) : une panne de service se regarde depuis Horizon. L'état précédent est relu sur les 2 dernières heures : worker arrêté plus longtemps ⇒ une notification de rappel, jamais de spam à chaque tour.

Endpoint de monitoring externe

GET /api/health/services[AllowAnonymous], 200 si aucun service critique n'est Down, 503 sinon. Réponse mise en cache 30 s : l'endpoint est anonyme et déclenche un appel sortant par service, sans cache un monitoring bavard martèlerait Saferpay, BC et VP. Le message de la sonde en est volontairement absent (pas d'URL ni de détail interne exposé), contrairement à la page back-office.

Ajouter un service surveillé

  1. Une classe dans Application/Services/Health/Probes/ dérivée de ServiceHealthProbe.
  2. Une ligne dans HealthServices.Configureseul endroit à toucher, Web et Worker partagent cette liste.
  3. Si l'option de configuration n'existe pas encore côté worker, l'ajouter dans WorkerHostBuilder (sinon la sonde y sera NotConfigured alors qu'elle est verte côté web).

Points d'attention / pièges

  • Ne jamais utiliser ExecuteDeleteAsync dans ce service : le provider InMemory des tests ne le supporte pas. La purge passe par RemoveRange sur un lot borné.
  • Le worker et le web ont deux DI distinctes : un service surveillé côté web mais non configuré côté worker produira un historique NotConfigured alors que la page affiche Up.
  • Workers.ServicesHealth est surveillé par /api/health/workers avec une tolérance de 15 min (3 intervalles).
  • La migration EF ServiceHealthChecks doit être créée et appliquée avant que la page d'historique et le job ne fonctionnent.

Dépendances

  • resources-visual-planning (sonde VP, contrepartie de la dégradation silencieuse).
  • billing-payment (sondes Saferpay / CembraPay / Business Central).
  • notes-notifications (notification Administrator sur bascule).

Contributors

No contributors

Changelog

No recent changes