LGTM Stack : l'observabilité complète de mon homelab


Après des mois à construire mon homelab à l’aveugle, j’ai décidé de mettre en place une observabilité digne d’un environnement de production. Le genre de setup qu’on trouve chez Scaleway, Datadog, ou dans n’importe quelle équipe SRE sérieuse.

Le résultat : une stack LGTM complète avec un dashboard Golden Signals qui me donne une vue instantanée de la santé de toute mon infra.

C’est quoi la stack LGTM ?

LGTM, c’est l’acronyme de l’écosystème Grafana Labs :

L → Loki      (Logs)          "Le grep distribué pour vos logs"
G → Grafana   (Visualisation) "Le cerveau qui affiche tout"
T → Tempo     (Traces)        "Le parcours complet d'une requête"
M → Mimir     (Métriques)     "Prometheus à l'échelle" (on utilise Prometheus directement)

C’est l’alternative open-source à Datadog, New Relic, ou Splunk. La grosse différence : tout est self-hosted, les données restent chez moi, et ça coûte 0 euros.

Pourquoi ces 3 piliers ?

L’observabilité repose sur 3 signaux complémentaires :

┌──────────────────────────────────────────────────────────────┐
│                    OBSERVABILITÉ                              │
│                                                              │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐                │
│  │ MÉTRIQUES│   │   LOGS   │   │  TRACES  │                │
│  │          │   │          │   │          │                │
│  │ "QUOI"   │   │ "POURQUOI│   │  "OÙ"    │                │
│  │          │   │          │   │          │                │
│  │ Combien  │   │ Qu'est-ce│   │ Quel     │                │
│  │ de req/s?│   │ qui s'est│   │ chemin   │                │
│  │ Quelle   │   │ passé?   │   │ prend la │                │
│  │ latence? │   │ Quelle   │   │ requête? │                │
│  │ % erreur?│   │ erreur?  │   │ Où est   │                │
│  │          │   │          │   │ le goulot│                │
│  └────┬─────┘   └────┬─────┘   └────┬─────┘                │
│       │              │              │                        │
│       └──────────────┴──────────────┘                        │
│                      │                                       │
│               ┌──────┴──────┐                                │
│               │   GRAFANA   │                                │
│               │ (Dashboard) │                                │
│               └─────────────┘                                │
└──────────────────────────────────────────────────────────────┘

Un exemple concret :

  1. Les métriques me disent : “le taux d’erreur vient de passer à 45%”
  2. Les logs me disent : “erreur 500 : connection refused to Redis”
  3. Les traces me disent : “la requête POST /api/v1/ai/chat attend 30s sur l’appel Redis avant de timeout”

Sans les 3 ensemble, on est aveugle.

Architecture déployée

Voici ce qu’on a déployé dans le cluster :

┌─────────────────────────────────────────────────────────┐
│  Namespace: monitoring                                   │
│                                                         │
│  ┌─────────────┐  ┌──────────┐  ┌──────────┐          │
│  │ Prometheus  │  │  Grafana  │  │  Tempo   │          │
│  │ (métriques) │  │ (visu)   │  │ (traces) │          │
│  │ :9090       │  │ :3000    │  │ :4317    │          │
│  └──────┬──────┘  └────┬─────┘  └────┬─────┘          │
│         │              │              │                  │
│  ┌──────┴──────┐       │              │                  │
│  │ Alertmanager│       │              │                  │
│  │ :9093       │       │              │                  │
│  └─────────────┘       │              │                  │
│                        │              │                  │
│  ┌─────────────┐       │              │                  │
│  │   Loki      ├───────┘              │                  │
│  │ (logs)      │                      │                  │
│  │ :3100       │                      │                  │
│  └─────────────┘                      │                  │
└───────────────────────────────────────┘                  │

┌──────────────────────────────────────────────────────────┤
│  Namespace: console                                      │
│                                                          │
│  ┌─────────────────────┐                                 │
│  │   console-api (Go)  │                                 │
│  │                     │                                 │
│  │  GET /metrics  ─────┼──── Prometheus scrape (pull)    │
│  │  OTel traces   ─────┼──── Tempo (gRPC push)          │
│  │  stdout logs   ─────┼──── Loki (via DaemonSet)       │
│  └─────────────────────┘                                 │
└──────────────────────────────────────────────────────────┘

