My App

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)       ──┘                                     └──► Alertmanager

Instrumentation

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 Elysia

RED 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 :

  1. Overview — requêtes/s globales, erreurs globales, durée p95, santé des workers
  2. API par module — breakdown par module (cardex, rooms, booking, ai, payment)
  3. PMS Integrations — latence Mews, échecs de sync, bridges en erreur
  4. 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)

AlerteConditionCanal
API error rate high> 5 % 5xx sur 5 minSlack #alerts + email on-call
BullMQ queue backing up> 100 jobs waiting depuis 5 minSlack
Worker downworker heartbeat miss > 2 minSlack + page
Postgres connections exhausted> 90 % du pool utiliséSlack
Mews sync fails3 échecs consécutifs sur un hôtelSlack + 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 possible
  • warn — anomalie non bloquante (Mews lent, retry en cours)
  • info — event métier (check-in confirmé, commande créée) — reste minime pour éviter le bruit
  • debug — 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:latest

Accè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 %)

On this page