distributed-tracing

Par wshobson · agents

Implémentez le tracing distribué avec Jaeger et Tempo pour suivre les requêtes entre microservices et identifier les goulots d'étranglement. À utiliser lors du débogage de microservices, de l'analyse des flux de requêtes ou de la mise en place de l'observabilité pour les systèmes distribués.

npx skills add https://github.com/wshobson/agents --skill distributed-tracing

Traçage distribué

Mettez en œuvre le traçage distribué avec Jaeger et Tempo pour la visibilité du flux des requêtes sur les microservices.

Objectif

Suivre les requêtes dans les systèmes distribués pour comprendre la latence, les dépendances et les points de défaillance.

Quand l'utiliser

  • Déboguer les problèmes de latence
  • Comprendre les dépendances entre services
  • Identifier les goulots d'étranglement
  • Tracer la propagation des erreurs
  • Analyser les chemins des requêtes

Concepts du traçage distribué

Structure d'une trace

Trace (Request ID: abc123)
  ↓
Span (frontend) [100ms]
  ↓
Span (api-gateway) [80ms]
  ├→ Span (auth-service) [10ms]
  └→ Span (user-service) [60ms]
      └→ Span (database) [40ms]

Composants clés

  • Trace - Parcours de la requête d'un bout à l'autre
  • Span - Opération unique au sein d'une trace
  • Context - Métadonnées propagées entre les services
  • Tags - Paires clé-valeur pour le filtrage
  • Logs - Événements horodatés au sein d'un span

Configuration de Jaeger

Déploiement Kubernetes

# Déployer Jaeger Operator
kubectl create namespace observability
kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.51.0/jaeger-operator.yaml -n observability

# Déployer une instance Jaeger
kubectl apply -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger
  namespace: observability
spec:
  strategy: production
  storage:
    type: elasticsearch
    options:
      es:
        server-urls: http://elasticsearch:9200
  ingress:
    enabled: true
EOF

Docker Compose

version: "3.8"
services:
  jaeger:
    image: jaegertracing/all-in-one:1.62
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686" # UI
      - "14268:14268" # Collector
      - "14250:14250" # gRPC
      - "9411:9411" # Zipkin
    environment:
      - COLLECTOR_ZIPKIN_HOST_PORT=:9411

Référence : Voir references/jaeger-setup.md

Instrumentation de l'application

OpenTelemetry (recommandé)

Python (Flask)

from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from flask import Flask

# Initialiser le tracer
resource = Resource(attributes={SERVICE_NAME: "my-service"})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(JaegerExporter(
    agent_host_name="jaeger",
    agent_port=6831,
))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# Instrumenter Flask
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)

@app.route('/api/users')
def get_users():
    tracer = trace.get_tracer(__name__)

    with tracer.start_as_current_span("get_users") as span:
        span.set_attribute("user.count", 100)
        # Logique métier
        users = fetch_users_from_db()
        return {"users": users}

def fetch_users_from_db():
    tracer = trace.get_tracer(__name__)

    with tracer.start_as_current_span("database_query") as span:
        span.set_attribute("db.system", "postgresql")
        span.set_attribute("db.statement", "SELECT * FROM users")
        # Requête à la base de données
        return query_database()

Node.js (Express)

const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const { JaegerExporter } = require("@opentelemetry/exporter-jaeger");
const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const {
  ExpressInstrumentation,
} = require("@opentelemetry/instrumentation-express");

// Initialiser le tracer
const provider = new NodeTracerProvider({
  resource: { attributes: { "service.name": "my-service" } },
});

const exporter = new JaegerExporter({
  endpoint: "http://jaeger:14268/api/traces",
});

provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();

// Instrumenter les bibliothèques
registerInstrumentations({
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
});

const express = require("express");
const app = express();

app.get("/api/users", async (req, res) => {
  const tracer = trace.getTracer("my-service");
  const span = tracer.startSpan("get_users");

  try {
    const users = await fetchUsers();
    span.setAttributes({ "user.count": users.length });
    res.json({ users });
  } finally {
    span.end();
  }
});

Go

package main

import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

func initTracer() (*sdktrace.TracerProvider, error) {
    exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://jaeger:14268/api/traces"),
    ))
    if err != nil {
        return nil, err
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String("my-service"),
        )),
    )

    otel.SetTracerProvider(tp)
    return tp, nil
}