1. Prometheus : les métriques

Comment ça marche

Prometheus fonctionne en mode pull : il va chercher les métriques à intervalle régulier (toutes les 15 secondes) en faisant un GET /metrics sur chaque target.

C’est l’inverse de Datadog/StatsD qui fonctionnent en push (l’app envoie les métriques). L’avantage du pull :

  • Prometheus contrôle la fréquence → pas de surcharge
  • Si une app est down, Prometheus le détecte (scrape échoue)
  • On peut curl /metrics pour debugger directement

Les types de métriques

Prometheus a 4 types fondamentaux :

Counter - ne fait que monter (comme un compteur kilométrique)

tom_lab_console_http_requests_total{method="GET", path="/api/v1/apps", status="2xx"} 42

Pour obtenir un débit, on utilise rate() : “combien de requêtes par seconde ?”

Gauge - monte et descend (comme un thermomètre)

go_goroutines 20
tom_lab_console_active_sse_connections 3

Histogram - répartition des valeurs dans des tranches (buckets)

tom_lab_console_http_request_duration_seconds_bucket{le="0.005"} 12    # 12 requêtes en <5ms
tom_lab_console_http_request_duration_seconds_bucket{le="0.01"}  18    # 18 requêtes en <10ms
tom_lab_console_http_request_duration_seconds_bucket{le="0.025"} 25    # 25 requêtes en <25ms
tom_lab_console_http_request_duration_seconds_bucket{le="+Inf"}  30    # 30 requêtes au total
tom_lab_console_http_request_duration_seconds_sum   4.5                # somme des durées
tom_lab_console_http_request_duration_seconds_count 30                 # nombre total

Grâce aux buckets, on peut calculer des percentiles : “99% des requêtes prennent moins de Xms”

Summary - similaire à l’histogram mais calcule les quantiles côté client (moins flexible, rarement utilisé)

Le code Go pour les métriques

Voici comment j’ai instrumenté le backend. J’utilise un registre custom au lieu du registre global par défaut :

// metrics.go - Création du registre et des métriques

func New(cfg Config) (Collector, error) {
    // Registre custom (pas le global)
    registry := prometheus.NewRegistry()
    registry.MustRegister(collectors.NewGoCollector())
    registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))

    c := &collector{registry: registry}

    // Counter : nombre total de requêtes HTTP
    c.httpRequestsTotal = promauto.With(registry).NewCounterVec(
        prometheus.CounterOpts{
            Namespace: "tom_lab",
            Subsystem: "console",
            Name:      "http_requests_total",
            Help:      "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},  // Labels
    )

    // Histogram : durée des requêtes HTTP
    c.httpRequestDuration = promauto.With(registry).NewHistogramVec(
        prometheus.HistogramOpts{
            Namespace: "tom_lab",
            Subsystem: "console",
            Name:      "http_request_duration_seconds",
            Help:      "HTTP request duration in seconds",
            Buckets:   prometheus.DefBuckets,  // 5ms, 10ms, 25ms, 50ms...
        },
        []string{"method", "path"},
    )

    return c, nil
}

Le middleware HTTP enregistre chaque requête automatiquement :

// middleware.go - Intercepte chaque requête pour mesurer

func HTTPMiddleware(collector Collector) mux.MiddlewareFunc {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()

            // Wrapper pour capturer le status code
            wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}

            next.ServeHTTP(wrapped, r)

            duration := time.Since(start)
            collector.RecordHTTPRequest(r.Method, r.URL.Path, wrapped.statusCode, duration)
        })
    }
}

Le bug du registre (et comment je l’ai trouvé)

Premier problème : après avoir tout déployé, le dashboard Grafana affichait “No data” partout. Les métriques existaient dans le code, mais Prometheus ne les voyait pas.

Le diagnostic :

# Prometheus scrape OK, target "up"
$ kubectl exec -n monitoring svc/prometheus -- wget -qO- \
    'http://localhost:9090/api/v1/targets' | jq '.data.activeTargets[] | select(.labels.job=="console-api")'
