Développer AuditStock ne consiste pas seulement à créer des écrans de gestion de stock. Je dois aussi pouvoir déployer l’application, faire évoluer ses fonctionnalités et comprendre rapidement ce qui se passe chez chaque client.

Un site peut répondre correctement tout en ayant un import bloqué. Une opération peut devenir lente sans provoquer d’erreur. Et lorsque plusieurs instances fonctionnent simultanément, je ne peux pas passer ma journée à les vérifier une par une.

J’ai donc organisé mon architecture autour de quatre éléments complémentaires :

  • Le site d’administration AuditStock, déployé dans un conteneur propre à chaque client.
  • Un service de distribution des plugins, pour faire évoluer les fonctionnalités.
  • BlazorTelemetry, avec un conteneur de télémétrie dédié à chaque instance.
  • Health, mon site de surveillance centralisé, accessible à mes partenaires et à moi-même.

Page d’accueil d’AuditStock et indicateurs de gestion des stocks

La page d’accueil d’AuditStock rassemble les indicateurs métier : maîtrise du stock, valorisation, disponibilité, approvisionnement et service client.

Une vue d’ensemble de mon architecture

Je sépare l’exécution de l’application, la distribution des extensions, l’analyse technique et la surveillance du parc.

flowchart TB
    Registry["Registre Docker"]
    Plugins["Catalogue de plugins"]

    subgraph A["Client A"]
        AdminA["Administration AuditStock"]
        DataA[("Données et plugins persistants")]
        TelemetryA["BlazorTelemetry dédié"]
        AdminA --> DataA
        AdminA -->|"Logs, traces et métriques"| TelemetryA
    end

    subgraph B["Client B"]
        AdminB["Administration AuditStock"]
        DataB[("Données et plugins persistants")]
        TelemetryB["BlazorTelemetry dédié"]
        AdminB --> DataB
        AdminB -->|"Logs, traces et métriques"| TelemetryB
    end

    Registry -->|"Image applicative"| AdminA
    Registry -->|"Image applicative"| AdminB
    Plugins -->|"Téléchargement des extensions"| AdminA
    Plugins -->|"Téléchargement des extensions"| AdminB

    AdminA -->|"Appairage automatique"| Health["Health : surveillance du parc"]
    AdminB -->|"Appairage automatique"| Health
    Health -->|"Contrôles HTTPS et rapports"| AdminA
    Health -->|"Contrôles HTTPS et rapports"| AdminB

    Team["Mes partenaires et moi"] --> Health
    Diagnostic["Diagnostic assisté via MCP"] --> TelemetryA
    Diagnostic --> TelemetryB

Cette organisation me permet de partager un même socle logiciel tout en conservant une configuration, des extensions et une destination de télémétrie propres à chaque client.

Un socle .NET modulaire

AuditStock repose sur .NET 10 et ASP.NET Core, avec une interface d’administration Blazor Server.

À l’intérieur de l’application, je distingue les composants d’interface, les contrats, les traitements métier et l’accès aux données. Les requêtes métier passent par ChannelMediator, ce qui me permet de structurer les traitements dans des handlers et de conserver les règles métier hors des écrans.

Cette séparation facilite aussi l’observation : lorsqu’une opération pose problème, je peux chercher quel traitement a été exécuté et où le temps a été consommé.

Je conserve ainsi une application modulaire, dont les fonctionnalités peuvent être enrichies par des plugins.

Comment je génère l’image Docker du site d’administration

Pour distribuer le site d’administration, je construis une image Docker à partir du projet AuditStock.AdminWebApp.

Le Dockerfile utilise plusieurs étapes :

  1. Je prépare l’environnement d’exécution ASP.NET Core et ses dépendances système.
  2. Je restaure les packages et compile l’application avec le SDK .NET.
  3. Je publie l’application en configuration Release.
  4. Je copie le résultat de cette publication dans l’image finale.

Le SDK et les sources utilisés pour construire l’application restent dans les étapes de compilation. L’image finale contient le runtime et les fichiers nécessaires à l’exécution, notamment les dépendances utilisées pour les documents et les PDF.

flowchart LR
    Source["Code source"] --> Restore["Restauration des packages"]
    Restore --> Build["Compilation Release"]
    Build --> Publish["dotnet publish"]
    Publish --> Image["Image finale ASP.NET Core"]
    Image --> Registry["GitHub Container Registry"]
    Registry --> Deploy["Déploiement des instances clients"]

    Secret["Secret de build"] -.-> Restore

L’accès aux packages privés passe par un secret de build Docker. Je n’ai pas besoin d’inscrire ce secret dans le Dockerfile.

Le workflow GitHub Actions publie l’image avec un identifiant de build et une étiquette d’environnement, comme staging ou latest. Je peux ainsi identifier l’artefact produit et sélectionner l’image à déployer.