func getUsers(ctx context.Context) ([]User, error) {
    tracer := otel.Tracer("my-service")
    ctx, span := tracer.Start(ctx, "get_users")
    defer span.End()

    span.SetAttributes(attribute.String("user.filter", "active"))

    users, err := fetchUsersFromDB(ctx)
    if err != nil {
        span.RecordError(err)
        return nil, err
    }

    span.SetAttributes(attribute.Int("user.count", len(users)))
    return users, nil
}

Référence : Voir references/instrumentation.md

Propagation de contexte

En-têtes HTTP

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE

Propagation dans les requêtes HTTP

Python

from opentelemetry.propagate import inject

headers = {}
inject(headers)  # Injecte le contexte de trace

response = requests.get('http://downstream-service/api', headers=headers)

Node.js

const { propagation } = require("@opentelemetry/api");

const headers = {};
propagation.inject(context.active(), headers);

axios.get("http://downstream-service/api", { headers });

Configuration de Tempo (Grafana)

Déploiement Kubernetes

apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-config
data:
  tempo.yaml: |
    server:
      http_listen_port: 3200

    distributor:
      receivers:
        jaeger:
          protocols:
            thrift_http:
            grpc:
        otlp:
          protocols:
            http:
            grpc:

    storage:
      trace:
        backend: s3
        s3:
          bucket: tempo-traces
          endpoint: s3.amazonaws.com

    querier:
      frontend_worker:
        frontend_address: tempo-query-frontend:9095
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tempo
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: tempo
          image: grafana/tempo:2.7
          args:
            - -config.file=/etc/tempo/tempo.yaml
          volumeMounts:
            - name: config
              mountPath: /etc/tempo
      volumes:
        - name: config
          configMap:
            name: tempo-config

Référence : Voir assets/jaeger-config.yaml.template

Stratégies d'échantillonnage

Échantillonnage probabiliste

# Échantillonner 1 % des traces
sampler:
  type: probabilistic
  param: 0.01

Échantillonnage avec limitation de débit

# Échantillonner max 100 traces par seconde
sampler:
  type: ratelimiting
  param: 100

Échantillonnage adaptatif

from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

# Échantillonner en fonction de l'ID de trace (déterministe)
sampler = ParentBased(root=TraceIdRatioBased(0.01))

Analyse des traces

Trouver les requêtes lentes

Requête Jaeger :

service=my-service
duration > 1s

Trouver les erreurs

Requête Jaeger :

service=my-service
error=true
tags.http.status_code >= 500

Graphique de dépendances entre services

Jaeger génère automatiquement des graphiques de dépendances entre services montrant :

  • Les relations entre services
  • Les taux de requêtes
  • Les taux d'erreurs
  • Les latences moyennes

Bonnes pratiques

  1. Échantillonner correctement (1-10 % en production)
  2. Ajouter des tags significatifs (user_id, request_id)
  3. Propager le contexte sur tous les limites de services
  4. Enregistrer les exceptions dans les spans
  5. Utiliser un nommage cohérent pour les opérations
  6. Surveiller la surcharge du traçage (<1 % d'impact CPU)
  7. Configurer des alertes pour les erreurs de trace
  8. Implémenter le contexte distribué (baggage)
  9. Utiliser les événements de span pour les jalons importants
  10. Documenter les normes d'instrumentation

Intégration avec la journalisation

Logs corrélés

import logging
from opentelemetry import trace

logger = logging.getLogger(__name__)

def process_request():
    span = trace.get_current_span()
    trace_id = span.get_span_context().trace_id

    logger.info(
        "Processing request",
        extra={"trace_id": format(trace_id, '032x')}
    )

Dépannage

Aucune trace n'apparaît :

  • Vérifier l'endpoint du collecteur
  • Vérifier la connectivité réseau
  • Vérifier la configuration d'échantillonnage
  • Examiner les logs de l'application

Surcharge de latence élevée :

  • Réduire le taux d'échantillonnage
  • Utiliser le processeur de span par lot
  • Vérifier la configuration de l'exportateur

Compétences associées

  • prometheus-configuration - Pour les métriques
  • grafana-dashboards - Pour la visualisation
  • slo-implementation - Pour les SLO de latence

Skills similaires