# → state: "up", lastScrape: "2s ago"

# Mais les métriques custom n'existent pas
$ curl http://console-api:8080/metrics | grep tom_lab
# → rien !

# Seulement les métriques Go runtime
$ curl http://console-api:8080/metrics | head -5
# → go_gc_duration_seconds, go_goroutines, etc.

Le problème : Dans main.go, le handler /metrics utilisait promhttp.Handler() qui sert le registre global par défaut de Prometheus. Mais toutes mes métriques custom étaient enregistrées dans un registre custom (prometheus.NewRegistry()) via promauto.With(registry).

Deux registres, zéro communication :

Registre GLOBAL (défaut)     Registre CUSTOM (le mien)
├── go_goroutines            ├── tom_lab_console_http_requests_total
├── go_gc_*                  ├── tom_lab_console_http_request_duration_seconds
├── process_*                ├── tom_lab_console_deployments_total
│                            ├── tom_lab_console_ai_requests_total
│                            └── ...

GET /metrics → promhttp.Handler()
           → sert le registre GLOBAL
           → métriques custom invisibles !

Le fix en 3 lignes :

// AVANT (main.go) - sert le mauvais registre
r.Handle("/metrics", promhttp.Handler())

// APRÈS - sert le bon registre via l'interface Collector
r.Handle("/metrics", metricsCollector.Handler())

// La méthode Handler() dans metrics.go
func (c *collector) Handler() http.Handler {
    return promhttp.HandlerFor(c.registry, promhttp.HandlerOpts{})
}

J’ai aussi ajouté collectors.NewGoCollector() et NewProcessCollector() au registre custom pour ne pas perdre les métriques runtime Go.

Après le fix et redeploy :

$ curl http://console-api:8080/metrics | grep tom_lab | head -5
tom_lab_console_http_requests_total{method="GET",path="/api/v1/apps",status="2xx"} 1
tom_lab_console_http_requests_total{method="POST",path="/api/v1/auth/login",status="2xx"} 1
tom_lab_console_http_request_duration_seconds_bucket{method="GET",path="/api/v1/cluster/nodes",le="0.025"} 2
...

ServiceMonitor pour Prometheus Operator

Pour que Prometheus découvre automatiquement notre API, on utilise un ServiceMonitor (CRD de Prometheus Operator) :

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: console-api
  namespace: monitoring
  labels:
    release: kube-prometheus-stack  # Label que Prometheus Operator surveille
spec:
  namespaceSelector:
    matchNames: ["console"]
  selector:
    matchLabels:
      app: console-api
  endpoints:
    - port: http
      path: /metrics
      interval: 15s

Prometheus Operator détecte ce ServiceMonitor, génère la config de scrape, et Prometheus commence à collecter nos métriques automatiquement.

2. OpenTelemetry : le tracing distribué

Pourquoi du tracing ?

Les métriques te disent “la latence est élevée”. Les logs te disent “il y a des erreurs”. Mais où exactement est le problème dans le parcours d’une requête ?

Le tracing répond à cette question en suivant chaque requête de bout en bout :

Requête: POST /api/v1/ai/chat

├── [Span 1] HTTP Handler (2.5s)
│   ├── [Span 2] Auth Middleware - ValidateToken (0.2ms)
│   ├── [Span 3] Redis GET conversation (1.2ms)
│   ├── [Span 4] Claude API Call (2.3s)     ← le goulot !
│   └── [Span 5] Redis SET response (0.8ms)

Total: 2.5s

D’un coup d’œil, on voit que c’est l’appel à l’API Claude qui prend 95% du temps. Sans tracing, on aurait fouillé les logs pendant des heures.

Les concepts OTel

Trace : le parcours complet d’une requête à travers le système Span : une opération unitaire (un appel HTTP, une query Redis, un traitement) Span Context : l’identifiant unique (trace_id + span_id) qui lie les spans entre eux

Trace ID: abc123def456