Je distingue bien ces deux opérations : publier une image dans le registre prépare sa distribution ; déployer cette image met effectivement à jour une instance.

Une image commune, une configuration par client

Je ne reconstruis pas toute l’application pour chaque client. Je réutilise l’image et fournis au déploiement les paramètres propres à l’instance.

Les fichiers persistants, les répertoires de plugins et la configuration ne doivent pas dépendre de la durée de vie du conteneur. Les volumes permettent de conserver ces éléments lorsque je remplace celui-ci.

Devant les instances, Traefik assure le routage par nom de domaine et l’exposition HTTPS.

Un hébergement centralisé pour distribuer les plugins

Les clients n’ont pas tous les mêmes besoins. Certains utilisent un connecteur spécifique, d’autres des fonctions d’analyse ou des extensions métier.

Pour gérer cette diversité, je dispose d’un service dédié : AuditStock.PluginApi.

Ce service possède sa propre image Docker et son stockage persistant. Il héberge le catalogue et les packages des plugins, avec un accès à l’API protégé par clé.

Je distingue ici deux responsabilités :

  • Le service de plugins héberge et distribue les packages.
  • L’instance AuditStock charge et exécute les plugins installés.

Un plugin téléchargé peut ainsi enrichir les menus, les écrans ou les traitements de l’application du client.

flowchart LR
    Code["Code du plugin"] --> Package["Package et métadonnées"]
    Package --> Publisher["Outil de publication"]
    Publisher --> API["PluginApi"]
    API --> Local["Téléchargement et préparation"]
    Local --> Startup["Application au démarrage"]
    Startup --> Admin["Chargement dans AuditStock"]

Au démarrage, l’application recherche les mises à jour des plugins installés et vérifie leur compatibilité avec sa version. Les modifications des fichiers interviennent avant le chargement des assemblies.

J’ai également prévu une limite de temps pour cette recherche : si le catalogue distant ne répond pas, l’application peut continuer son démarrage avec les plugins disponibles localement.

Un conteneur BlazorTelemetry dédié à chaque instance

Chaque instance AuditStock envoie sa télémétrie vers son propre conteneur BlazorTelemetry.

Je conserve ainsi un périmètre de diagnostic clair : lorsque j’analyse un incident chez un client, je consulte la destination de télémétrie associée à son instance.

Tableau de bord BlazorTelemetry avec graphiques HTTP, CPU, mémoire, réseau et disque

Le tableau de bord BlazorTelemetry me permet de rapprocher l’activité applicative de la consommation des ressources. Les informations d’identification ont été floutées.

La collecte repose sur OpenTelemetry et le protocole OTLP. Elle rassemble trois types de signaux complémentaires.

Signal Ce que j’y cherche
Logs Les événements, avertissements et erreurs qui expliquent le déroulement d’une opération.
Traces Le parcours d’une requête et la durée de ses différentes étapes.
Métriques L’évolution des performances, des volumes et des ressources dans le temps.

L’instrumentation couvre notamment ASP.NET Core, les appels HTTP, le runtime, Entity Framework et les handlers ChannelMediator.

Suivre les requêtes et comprendre les lenteurs

La vue des requêtes me permet d’examiner l’activité HTTP : méthode utilisée, statut de réponse, durée et lien vers la trace correspondante.

Liste des requêtes HTTP dans BlazorTelemetry, avec routes et adresses IP floutées

Je peux repérer une requête lente ou en erreur, puis poursuivre l’analyse à partir de sa trace. Les routes et les adresses IP sont floutées.

Si une opération devient lente, je peux examiner sa trace, repérer un traitement coûteux, puis consulter les logs corrélés. Les métriques me permettent ensuite de déterminer si le phénomène est ponctuel ou récurrent.

Détail d’une trace BlazorTelemetry et chronologie des traitements, avec ressources floutées

La chronologie d’une trace rend visibles les traitements et leurs durées. Je peux suivre les handlers métier et localiser les étapes qui méritent une investigation.

Interroger la télémétrie avec un serveur MCP

BlazorTelemetry expose également un serveur MCP, protégé par une clé dédiée.

Il permet à un assistant autorisé de consulter les données de diagnostic en lecture seule : rechercher des erreurs, filtrer les logs, retrouver une trace et ses événements associés, ou examiner une série de métriques.

Je peux ainsi formuler des demandes comme :

« Recherche les erreurs de cette instance sur la dernière heure et retrouve les traces associées. »

Ou :

« Examine les traces les plus lentes sur cette période pour identifier les opérations concernées. »

Le MCP fournit un accès structuré aux observations. Je peux m’en servir pour accélérer une investigation, puis vérifier les hypothèses à partir des traces et des mesures disponibles.

Health : voir l’état de tous mes clients sur un seul écran

BlazorTelemetry me donne le détail technique d’une instance. Avec Health, je cherche d’abord à répondre à une autre question :

