Aller au contenu principal
ObservabilitéOpenTelemetryMonitoringDevOpsGrafana

OpenTelemetry en 2026 : le standard de l'observabilité

30 min de lecture

Découvre OpenTelemetry, le standard open source pour les traces, métriques et logs. Comparatif avec les solutions propriétaires, instrumentation pratique, et démo complète avec la stack LGTM.

Traces, Métriques, Logs : le trio de l’observabilité

L’observabilité, c’est la capacité à comprendre l’état interne de ton système en observant ses sorties. En 2026, ça repose sur trois piliers :

Métriques

Des valeurs numériques agrégées dans le temps. Elles répondent à “combien ?” et “à quelle vitesse ?”.

http_requests_total{method="GET", status="200"} → 142857
http_request_duration_seconds{quantile="0.99"} → 0.250
system_cpu_usage → 0.45

Caractéristiques :

  • Faible coût de stockage (agrégées)
  • Idéales pour les alertes
  • Dashboards et tendances
  • Ne donnent pas le “pourquoi”

Logs

Des événements textuels horodatés. Ils racontent ce qui s’est passé.

{
  "timestamp": "2026-03-20T08:15:30Z",
  "level": "ERROR",
  "service": "payment-api",
  "message": "Failed to process payment",
  "trace_id": "abc123def456",
  "user_id": "usr_789",
  "error": "timeout connecting to stripe API"
}

Caractéristiques :

  • Détails riches sur les événements
  • Coût de stockage élevé (texte brut)
  • Difficile à corréler sans trace_id
  • Debug et post-mortem

Traces (distributed tracing)

Le parcours complet d’une requête à travers tous les services. C’est le chaînon qui lie les métriques et les logs.

[Trace: abc123def456]
├── [Span] api-gateway (12ms)
│   ├── [Span] auth-service.validate (3ms)
│   └── [Span] payment-service.process (8ms)
│       ├── [Span] db.query (2ms)
│       └── [Span] stripe-api.charge (5ms) ← ERREUR ICI

Caractéristiques :

  • Visibilité end-to-end sur les requêtes
  • Identifie les goulots d’étranglement
  • Corrèle les services entre eux
  • Essentiel en architecture microservices

Le trio ensemble

Alerte (métrique) → "Le taux d'erreur 5xx dépasse 5%"

Trace → "Les erreurs viennent du payment-service → stripe-api"

Logs → "timeout connecting to stripe API, retry 3/3 failed"

Action → "Stripe a un incident, activer le circuit breaker"

Sans les trois, tu navigues à l’aveugle. Les métriques te disent quoi, les traces te disent , les logs te disent pourquoi.


OpenTelemetry vs solutions propriétaires

C’est quoi OpenTelemetry ?

OpenTelemetry (OTel) est un framework d’observabilité open source qui fournit :

  • Des APIs et SDKs pour instrumenter ton code
  • Un Collector pour recevoir, traiter et exporter les données
  • Un protocole standard (OTLP) pour transporter les données

C’est un projet CNCF — le 2ème plus actif après Kubernetes.

Pourquoi pas Datadog/New Relic/Dynatrace directement ?

AspectSolution propriétaireOpenTelemetry
Vendor lock-in🔴 Fort🟢 Aucun
Coût💰💰💰 ($15-27/host/mois)🟢 Gratuit (infra à gérer)
Setup initial🟢 Rapide (agent = done)🟡 Plus de config
Flexibilité🟡 Limitée à leurs features🟢 Totale
Standards🔴 Propriétaire🟢 OTLP standard
Backend🔴 Leur cloud uniquement🟢 N’importe quel backend
Communauté🟡 Forums vendeur🟢 Open source massive

Le compromis intelligent

Tu peux utiliser OpenTelemetry pour l’instrumentation et exporter vers n’importe quel backend :

App → OTel SDK → OTel Collector → Datadog (ou Grafana, ou Jaeger, ou...)

Si tu changes de backend demain, tu changes juste la config de l’exporteur. Pas une ligne de code à toucher.

L’écosystème OTel en 2026

ComposantStatusDescription
Traces✅ StableGA depuis 2023
Métriques✅ StableGA depuis 2023
Logs✅ StableGA en 2024
Profiling🟡 BetaContinu profiling
OTel Collector✅ StableLe couteau suisse
Auto-instrumentation✅ StableJava, Python, Node.js, .NET, Go

Instrumentation : auto vs manuelle

Auto-instrumentation

L’auto-instrumentation intercepte automatiquement les appels HTTP, DB, gRPC, etc. Zéro code à écrire.