├── Span ID: 001 (parent: none) → HTTP POST /api/v1/ai/chat
│   ├── Span ID: 002 (parent: 001) → Auth Middleware
│   ├── Span ID: 003 (parent: 001) → Redis GET
│   ├── Span ID: 004 (parent: 001) → Claude API
│   └── Span ID: 005 (parent: 001) → Redis SET

La propagation de contexte

En microservices, une requête traverse plusieurs services. Pour garder la trace, on propage le contexte via les headers HTTP (standard W3C TraceContext) :

Service A → Service B
Headers:
  traceparent: 00-abc123def456-001-01
  tracestate: ...

Chaque service lit le header, crée ses propres spans en rattachant au trace_id parent, et propage le contexte au service suivant.

Implémentation dans notre Go backend

L’initialisation du tracing :

// tracing.go - Pipeline OpenTelemetry

func Init(ctx context.Context, serviceName, tempoEndpoint string) (func(context.Context) error, error) {
    // Connexion gRPC vers Tempo
    conn, _ := grpc.NewClient(tempoEndpoint,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )

    // Exporter OTLP (OpenTelemetry Protocol)
    exporter, _ := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))

    // Resource attributes (qui suis-je ?)
    res, _ := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName(serviceName),      // "tom-lab-console"
            semconv.ServiceVersion("1.0.0"),
        ),
        resource.WithHost(),
        resource.WithProcess(),
    )

    // TracerProvider avec batch export
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter, sdktrace.WithBatchTimeout(5*time.Second)),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
    )

    // Définir comme TracerProvider global
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},  // W3C standard
        propagation.Baggage{},
    ))

    return tp.Shutdown, nil
}

L’instrumentation est automatique grâce au middleware otelmux :

// main.go - Un seul middleware pour instrumenter toutes les routes
r := mux.NewRouter()
r.Use(otelmux.Middleware("tom-lab-console"))  // ← Auto-trace chaque requête HTTP

Chaque requête HTTP génère automatiquement un span avec :

  • Méthode HTTP, path, status code
  • Durée
  • Attributs du service (nom, version, host)

Le bug du schéma URL

À la première tentative, le tracing plantait au démarrage :

ERROR: conflicting Schema URL:
  https://opentelemetry.io/schemas/1.39.0
  and
  https://opentelemetry.io/schemas/1.21.0

Le problème : resource.Merge(resource.Default(), customResource) essayait de fusionner deux resources avec des versions de schéma différentes. La resource par défaut utilisait le schéma OTel v1.39.0 (dernière version du SDK), mais semconv/v1.21.0 définissait le schéma v1.21.0.

Le fix : utiliser resource.New() au lieu de resource.Merge() pour éviter le conflit :

// AVANT - conflit de schéma
res, _ := resource.Merge(
    resource.Default(),  // ← schéma v1.39.0
    resource.NewWithAttributes(semconv.SchemaURL, ...)  // ← schéma v1.21.0
)

// APRÈS - pas de merge, pas de conflit
res, _ := resource.New(ctx,
    resource.WithAttributes(
        semconv.ServiceName(serviceName),
        semconv.ServiceVersion("1.0.0"),
    ),
    resource.WithHost(),
    resource.WithProcess(),
)

Tempo : le backend de traces

Tempo est le backend Grafana Labs pour stocker les traces. Il est optimisé pour :

  • Ingestion massive (OTLP gRPC)
  • Stockage efficient (object storage ou local)
  • Intégration native avec Grafana (clic sur un trace_id dans les logs → vue trace)

Déploiement simple dans le cluster :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tempo
  namespace: monitoring
spec:
  template:
    spec:
      containers:
      - name: tempo
        image: grafana/tempo:latest
        ports:
        - containerPort: 4317   # OTLP gRPC (réception traces)
        - containerPort: 3200   # HTTP API (query par Grafana)

3. Loki : les logs

Collecte des logs

Loki collecte les logs de tous les pods du cluster via un DaemonSet (un agent sur chaque nœud) qui lit les fichiers de log des containers.

Ce qui est génial avec Loki, c’est qu’on peut corréler logs et traces. Si notre application Go écrit le trace ID dans les logs :

logger.Info("request processed",
    zap.String("trace_id", span.SpanContext().TraceID().String()),
    zap.String("path", r.URL.Path),
    zap.Int("status", statusCode),
)

