
OpsPulse
Plateforme d'exploitation et d'incidents en temps réel
Ingère des événements système en volume, déduplique les alertes en incidents et pilote la prise en charge et l'escalade via une machine à états explicite. Les écritures passent par EF Core ; les tableaux de bord lisent des projections Dapper et se mettent à jour en direct via SignalR.
Ingestion à haut débit, séparation lecture/écriture et diffusion en temps réel.
- .NET 10
- ASP.NET Core
- Dapper
- RabbitMQ
- SignalR
- Hangfire
- Serilog
- Docker
Projet personnel — pas encore de dépôt public
Le problème qu'elle sert à travailler
OrderFlow, c'est peu de données et beaucoup de règles. OpsPulse est l'image miroir : beaucoup de données et une petite machine à états au milieu. C'est volontaire — les deux réunis couvrent l'essentiel de ce sur quoi on demande à un architecte backend de raisonner.
La tension centrale est que l'ingestion veut être rapide et bête, tandis que la gestion d'incidents veut être prudente et à état. Mettre les deux sur un même chemin d'écriture en dégrade nécessairement une.
Le chemin d'ingestion
Accepter un événement ne déclenche aucune analyse. On valide la forme, on l'écrit et on répond — parce que la seule chose pire qu'une alerte manquée est un point d'ingestion qui ralentit assez pour commencer à les refuser pendant l'incident même qu'il devait attraper.
Le dédoublonnage, c'est tout le produit
Un service en panne n'émet pas un événement. Il en émet des milliers, et une plateforme qui transforme chacun d'eux en incident est pire que pas de plateforme du tout, parce qu'elle apprend aux gens à l'ignorer. Condenser un déluge en un incident assorti d'un compteur est la fonctionnalité que tout le reste sert à soutenir.
- Les événements portent une empreinte — service, contrôle et type de défaillance — et ceux qui partagent une empreinte se replient dans l'incident ouvert correspondant plutôt que d'en créer un nouveau.
- Des fenêtres de suppression empêchent un service instable de réouvrir sans cesse le même incident.
- Le flux d'événements brut est conservé intact et en append-only : un incident dédoublonné peut donc toujours être redéployé en tout ce qui l'a alimenté.
Un système d'alerte se juge à ce pour quoi il ne vous réveille pas.
La machine à états d'un incident
Le statut d'un incident est une machine à états sur l'agrégat, pas une colonne de statut. Les mouvements légaux sont peu nombreux, et l'intérêt de les encoder est qu'un incident ne peut pas être résolu sans avoir été pris en charge, ni pris en charge deux fois par des personnes différentes, quel que soit l'ordre d'arrivée des messages.
- 1OuvertDéclenché par le moteur de détection. Les minuteurs d'escalade démarrent ici.
- 2Pris en chargeUn humain s'en est saisi. L'escalade s'arrête ; les minuteurs de SLA continuent.
- 3EscaladéAucune prise en charge dans la fenêtre prévue par la politique — l'astreinte suivante est notifiée.
- 4RésoluÉtat terminal. Une réouverture crée un incident lié plutôt que de modifier un incident clos.
Chaque transition s'ajoute à une chronologie immuable. Après l'incident, la question est toujours « quand l'avons-nous su, et quand avons-nous agi » — à laquelle on ne peut répondre que si la réponse a été enregistrée au fil de l'eau.
Escalade et astreinte
L'escalade est pilotée par le temps, ce qui en fait du travail planifié et non du travail événementiel. Hangfire détient les minuteurs ; quand l'un se déclenche, il publie un événement et l'escalade elle-même circule par le même chemin de messagerie que tout le reste.
Lecture et écriture, séparées
C'est ici que CQRS cesse d'être un nom de pattern pour commencer à payer son loyer. Les écritures sont petites, fréquentes et transactionnelles. Les lectures sont larges, agrégées et constantes — un tableau de bord d'exploitation est une charge de requêtes permanente.
| Côté écriture | Côté lecture | |
|---|---|---|
| Forme | Un incident à la fois | Compteurs, plages et agrégations sur beaucoup |
| Outil | EF Core 10, suivi, transactionnel | Dapper, SQL explicite, projections |
| Optimisé pour | La justesse sous concurrence | La latence à charge constante |
| Pagination | Sans objet | Par keyset, parce que les offsets dérivent sous les insertions |
La pagination par keyset compte davantage ici que dans la plupart des systèmes : des lignes sont insérées en continu, et une pagination par offset sur une table qui bouge montre au lecteur des doublons et des trous à mesure qu'il tourne les pages.
Le tableau de bord en direct
Un tableau de bord qu'il faut rafraîchir n'est pas un tableau de bord d'exploitation. SignalR pousse la création, la prise en charge et la résolution des incidents vers les clients connectés au moment où elles se produisent, et le client les applique à l'état qu'il détient déjà plutôt que de tout recharger.
- Les envois sont des diffs, pas des rechargements complets du tableau de bord.
- Un client qui se reconnecte se resynchronise depuis le modèle de lecture : une socket coupée ne peut donc pas laisser l'interface silencieusement périmée.
- Uniquement côté client — le trafic entre services reste sur RabbitMQ.
Le travail planifié
- Sondes de santé contre les services enregistrés, pour que le silence se distingue de la bonne santé.
- Minuteurs d'escalade et de SLA.
- Retraitement des événements dont la détection a échoué, hors du chemin d'ingestion.
- Rétention et agrégation du flux d'événements brut.
Observer l'observateur
Une plateforme de supervision que l'on ne peut pas superviser est un point de défaillance unique avec des étapes en plus. Le même dispositif Serilog et OpenTelemetry s'applique, auquel s'ajoutent les questions propres à cette forme : retard d'ingestion, retard de détection, et l'écart entre les modèles de lecture et le côté écriture.
- Retard d'ingestion et profondeur de file comme métriques de premier ordre, pas comme lignes de logs.
- Corrélation de l'événement entrant jusqu'à l'incident qu'il a alimenté, en passant par la détection.
- Liveness et readiness séparées, pour qu'une file pleine ne se lise pas comme un processus mort.
En quoi cela diffère d'OrderFlow
Les deux ne sont volontairement pas le même système avec d'autres noms. Ils sollicitent des parties différentes de la même boîte à outils, et c'est précisément l'intérêt de construire les deux.
| OrderFlow | OpsPulse | |
|---|---|---|
| Le point dur | La cohérence transactionnelle à travers un workflow | Le débit, et la condensation du volume en sens |
| Poids du domaine | Lourd — des règles par commande | Léger — une petite machine à états |
| Poids des données | Léger | Lourd |
| Défaillance à craindre | Une commande bloquée en cours de workflow | Une tempête d'alertes, ou une alerte manquée |
| Côté lecture | Utile | Porteur |
Les compromis que j'accepte
| Décision | Coût |
|---|---|
| Ingestion « accepter et mettre en file » | L'appelant n'obtient aucun résultat de détection ; les incidents apparaissent un instant plus tard |
| Dédoublonnage par empreinte | Une mauvaise empreinte fusionne des défaillances distinctes, ou en scinde une |
| Événements bruts en append-only | Le stockage croît vite ; rétention et agrégation sont obligatoires, pas optionnelles |
| Modèles de lecture Dapper | Les projections doivent être maintenues à la main à mesure que le modèle d'écriture change |
Envie d'en discuter la conception ?
C'est une conception : elle est donc encore discutable. Si vous pensez que l'un des compromis ci-dessus est mauvais, je préfère l'entendre maintenant plutôt qu'après la construction.