Node.js :

npm install @opentelemetry/auto-instrumentations-node @opentelemetry/sdk-node
// tracing.js — à charger AVANT ton app
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');

const sdk = new NodeSDK({
  serviceName: 'payment-api',
  traceExporter: new OTLPTraceExporter({
    url: 'http://otel-collector:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://otel-collector:4318/v1/metrics',
    }),
    exportIntervalMillis: 15000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
console.log('OpenTelemetry initialized');

process.on('SIGTERM', () => {
  sdk.shutdown().then(() => process.exit(0));
});
# Lancer avec l'instrumentation
node --require ./tracing.js server.js

Python :

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install  # Installe les instrumentations détectées
# Lancer avec auto-instrumentation
opentelemetry-instrument \
  --service_name payment-api \
  --traces_exporter otlp \
  --metrics_exporter otlp \
  --exporter_otlp_endpoint http://otel-collector:4317 \
  python app.py

Java (agent JAR) :

# Télécharger l'agent
curl -LO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

# Lancer avec l'agent
java -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=payment-api \
  -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
  -jar app.jar

Kubernetes — L’opérateur OTel peut injecter l’auto-instrumentation automatiquement :

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: auto-instrumentation
  namespace: production
spec:
  exporter:
    endpoint: http://otel-collector.observability:4317
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.25"  # Échantillonner 25% des traces
  nodejs:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:latest
  python:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
# Annoter le deployment pour activer l'auto-instrumentation
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
spec:
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-nodejs: "true"  # ou inject-python, inject-java
    spec:
      containers:
        - name: payment-api
          image: ghcr.io/devopslab/payment-api:latest

Instrumentation manuelle

Pour des spans et métriques custom, il faut instrumenter manuellement.

Node.js :

const { trace, metrics, SpanStatusCode } = require('@opentelemetry/api');

// Créer un tracer
const tracer = trace.getTracer('payment-service');
// Créer un meter
const meter = metrics.getMeter('payment-service');

// Métriques custom
const paymentCounter = meter.createCounter('payments_processed_total', {
  description: 'Total number of payments processed',
});
const paymentDuration = meter.createHistogram('payment_duration_ms', {
  description: 'Payment processing duration in milliseconds',
});

async function processPayment(userId, amount) {
  // Créer un span
  return tracer.startActiveSpan('process-payment', async (span) => {
    const startTime = Date.now();
    
    try {
      // Ajouter des attributs au span
      span.setAttribute('user.id', userId);
      span.setAttribute('payment.amount', amount);
      span.setAttribute('payment.currency', 'CHF');
      
      // Span enfant pour l'appel DB
      const result = await tracer.startActiveSpan('db.query', async (dbSpan) => {
        dbSpan.setAttribute('db.system', 'postgresql');
        dbSpan.setAttribute('db.statement', 'INSERT INTO payments...');
        const res = await db.insertPayment(userId, amount);
        dbSpan.end();
        return res;
      });
      
      // Span enfant pour l'appel Stripe
      await tracer.startActiveSpan('stripe.charge', async (stripeSpan) => {
        stripeSpan.setAttribute('http.method', 'POST');
        stripeSpan.setAttribute('http.url', 'https://api.stripe.com/v1/charges');
        await stripe.charges.create({ amount, currency: 'chf' });
        stripeSpan.end();
      });
      
      // Incrémenter le compteur
      paymentCounter.add(1, { status: 'success', currency: 'CHF' });
      
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
      
    } catch (error) {
      // Enregistrer l'erreur
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      paymentCounter.add(1, { status: 'error', currency: 'CHF' });
      throw error;
      
    } finally {
      paymentDuration.record(Date.now() - startTime);
      span.end();
    }
  });
}

Python :

from opentelemetry import trace, metrics

tracer = trace.get_tracer("payment-service")
meter = metrics.get_meter("payment-service")

payment_counter = meter.create_counter(
    "payments_processed_total",
    description="Total payments processed",
)
payment_duration = meter.create_histogram(
    "payment_duration_ms",
    description="Payment processing duration",
)

def process_payment(user_id: str, amount: float):
    with tracer.start_as_current_span("process-payment") as span:
        span.set_attribute("user.id", user_id)
        span.set_attribute("payment.amount", amount)
        
        try:
            with tracer.start_as_current_span("db.insert_payment") as db_span:
                db_span.set_attribute("db.system", "postgresql")
                result = db.insert_payment(user_id, amount)
            
            with tracer.start_as_current_span("stripe.charge") as stripe_span:
                stripe_span.set_attribute("http.url", "https://api.stripe.com/v1/charges")
                stripe.Charge.create(amount=amount, currency="chf")
            
            payment_counter.add(1, {"status": "success"})
            return result
            
        except Exception as e:
            span.set_status(trace.StatusCode.ERROR, str(e))
            span.record_exception(e)
            payment_counter.add(1, {"status": "error"})
            raise

Le OTel Collector

Le Collector est le composant central de l’architecture OTel. Il reçoit, traite et exporte les données de télémétrie.

Apps → [Receivers] → [Processors] → [Exporters] → Backends

Configuration

otel-collector-config.yaml :

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  
  # Scraper Prometheus (métriques existantes)
  prometheus:
    config:
      scrape_configs:
        - job_name: 'kubernetes-pods'
          scrape_interval: 15s
          kubernetes_sd_configs:
            - role: pod
  
  # Host metrics (CPU, RAM, disk)
  hostmetrics:
    collection_interval: 30s
    scrapers:
      cpu: {}
      memory: {}
      disk: {}
      network: {}

processors:
  # Batch pour optimiser les exports
  batch:
    timeout: 5s
    send_batch_size: 1000
    send_batch_max_size: 2000
  
  # Ajouter des attributs
  resource:
    attributes:
      - key: environment
        value: production
        action: upsert
      - key: cluster
        value: prod-eu-west
        action: upsert
  
  # Filtrer (réduire le volume)
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.target"] == "/health"'  # Exclure les health checks
    metrics:
      metric:
        - 'name == "go_gc_duration_seconds"'  # Exclure les métriques Go internes
  
  # Sampling (tail-based)
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow-requests
        type: latency
        latency: {threshold_ms: 1000}
      - name: probabilistic
        type: probabilistic
        probabilistic: {sampling_percentage: 10}

  # Memory limiter (protection OOM)
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

exporters:
  # Grafana Tempo (traces)
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  
  # Grafana Mimir (métriques)
  prometheusremotewrite:
    endpoint: http://mimir:9009/api/v1/push
  
  # Grafana Loki (logs)
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
  
  # Debug (stdout)
  debug:
    verbosity: basic

extensions:
  health_check:
    endpoint: 0.0.0.0:13133
  zpages:
    endpoint: 0.0.0.0:55679

service:
  extensions: [health_check, zpages]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, filter, tail_sampling, batch]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp, prometheus, hostmetrics]
      processors: [memory_limiter, resource, filter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [loki]

Stack LGTM (Loki, Grafana, Tempo, Mimir)

La stack LGTM de Grafana Labs est le backend open source le plus populaire pour OpenTelemetry :

ComposantRôleÉquivalent propriétaire
LokiStockage de logsSplunk, Elasticsearch
GrafanaVisualisationDatadog dashboards
TempoStockage de tracesJaeger, Zipkin
MimirStockage de métriquesPrometheus long-term, Thanos

Architecture

                    ┌──────────────┐
                    │   Grafana    │  ← Visualisation
                    │   :3000      │
                    └──────┬───────┘
                           │ query
        ┌──────────┬───────┼───────────┐
        │          │       │           │
   ┌────▼───┐ ┌───▼───┐ ┌─▼──────┐ ┌──▼───┐
   │  Loki  │ │ Tempo │ │ Mimir  │ │Pyro- │
   │ (logs) │ │(trace)│ │(metric)│ │scope │
   │ :3100  │ │ :3200 │ │ :9009  │ │:4040 │
   └────▲───┘ └───▲───┘ └───▲────┘ └──▲───┘
        │         │         │          │
        └─────────┴────┬────┴──────────┘

              ┌────────▼─────────┐
              │  OTel Collector  │  ← Réception & routage
              │  :4317 / :4318   │
              └────────▲─────────┘
                       │ OTLP
        ┌──────┬───────┼───────┬──────┐
        │      │       │       │      │
      App1   App2    App3    App4   App5

Démo pratique avec Docker Compose

On va monter toute la stack : apps instrumentées → OTel Collector → LGTM → Grafana.

docker-compose.yml

services:
  # ============================================
  # Backend d'observabilité
  # ============================================
  
  grafana:
    image: grafana/grafana:11.4.0
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: devopslab
      GF_AUTH_ANONYMOUS_ENABLED: "true"
      GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
    volumes:
      - ./config/grafana/provisioning:/etc/grafana/provisioning
      - grafana-data:/var/lib/grafana
    depends_on:
      - loki
      - tempo
      - mimir

  loki:
    image: grafana/loki:3.3.0
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yaml
    volumes:
      - ./config/loki/config.yaml:/etc/loki/config.yaml
      - loki-data:/loki

  tempo:
    image: grafana/tempo:2.6.0
    ports:
      - "3200:3200"    # HTTP API
      - "4317:4317"    # OTLP gRPC (direct, sans collector)
    command: -config.file=/etc/tempo/config.yaml
    volumes:
      - ./config/tempo/config.yaml:/etc/tempo/config.yaml
      - tempo-data:/var/tempo

  mimir:
    image: grafana/mimir:2.14.0
    ports:
      - "9009:9009"
    command: -config.file=/etc/mimir/config.yaml
    volumes:
      - ./config/mimir/config.yaml:/etc/mimir/config.yaml
      - mimir-data:/data

  # ============================================
  # OpenTelemetry Collector
  # ============================================
  
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.115.0
    ports:
      - "4318:4318"    # OTLP HTTP
      - "13133:13133"  # Health check
      - "55679:55679"  # zPages
    volumes:
      - ./config/otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml
    depends_on:
      - loki
      - tempo
      - mimir

  # ============================================
  # Applications de démo
  # ============================================
  
  api-gateway:
    build:
      context: ./apps/api-gateway
    ports:
      - "8080:8080"
    environment:
      OTEL_SERVICE_NAME: api-gateway
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
      PAYMENT_SERVICE_URL: http://payment-service:8081
      USER_SERVICE_URL: http://user-service:8082
    depends_on:
      - otel-collector
      - payment-service
      - user-service

  payment-service:
    build:
      context: ./apps/payment-service
    ports:
      - "8081:8081"
    environment:
      OTEL_SERVICE_NAME: payment-service
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
      DB_HOST: postgres
      DB_NAME: payments
    depends_on:
      - otel-collector
      - postgres

  user-service:
    build:
      context: ./apps/user-service
    ports:
      - "8082:8082"
    environment:
      OTEL_SERVICE_NAME: user-service
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
      REDIS_URL: redis://redis:6379
    depends_on:
      - otel-collector
      - redis

  # ============================================
  # Datastores
  # ============================================
  
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: payments
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - pg-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data

  # ============================================
  # Générateur de trafic
  # ============================================
  
  load-generator:
    build:
      context: ./apps/load-generator
    environment:
      TARGET_URL: http://api-gateway:8080
      REQUESTS_PER_SECOND: 5
    depends_on:
      - api-gateway

volumes:
  grafana-data:
  loki-data:
  tempo-data:
  mimir-data:
  pg-data:
  redis-data:

Configurations des backends

config/loki/config.yaml :

auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: "2024-01-01"
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  allow_structured_metadata: true
  volume_enabled: true

config/tempo/config.yaml :

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

storage:
  trace:
    backend: local
    local:
      path: /var/tempo/traces
    wal:
      path: /var/tempo/wal

metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: docker-compose
  storage:
    path: /var/tempo/generator/wal
    remote_write:
      - url: http://mimir:9009/api/v1/push
        send_exemplars: true
  traces_storage:
    path: /var/tempo/generator/traces
  processor:
    service_graphs:
      dimensions:
        - service.namespace
    span_metrics:
      dimensions:
        - http.method
        - http.status_code

overrides:
  defaults:
    metrics_generator:
      processors: [service-graphs, span-metrics]

config/mimir/config.yaml :

multitenancy_enabled: false

server:
  http_listen_port: 9009

blocks_storage:
  backend: filesystem
  filesystem:
    dir: /data/blocks
  tsdb:
    dir: /data/tsdb

compactor:
  data_dir: /data/compactor

distributor:
  ring:
    kvstore:
      store: memberlist

ingester:
  ring:
    kvstore:
      store: memberlist
    replication_factor: 1

store_gateway:
  sharding_ring:
    replication_factor: 1

limits:
  native_histograms_ingestion_enabled: true

config/otel-collector/config.yaml :

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 512
  
  resource:
    attributes:
      - key: deployment.environment
        value: demo
        action: upsert

  memory_limiter:
    check_interval: 1s
    limit_mib: 256

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  prometheusremotewrite:
    endpoint: http://mimir:9009/api/v1/push
    resource_to_telemetry_conversion:
      enabled: true

  loki:
    endpoint: http://loki:3100/loki/api/v1/push

  debug:
    verbosity: basic

extensions:
  health_check:
    endpoint: 0.0.0.0:13133

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [loki]

config/grafana/provisioning/datasources/datasources.yaml :

apiVersion: 1
datasources:
  - name: Mimir
    type: prometheus
    access: proxy
    url: http://mimir:9009/prometheus
    isDefault: true
    jsonData:
      exemplarTraceIdDestinations:
        - name: traceID
          datasourceUid: tempo

  - name: Tempo
    type: tempo
    access: proxy
    uid: tempo
    url: http://tempo:3200
    jsonData:
      tracesToLogs:
        datasourceUid: loki
        filterByTraceID: true
      tracesToMetrics:
        datasourceUid: mimir
      serviceMap:
        datasourceUid: mimir

  - name: Loki
    type: loki
    access: proxy
    uid: loki
    url: http://loki:3100
    jsonData:
      derivedFields:
        - datasourceUid: tempo
          matcherRegex: '"trace_id":"(\w+)"'
          name: TraceID
          url: '$${__value.raw}'

Lancer la démo

# Tout démarrer
docker compose up -d

# Vérifier que tout tourne
docker compose ps

# Ouvrir Grafana
open http://localhost:3000
# Login: admin / devopslab

# Générer du trafic
curl http://localhost:8080/api/users
curl http://localhost:8080/api/payments -X POST -d '{"amount": 42}'

# Voir les traces dans Grafana
# → Explore → Tempo → Search
# → Service Map (les connexions entre services)

# Voir les logs
# → Explore → Loki → {service_name="payment-service"}

# Voir les métriques
# → Explore → Mimir → http_server_request_duration_seconds_bucket

Ce que tu vas voir dans Grafana

  1. Service Map — Graphe automatique des connexions entre tes services
  2. Traces — Le parcours complet d’une requête avec les timings
  3. Logs corrélés — Cliquer sur une trace ouvre les logs associés
  4. RED metrics — Rate, Errors, Duration générés automatiquement par Tempo
  5. Exemplars — Points sur les graphiques qui linkent vers la trace correspondante

Bonnes pratiques

Sampling intelligent

Tu ne peux pas stocker 100% des traces en production. Utilise le sampling :

# Head-based sampling (décision au début de la trace)
# Simple mais peut rater des traces intéressantes
sampler:
  type: parentbased_traceidratio
  argument: "0.1"  # 10% des traces

# Tail-based sampling (décision à la fin, dans le Collector)
# Plus coûteux mais garde les traces intéressantes
processors:
  tail_sampling:
    policies:
      - name: keep-errors
        type: status_code
        status_code: {status_codes: [ERROR]}      # 100% des erreurs
      - name: keep-slow
        type: latency
        latency: {threshold_ms: 500}               # 100% des requêtes lentes
      - name: sample-rest
        type: probabilistic
        probabilistic: {sampling_percentage: 5}     # 5% du reste

Conventions de nommage (Semantic Conventions)

OTel définit des conventions standard pour les attributs :

# HTTP
http.method = "GET"
http.status_code = 200
http.url = "https://api.devopslab.ch/users"

# Database
db.system = "postgresql"
db.name = "payments"
db.statement = "SELECT * FROM users WHERE id = ?"

# Service
service.name = "payment-api"
service.version = "2.4.0"
service.namespace = "production"

# Infrastructure
host.name = "web01"
container.id = "abc123"
k8s.pod.name = "payment-api-7f8d9b-x2k4l"

Coûts à surveiller

SignalVolume typiqueCoût stockage
Métriques~1-5K series/serviceFaible
Logs~1-10 GB/jour/serviceÉlevé
Traces~100K-1M spans/jourMoyen

Réduire les coûts :

  • Sampling des traces (10-25% suffit souvent)
  • Filtrer les health checks et les métriques inutiles
  • Rétention adaptée (traces 7j, métriques 90j, logs 30j)
  • Compression (Loki et Tempo compressent nativement)

Conclusion

OpenTelemetry est devenu le standard de facto pour l’observabilité en 2026. Avec la stack LGTM, tu as une solution complète, open source, et sans vendor lock-in.

Par où commencer :

  1. Ajoute l’auto-instrumentation à une app existante (5 minutes)
  2. Déploie un OTel Collector pour centraliser les données
  3. Monte Grafana + Tempo pour voir tes premières traces
  4. Ajoute Loki pour les logs, Mimir pour les métriques long-term
  5. Crée des dashboards et des alertes

L’observabilité n’est pas un luxe — c’est ce qui fait la différence entre “ça marche” et “je sais pourquoi ça marche (ou pas)”.

Articles liés