Alors dans Grafana, on peut cliquer sur un log et sauter directement à la trace correspondante dans Tempo.

LogQL : le langage de requête

Comme PromQL pour les métriques, Loki a LogQL pour les logs :

# Tous les logs du console-api
{namespace="console", app="console-api"}

# Seulement les erreurs
{namespace="console"} |= "error" | json | level="error"

# Taux d'erreurs par seconde
count_over_time({namespace="console"} |= "error" [5m]) / count_over_time({namespace="console"} [5m])

4. Le Dashboard Golden Signals

C’est quoi les Golden Signals ?

Les 4 Golden Signals viennent du livre “Site Reliability Engineering” de Google. Ce sont les 4 métriques essentielles pour monitorer n’importe quel service :

SignalQuestionMétrique
TrafficCombien de demande je reçois ?Requêtes par seconde
LatencyCombien de temps pour répondre ?p50, p95, p99 de durée
ErrorsCombien de requêtes échouent ?Taux d’erreur 5xx (%)
SaturationMon service est-il saturé ?Goroutines, CPU, mémoire

Si ces 4 signaux sont verts, le service va bien. Si un seul est rouge, il y a un problème.

Les requêtes PromQL du dashboard

Traffic - Request Rate (req/s)

sum(rate(tom_lab_console_http_requests_total[5m]))

rate() calcule le taux de changement par seconde du counter sur une fenêtre de 5 minutes.

Traffic par endpoint

sum by (path) (rate(tom_lab_console_http_requests_total[5m]))

Le by (path) découpe le résultat par endpoint pour voir lequel reçoit le plus de trafic.

Latency - p50, p95, p99

histogram_quantile(0.99, sum(rate(tom_lab_console_http_request_duration_seconds_bucket[5m])) by (le))

histogram_quantile() utilise les buckets de l’histogram pour calculer : “99% des requêtes prennent moins de X secondes”.

Errors - Taux d’erreur (%)

100 * sum(rate(tom_lab_console_http_requests_total{status="5xx"}[5m]))
      /
      sum(rate(tom_lab_console_http_requests_total[5m]))

Division : requêtes 5xx / total des requêtes, multiplié par 100 pour avoir un pourcentage.

Saturation - Goroutines

go_goroutines{job="console-api"}

Les goroutines Go sont un excellent indicateur de saturation. Normalement ~20-50. Si ça monte à 5000+, il y a des requêtes qui s’accumulent.

Résultat dans Grafana

Le dashboard “Golden Signals - Tom Lab Console” avec 4 rangées :

┌─────────────────────────────────────────────────────────┐
│  TRAFFIC                                                │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │ Request Rate │  │  By Endpoint │                    │
│  │  0.04 req/s  │  │  /apps: 40%  │                    │
│  │    ___/──    │  │  /auth: 30%  │                    │
│  └──────────────┘  └──────────────┘                    │
├─────────────────────────────────────────────────────────┤
│  LATENCY                                                │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │   p50/p95    │  │  p99 Latency │                    │
│  │  p50: 12ms   │  │   2.3s       │                    │
│  │  p95: 450ms  │  │   (login)    │                    │
│  └──────────────┘  └──────────────┘                    │
├─────────────────────────────────────────────────────────┤
│  ERRORS                                                 │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │  Error Rate  │  │  By Status   │                    │
│  │    0.0%      │  │  2xx: 85%    │                    │
│  │   (healthy)  │  │  4xx: 15%    │                    │
│  └──────────────┘  └──────────────┘                    │
├─────────────────────────────────────────────────────────┤
│  SATURATION                                             │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │ Goroutines   │  │  Concurrent  │                    │
│  │     20       │  │  Deploys: 0  │                    │
│  │   (normal)   │  │  SSE: 0      │                    │
│  └──────────────┘  └──────────────┘                    │
└─────────────────────────────────────────────────────────┘

5. L’interface Collector en Go

Le pattern Interface + Implémentation

Un point d’architecture important : on ne manipule jamais les métriques Prometheus directement dans le code métier. On passe par une interface :