Quel client nécessite mon attention maintenant ?

Le site de surveillance centralise les instances dans un tableau de bord commun. Mes partenaires et moi pouvons y repérer les incidents sans ouvrir chaque application séparément.

Tableau de bord Health présentant la disponibilité des instances et le suivi des imports, avec clients et logos floutés

Health rassemble la disponibilité, la latence, l’état des imports, la taille des bases, les versions et la date du dernier rapport. Les noms, logos et URL des clients sont floutés.

L’accès repose sur des comptes individuels et une authentification par identifiant et code temporaire TOTP. Je peux attribuer des comptes lecteurs aux partenaires qui ont besoin de consulter l’état du parc.

Un appairage automatique des instances

Lorsque la fonction est activée en production, l’instance AuditStock lance automatiquement son appairage avec Health au démarrage.

Health authentifie la demande, puis rappelle l’instance sur son endpoint de santé. Cette vérification permet de contrôler son identité et son état avant de l’enregistrer dans la surveillance.

sequenceDiagram
    participant Instance as Instance AuditStock
    participant Health as Site Health
    participant Dashboard as Tableau de bord

    Instance->>Health: Demande d’appairage authentifiée
    Health->>Instance: Vérification de l’identité et de l’état
    Instance-->>Health: Réponse de santé
    Health->>Dashboard: Enregistrement ou actualisation

    loop Surveillance périodique
        Health->>Instance: Contrôle de disponibilité
        Instance-->>Health: État courant
        Health->>Instance: Collecte du rapport
        Instance-->>Health: Informations de suivi
        Health->>Dashboard: Actualisation des indicateurs
    end

Si le service de surveillance est temporairement inaccessible, l’instance réessaie avec un délai progressif.

Je réduis ainsi les opérations manuelles nécessaires pour intégrer une nouvelle instance au tableau de bord.

Des alertes adaptées aux différents problèmes

Je surveille plusieurs situations, car une application accessible peut malgré tout rencontrer un problème métier.

Dans Health, je distingue trois catégories d’incidents :

Incident Détection prévue par défaut
Indisponibilité Trois échecs consécutifs des contrôles effectués chaque minute.
Échec de collecte Trois échecs consécutifs de récupération des rapports, collectés toutes les cinq minutes.
Retard d’import Absence de mise à jour au-delà de deux heures de temps surveillé pour les familles d’import suivies.

Le suivi des imports couvre les ventes, les livraisons, les achats et les réceptions.

Je tiens compte des plages de surveillance : par défaut, du lundi au vendredi, de 8 h à 18 h, dans le fuseau Europe/Paris. Un arrêt normal des échanges pendant la nuit ne doit pas être traité comme un blocage en pleine journée.

Lorsqu’une instance est indisponible, je suspends les alertes de retard d’import jusqu’à la réception d’un rapport valide. Cela évite de multiplier les alertes secondaires pour une même panne.

Tableau de bord, e-mails et alertes techniques

Health affiche les incidents dans le tableau de bord et envoie les notifications par e-mail via Brevo. Le cycle comprend une alerte initiale, des rappels périodiques — toutes les quatre heures par défaut — et une notification de rétablissement.

BlazorTelemetry complète ce dispositif avec des règles techniques : erreurs dans les logs, taux d’erreur des traces, latence au percentile 95, dépassement d’un seuil de métrique ou absence de télémétrie.

Selon la configuration, ces notifications techniques peuvent être envoyées par webhook, SMTP ou ntfy.

Je dispose donc de deux niveaux complémentaires : la surveillance opérationnelle du parc et les alertes détaillées sur le comportement de chaque application.

De l’alerte au diagnostic

Imaginons qu’un client ne reçoive plus ses données d’achat.

L’application peut rester accessible. Le problème apparaîtra pourtant dans Health lorsque le retard d’import dépassera le seuil prévu.

Je peux alors ouvrir la télémétrie de cette instance, rechercher les erreurs du connecteur, examiner les traces concernées et utiliser le serveur MCP pour faciliter l’analyse.

flowchart LR
    Detection["Health détecte un retard"] --> Alert["Tableau de bord et e-mail"]
    Alert --> Investigation["Consultation de BlazorTelemetry"]
    Investigation --> MCP["Recherche assistée via MCP"]
    MCP --> Cause["Identification de la cause"]
    Cause --> Action["Intervention"]
    Action --> Recovery["Contrôle du rétablissement"]

C’est cette continuité que je recherche dans l’architecture d’AuditStock : pouvoir distribuer une version, adapter les fonctionnalités à chaque client, détecter les anomalies et disposer des éléments nécessaires pour les comprendre.

À mesure que le parc grandit, je veux que mes partenaires et moi conservions une vue lisible de son état, tout en pouvant descendre jusqu’au détail d’une opération lorsque la situation l’exige.