ADR-08 — Monitoring via Signoz + OpenTelemetry
Signoz self-hosté comme stack observabilité unifiée (traces, metrics, logs)
Statut : Accepté Date : 2026-04 Sujet : Stack d'observabilité (traces, metrics, logs, dashboards, alertes)
Contexte
L'ancien projet n'avait aucune observabilité au-delà de console.log. Quand un bug survenait :
- Impossible de voir la chaîne de requêtes qui l'a produit
- Impossible de savoir si Mews était lent cette nuit-là
- Impossible de corréler un spike 500 avec un deploy
- Pas d'alerte automatique — on découvrait les pannes par les users
Pour un projet qui vise plusieurs hôtels en production, on ne peut pas se permettre ça. Il faut un single pane of glass pour voir l'état de la plateforme en temps réel.
Alternatives considérées
Option 1 — Stack custom (Prometheus + Grafana + Loki + Tempo)
Le stack "CNCF pur".
Pour :
- 100 % open source, maîtrise totale
- Outillage mature
Contre :
- 4 services à déployer et interconnecter (Prometheus, Grafana, Loki, Tempo)
- Chaque outil a sa propre UI
- Corrélation traces ↔ logs ↔ metrics nécessite config manuelle
- Courbe d'apprentissage
Option 2 — Datadog / New Relic / Sentry
SaaS payants.
Pour :
- Ultra-rapide à mettre en place
- Support
Contre :
- Coût qui scale avec le volume (Datadog à $$ /mois à peu de volume)
- Données hébergées à l'étranger — question RGPD pour un produit hôtelier européen
- Vendor lock-in
Option 3 — Signoz (retenu)
Stack d'observabilité unifiée open source, basé sur OpenTelemetry nativement, stockage ClickHouse.
Pour :
- Un seul service pour traces, metrics, logs, dashboards, alertes
- OpenTelemetry natif — instrumentation standard, pas de SDK propriétaire
- UI unique pour corréler traces ↔ logs ↔ metrics
- Self-host (Docker Compose ou K8s) — données chez nous
- Alerting intégré (PagerDuty, Slack, email)
- Gratuit à l'usage, payant seulement pour les fonctionnalités entreprise (retention longue, RBAC fine)
- Performances ClickHouse → scale à plusieurs millions d'events/jour sans broncher
Contre :
- Plus récent que Grafana Cloud/Datadog — communauté plus petite
- Self-host = maintenance (upgrade, backup)
- ClickHouse = nouveau runtime à comprendre si incident
Décision
Signoz self-hosté en Docker Compose, instrumenté via OpenTelemetry sur toutes les briques Bell.
Architecture
apps/server (Elysia) ──┐
apps/worker (BullMQ) ──┤
apps/pwa (Next.js) ──┤
apps/dashboard (Next.js) ──┼── OTLP gRPC ──► Signoz Collector ──► ClickHouse ──► Signoz UI
BullMQ Redis ──┤ │
Postgres (pgBouncer) ──┘ └──► AlertmanagerInstrumentation
Package dédié packages/observability/ :
packages/observability/src/
├── index.ts → export `initOtel(serviceName)`
├── tracer.ts → setup BatchSpanProcessor + OTLPExporter
├── logger.ts → pino + pino-opentelemetry-transport
├── metrics.ts → business metrics (MeterProvider)
└── instrumentations/
├── elysia.ts → middleware tracing pour routes Elysia
├── drizzle.ts → spans pour queries DB
├── bullmq.ts → spans pour jobs
└── fetch.ts → spans pour appels HTTP sortants (Mews, Stripe, Reacher)Chaque app appelle initOtel(...) au boot :
// apps/server/src/index.ts
import { initOtel } from "@bell/observability";
initOtel({ serviceName: "bell-server", environment: process.env.NODE_ENV });
// ... reste du code ElysiaRED metrics automatiques
Toutes les routes Elysia exposent automatiquement :
- Rate — requêtes/seconde par route
- Errors — taux d'erreurs (2xx/4xx/5xx)
- Duration — p50, p95, p99 par route
Visibles dans Signoz sans config supplémentaire.
Business metrics (custom)
En plus des RED metrics techniques, on trace des métriques métier dans les services :
// packages/api/src/modules/cardex/cardex.service.ts
import { meter } from "@bell/observability";
const checkInCounter = meter.createCounter("bell.check_in.completed", {
description: "Nombre de check-ins confirmés",
});
export async function confirmGuestArrival(orgId: string, guestId: string) {
// ... logique métier
checkInCounter.add(1, { organizationId: orgId });
}Dashboards Signoz de base (jour 1)
Quatre dashboards créés au démarrage :
- Overview — requêtes/s globales, erreurs globales, durée p95, santé des workers
- API par module — breakdown par module (cardex, rooms, booking, ai, payment)
- PMS Integrations — latence Mews, échecs de sync, bridges en erreur
- Business — check-ins/jour par hôtel, revenus Stripe, commandes par service
Les dashboards sont versionnés en JSON dans packages/observability/dashboards/ et appliqués via l'API Signoz au provisioning (pas de config manuelle cliquée).
Alertes critiques (jour 1)
| Alerte | Condition | Canal |
|---|---|---|
| API error rate high | > 5 % 5xx sur 5 min | Slack #alerts + email on-call |
| BullMQ queue backing up | > 100 jobs waiting depuis 5 min | Slack |
| Worker down | worker heartbeat miss > 2 min | Slack + page |
| Postgres connections exhausted | > 90 % du pool utilisé | Slack |
| Mews sync fails | 3 échecs consécutifs sur un hôtel | Slack + email admin |
| Stripe webhook signature fail | > 0 (signale une attaque ou une mauvaise config) | Slack + email |
Logs structurés
Pino partout, JSON, auto-forwarded vers Signoz via pino-opentelemetry-transport.
Niveaux :
error— quelque chose a cassé, human action possiblewarn— anomalie non bloquante (Mews lent, retry en cours)info— event métier (check-in confirmé, commande créée) — reste minime pour éviter le bruitdebug— dev only
Chaque log porte automatiquement le trace_id → cliquer sur un log dans Signoz ouvre la trace complète.
RGPD
Les traces et logs ne contiennent jamais :
- Email complet (on log le hash, ou les 4 premiers caractères)
- Numéro de téléphone
- Adresse
- Moyen de paiement
Un linter custom no-pii-in-logs (à écrire comme ESLint rule ou biome plugin) détecte les patterns à risque dans les call sites de logger.*.
Déploiement
Dev
Signoz inclus dans docker-compose.yml :
# docker-compose.yml (dev)
services:
signoz:
image: signoz/signoz-otel-collector:latest
ports: ["4317:4317", "4318:4318"]
signoz-ui:
image: signoz/frontend:latest
ports: ["3301:3301"]
clickhouse:
image: clickhouse/clickhouse-server:latestAccès UI : http://localhost:3301
Prod
Signoz tourne sur un VPS dédié (ou Kubernetes), séparé du serveur Bell. Pas sur le même host que Postgres/Redis pour ne pas croiser les SLA.
Conséquences
Positives :
- Visibilité complète de la plateforme dès le jour 1
- Zéro dépendance externe payante
- Données chez nous — RGPD clair
- Ops team peut investiguer un incident sans devoir grep les logs SSH
- Dashboards versionnés = reproductibles
Négatives :
- Maintenance Signoz (upgrade ClickHouse, backup) — compensé par une doc
operations/monitoring.mdx - Disque ClickHouse grossit avec les traces — retention policy à 30 jours par défaut (configurable)
- Instrumentation fine du code (~2–3 heures par module) — coût one-shot
Métriques à surveiller
- Volume de traces/jour (cible : < 10M/jour, sinon ajuster le sampling à 10 %)
- Taille du disque ClickHouse (cible : < 100 GB avec retention 30j)
- Latence Signoz Collector (cible : < 50ms p95)
- Nombre d'alertes déclenchées/jour (cible : < 5, au-delà l'alerting est bruyant)
- Pourcentage d'incidents détectés par Signoz avant signalement user (cible : > 80 %)