type Collector interface {
    RecordHTTPRequest(method, path string, status int, duration time.Duration)
    RecordK8sOperation(operation string, success bool, duration time.Duration)
    RecordDeployment(chart string, success bool, duration time.Duration)
    RecordAIRequest(action string, duration time.Duration)
    RecordAITokens(action string, tokens int)
    SetActiveConnections(count int)
    SetConcurrentDeployments(count int)
    RecordRedisOperation(operation string, success bool, duration time.Duration)
    RecordNPMOperation(operation string, success bool, duration time.Duration)
    Handler() http.Handler
    Flush() error
}

Avec deux implémentations :

collector - la vraie implémentation avec Prometheus

func (c *collector) RecordHTTPRequest(method, path string, status int, duration time.Duration) {
    c.httpRequestsTotal.WithLabelValues(method, path, statusToString(status)).Inc()
    c.httpRequestDuration.WithLabelValues(method, path).Observe(duration.Seconds())
}

noopCollector - ne fait rien (quand les métriques sont désactivées)

func (n *noopCollector) RecordHTTPRequest(method, path string, status int, duration time.Duration) {}

Pourquoi ce pattern ?

  1. Tests : dans les tests unitaires, on injecte noopCollector pour ne pas avoir besoin de Prometheus
  2. Configuration : on peut désactiver les métriques avec METRICS_ENABLED=false
  3. Découplage : le code métier ne connaît pas Prometheus, juste l’interface Collector
  4. Extensibilité : on pourrait ajouter une implémentation Datadog sans changer le code métier

C’est le principe SOLID D (Dependency Inversion) : dépendre d’abstractions, pas d’implémentations.

6. En production : comment j’utilise tout ça

Scénario 1 : Debug d’un problème de latence

1. Dashboard Golden Signals → p99 latency spike à 30s
2. Panel "Latency par endpoint" → /api/v1/ai/chat est le coupable
3. Grafana → Explore → Tempo → chercher traces avec duration > 10s
4. Trace trouvée → le span "Claude API Call" prend 28s
5. → Problème côté API Claude, pas côté nous

Scénario 2 : Erreurs après un déploiement

1. Dashboard Golden Signals → Error rate passe à 15%
2. Panel "Errors par status" → 401 Unauthorized en masse
3. Grafana → Explore → Loki → {namespace="console"} |= "401"
4. Logs → "invalid token" sur toutes les requêtes API
5. → Le déploiement a régénéré les secrets JWT (pas en env var)
6. Fix : patcher le secret K8s avec JWT_SECRET stable

Scénario 3 : Saturation mémoire

1. Dashboard Golden Signals → go_goroutines monte à 2000
2. Panel "Active SSE Connections" → 200 (normalement < 10)
3. Grafana → Explore → Loki → {namespace="console"} |= "SSE"
4. Logs → connections SSE qui ne se ferment jamais
5. → Bug dans le frontend qui ne close pas les EventSource

Ce que j’ai appris

1. Le registre Prometheus, c’est important. J’ai perdu 2 heures à debugger pourquoi mes métriques n’apparaissaient pas. Le registre custom vs défaut, c’est un piège classique en Go.

2. OTel c’est puissant mais complexe. Le setup initial demande de comprendre les concepts (TracerProvider, Exporter, Resource, Propagator, Sampler). Une fois en place, c’est magique.

3. Les 3 piliers ensemble valent 10x chacun seul. Métriques seules = on sait QUOI. Logs seuls = on sait POURQUOI. Traces seules = on sait OÙ. Les 3 ensemble = on sait TOUT.

4. Les Golden Signals suffisent pour 95% des cas. Pas besoin de 50 dashboards. 4 métriques bien choisies couvrent l’essentiel.

5. L’observabilité doit être là dès le jour 1. J’aurais dû mettre ça en place bien plus tôt. Chaque incident qu’on debug sans observabilité, c’est du temps perdu.


Prochaine étape : ajouter des alertes automatiques (Alertmanager + Slack/Telegram) pour être prévenu quand un Golden Signal dépasse un seuil. Plus besoin de regarder le dashboard en permanence.

Mais ça, c’est pour un prochain article…