Cloud
Monitoring

Grafana Tempo 2026 : tracing distribué cost-effective avec OpenTelemetry

9 février 2026

21 min de lecture

Le tracing distribué est devenu indispensable pour comprendre les architectures microservices modernes. Grafana Tempo apporte une approche radicalement différente du tracing : un backend extrêmement cost-effective qui stocke les traces sur object storage (S3, GCS, Azure Blob), tout en offrant des performances de recherche remarquables grâce à TraceQL. Ce guide complet explore comment Tempo révolutionne l'observabilité distribuée en réduisant les coûts de 80-90% comparé aux solutions traditionnelles.

L'évolution du tracing distribué

Le problème des backends traditionnels

Le tracing distribué n'est pas nouveau. Jaeger et Zipkin existent depuis des années et fonctionnent bien techniquement. Le problème réside dans leurs coûts d'infrastructure. Ces systèmes stockent traditionnellement les traces dans des bases de données (Cassandra, Elasticsearch, BadgerDB), ce qui implique :

Des clusters de stockage massifs : Une architecture microservices moderne génère facilement des dizaines de millions de spans par jour. Stocker ces données dans Elasticsearch ou Cassandra nécessite des clusters substantiels - pensez 10-20 nœuds pour une charge modeste. Ces nœuds doivent rester online 24/7, même si vous ne recherchez des traces qu'occasionnellement.

Une rétention limitée : Face aux coûts de stockage, les équipes se résignent à conserver seulement 3-7 jours de traces. Cela limite drastiquement l'utilité du tracing pour les analyses long-terme, les tendances de performance, ou le debugging de problèmes intermittents qui n'apparaissent qu'après plusieurs jours.

Une complexité opérationnelle : Gérer un cluster Cassandra ou Elasticsearch demande une expertise spécialisée. Vous devez monitorer les nœuds, gérer les réplications, tuner les performances, planifier la capacité. Cette charge opérationnelle détourne les équipes de leur mission principale : l'observabilité applicative.

Tempo : l'approche object storage

Grafana Tempo prend une approche radicalement différente : plutôt que d'indexer et stocker les traces dans une base de données complexe, Tempo écrit directement sur object storage (S3, GCS, Azure Blob). Cette architecture simple-mais-brillante apporte des avantages transformateurs.

Coûts réduits de 80-90% : Le stockage object (S3 Standard) coûte environ $0.023/GB/mois. Avec S3 Intelligent-Tiering ou Glacier, vous descendez sous $0.005/GB/mois. Comparé à maintenir un cluster Elasticsearch à ~$0.20/GB/mois (serveurs + stockage SSD), l'économie est massive. Une entreprise dépensant $50k/mois pour Jaeger sur Elasticsearch peut descendre à $5-10k/mois avec Tempo sur S3.

Rétention illimitée (pratiquement) : Avec des coûts aussi bas, conserver 1 an de traces devient abordable. Cela ouvre des possibilités nouvelles : analyse de tendances long-terme, comparaison de performances entre releases, debugging de problèmes intermittents découverts tardivement.

Simplicité opérationnelle : Tempo lui-même est stateless. Pas de cluster à gérer, pas de sharding à planifier, pas de compaction à tuner. Vous déployez simplement quelques instances Tempo (pour la HA) qui écrivent sur S3. L'opération devient triviale comparée à un cluster Jaeger/Cassandra.

TraceQL : la révolution query

L'innovation la plus impressionnante de Tempo est TraceQL, un langage de query dédié au tracing. Historiquement, chercher des traces nécessitait soit d'avoir l'ID de trace exact (trouvé dans les logs), soit de parcourir des millions de traces aléatoirement. TraceQL change la donne en permettant des queries structurées :

# Trouver traces lentes du service checkout avec erreurs
{ service.name = "checkout" && duration > 1s && status = error }

# Traces contenant un appel à la base payments
{ span.db.name = "payments" }

# Traces avec plus de 50 spans (complexes)
{ span.count > 50 }

Cette capacité de query transforme le tracing d'un outil de debugging ponctuel en outil d'analyse proactive de performance.


Architecture Tempo

Composants et responsabilités

Tempo adopte une architecture modulaire inspirée de Loki et Mimir. Comprendre ces composants est essentiel pour un déploiement optimal :

Distributor : Point d'entrée pour les traces. Les applications (via OTLP ou Jaeger) envoient leurs traces au distributor. Celui-ci valide les données, applique éventuellement du sampling, et les distribue vers les ingesters selon un consistent hashing. Le distributor est stateless et scale horizontalement trivialement.

Ingester : Agrège les spans en traces complètes et les écrit sur object storage. Les ingesters maintiennent un buffer en mémoire (appelé head block) pour les traces récentes (dernières minutes). Une fois un block complet (~100-200 MB), l'ingester le flush vers S3. Les ingesters utilisent un WAL (Write-Ahead Log) local pour la durabilité en cas de crash.

Compactor : Consolide les petits blocks en plus gros blocks optimisés pour la recherche. Sans compaction, vous accumuleriez des milliers de petits fichiers S3, dégradant les performances de query. Le compactor tourne périodiquement (typiquement toutes les heures), merge les blocks, applique la retention policy, et produit des blocks efficaces.

Query Frontend : Interface pour les queries TraceQL. Le frontend parse les queries, les distribue aux queriers, agrège les résultats. Il implémente également un cache pour accélérer les queries répétées.

Querier : Exécute les queries contre les blocks S3. Les queriers téléchargent les blocks, extraient les bloom filters et indexes, cherchent les traces matchant la query. Ils maintiennent un cache local des blocks récents pour optimiser les queries fréquentes.

Data flow : de l'application à S3

Suivons une trace depuis son émission :

1. Application émet spans via SDK OpenTelemetry

App → OTLP/gRPC → Tempo Distributor

2. Distributor route vers ingester

Distributor → [consistent hash sur trace ID] → Ingester X

Tous les spans d'une même trace vont vers le même ingester, assurant qu'ils sont assemblés ensemble.

3. Ingester accumule spans en mémoire

Ingester garde trace en RAM dans "head block"
+ écrit dans WAL pour durabilité

4. Flush périodique vers S3

Toutes les 30-60 secondes:
Ingester → sérialise block → compress (zstd) → upload S3

Chaque block contient typiquement 10k-50k traces (100-200 MB compressés).

5. Compaction asynchrone

Compactor (hourly):
- Liste blocks âgés de 2-24h
- Merge en blocks plus gros (1-2 GB)
- Applique retention (supprime vieux blocks)
- Upload blocks compactés
- Supprime blocks originaux

6. Query s'exécute contre blocks S3

Query Frontend → parse TraceQL → dispatch Queriers
Queriers → download blocks → scan bloom filters → extract matching traces

Cette architecture découple l'ingestion (write path rapide) et la query (read path optimisé), permettant d'optimiser chacun indépendamment.


Déploiement Tempo

Configuration minimale

Pour démarrer avec Tempo, une configuration minimale suffit. Voici un setup single-process (monolithique) pour développement ou petite production :

# /etc/tempo/tempo.yaml

server:
  http_listen_port: 3200
  grpc_listen_port: 9095

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318
    jaeger:
      protocols:
        grpc:
          endpoint: 0.0.0.0:14250
        thrift_http:
          endpoint: 0.0.0.0:14268

ingester:
  max_block_duration: 10m # Flush toutes les 10 minutes
  complete_block_timeout: 30m

compactor:
  compaction:
    block_retention: 720h # Conserver 30 jours
    compacted_block_retention: 1h

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      # Credentials via IAM role recommandé
    wal:
      path: /var/lib/tempo/wal
    cache:
      path: /var/lib/tempo/cache

querier:
  search:
    enabled: true
  max_concurrent_queries: 20

query_frontend:
  search:
    enabled: true

Démarrage :

# Installer Tempo
wget https://github.com/grafana/tempo/releases/download/v2.4.0/tempo_2.4.0_linux_amd64.tar.gz
tar -xvf tempo_2.4.0_linux_amd64.tar.gz
sudo mv tempo /usr/local/bin/

# Créer directories
sudo mkdir -p /var/lib/tempo/{wal,cache}
sudo chown tempo:tempo /var/lib/tempo -R

# Lancer
tempo -config.file=/etc/tempo/tempo.yaml
Déploiement mode microservices

Pour production avec haute disponibilité, déployez Tempo en mode microservices où chaque composant scale indépendamment :

Distributor (3+ replicas) :

# distributor.yaml
target: distributor

server:
  http_listen_port: 3200
  grpc_listen_port: 9095

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
        http:
  ring:
    kvstore:
      store: etcd # Ou consul, memberlist
      etcd:
        endpoints: ['etcd:2379']

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces

Ingester (5+ replicas) :

# ingester.yaml
target: ingester

server:
  http_listen_port: 3200
  grpc_listen_port: 9095

ingester:
  lifecycler:
    ring:
      replication_factor: 3 # Chaque trace répliquée 3x
      kvstore:
        store: etcd
        etcd:
          endpoints: ['etcd:2379']

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
    wal:
      path: /var/lib/tempo/wal

Querier (3+ replicas) :

# querier.yaml
target: querier

querier:
  max_concurrent_queries: 20
  search:
    enabled: true

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
    cache:
      path: /var/lib/tempo/cache

Cette architecture permet de scale chaque composant selon sa charge spécifique. Typiquement, vous avez besoin de plus d'ingesters (write-heavy) que de queriers (read-light).

Kubernetes Helm chart

Le déploiement Kubernetes est grandement simplifié via Helm :

# Ajouter repo Grafana
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Installer Tempo distributed
helm install tempo grafana/tempo-distributed \
  --namespace observability \
  --create-namespace \
  --values tempo-values.yaml

tempo-values.yaml pour production :

# tempo-values.yaml

# Storage backend
storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces-prod
      region: us-east-1

# Distributor
distributor:
  replicas: 3
  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: 2000m
      memory: 2Gi

# Ingester
ingester:
  replicas: 5
  persistence:
    enabled: true  # Pour WAL
    size: 50Gi
  resources:
    requests:
      cpu: 1000m
      memory: 2Gi
    limits:
      cpu: 4000m
      memory: 4Gi

# Querier
querier:
  replicas: 3
  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: 2000m
      memory: 2Gi

# Query Frontend
queryFrontend:
  replicas: 2
  resources:
    requests:
      cpu: 500m
      memory: 512Mi

# Compactor
compactor:
  replicas: 1  # Single compactor suffit
  resources:
    requests:
      cpu: 1000m
      memory: 2Gi

# Retention
compactor:
  config:
    compaction:
      block_retention: 2160h  # 90 jours

Intégration OpenTelemetry

Applications instrumentées OTLP

OpenTelemetry est devenu le standard de facto pour l'instrumentation. Instrumenter une application pour envoyer vers Tempo :

Node.js application :

// app.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
	resource: new Resource({
		[SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
		[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
		[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production',
	}),
	traceExporter: new OTLPTraceExporter({
		url: 'http://tempo-distributor:4317',
		// TLS en production
	}),
});

sdk.start();

// Votre application Express, etc.
const express = require('express');
const app = express();

app.get('/api/users', async (req, res) => {
	// Les spans sont créés automatiquement
	const users = await db.query('SELECT * FROM users');
	res.json(users);
});

Python application (FastAPI) :

# app.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource

# Configuration
resource = Resource(attributes={
    "service.name": "python-api",
    "service.version": "2.0.0",
    "deployment.environment": "production"
})

provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="tempo-distributor:4317", insecure=True)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# FastAPI auto-instrumentation
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from fastapi import FastAPI

app = FastAPI()
FastAPIInstrumentor.instrument_app(app)

@app.get("/api/products")
async def get_products():
    # Span automatiquement créé
    products = await db.fetch_products()
    return products

L'auto-instrumentation OpenTelemetry capture automatiquement les requêtes HTTP, queries DB, appels Redis, etc. Pas besoin de code tracing manuel pour les cas courants.

OpenTelemetry Collector

Pour des architectures plus complexes, déployez l'OTel Collector entre applications et Tempo. Le Collector offre buffering, filtering, sampling, et peut router vers multiples backends :

# otel-collector-config.yaml

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

processors:
  batch:
    timeout: 10s
    send_batch_size: 1024

  # Attributs additionnels
  resource:
    attributes:
      - key: cluster
        value: production
        action: upsert

  # Sampling probabiliste (garder 10%)
  probabilistic_sampler:
    sampling_percentage: 10

exporters:
  otlp:
    endpoint: tempo-distributor:4317
    tls:
      insecure: false
      cert_file: /certs/cert.pem
      key_file: /certs/key.pem

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, resource, probabilistic_sampler]
      exporters: [otlp]

Déployer en sidecar ou DaemonSet Kubernetes :

# otel-collector-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: observability
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: collector
          image: otel/opentelemetry-collector:0.95.0
          command: ['--config=/conf/otel-collector-config.yaml']
          ports:
            - containerPort: 4317 # OTLP gRPC
            - containerPort: 4318 # OTLP HTTP
          volumeMounts:
            - name: config
              mountPath: /conf
      volumes:
        - name: config
          configMap:
            name: otel-collector-config

Les applications envoient alors vers localhost:4317 (DaemonSet) plutôt que directement vers Tempo.


TraceQL : queries avancées

Syntaxe de base

TraceQL permet de chercher des traces selon leurs attributs. La syntaxe ressemble à PromQL mais adaptée au tracing :

Sélecteurs simples :

# Toutes traces du service checkout
{ service.name = "checkout" }

# Traces avec erreurs
{ status = error }

# Traces HTTP POST
{ http.method = "POST" }

# Combinaison AND
{ service.name = "api" && http.status_code >= 500 }

# Combinaison OR
{ service.name = "auth" || service.name = "users" }

Filtres sur durée :

# Traces lentes (>1 seconde)
{ duration > 1s }

# Traces très lentes
{ duration > 5s }

# Traces rapides
{ duration < 100ms }

# Range
{ duration > 500ms && duration < 2s }

Attributs de span :

# Traces contenant un span avec db.name = "payments"
{ span.db.name = "payments" }

# Traces avec appel Redis
{ span.db.system = "redis" }

# Traces touchant table users
{ span.db.table = "users" }
Agrégations et métriques

TraceQL supporte des agrégations pour analyser les traces :

Compter traces :

# Nombre de traces par service
{ } | count by (service.name)

# Nombre d'erreurs par service
{ status = error } | count by (service.name)

Percentiles de durée :

# P50, P95, P99 du service checkout
{ service.name = "checkout" } | percentile(duration, 0.5, 0.95, 0.99)

# Latence max par endpoint
{ http.target != "" } | max(duration) by (http.target)

Span count :

# Traces complexes (beaucoup de spans)
{ span.count > 100 }

# Moyenne de spans par service
{ } | avg(span.count) by (service.name)
Use cases pratiques

Debugging d'un problème de performance :

# 1. Identifier services lents
{ duration > 2s } | count by (service.name)
# Résultat : service "recommendation" apparaît souvent

# 2. Regarder spans dans recommendation
{ service.name = "recommendation" && duration > 2s }
# Examiner quelques traces

# 3. Identifier span lent spécifique
{ service.name = "recommendation" && span.name = "compute_scores" && span.duration > 1s }
# Le span compute_scores est le bottleneck

Analyse d'erreurs :

# 1. Erreurs par service
{ status = error } | count by (service.name)

# 2. Erreurs par type
{ status = error } | count by (error.type)

# 3. Traces avec erreur spécifique
{ error.type = "DatabaseTimeoutError" }

Impact d'un déploiement :

# Comparer latence avant/après deploy
# Avant (timestamp < 1706097600)
{ service.name = "api" && start_time < 2024-01-24T10:00:00Z } | percentile(duration, 0.95)
# P95 = 850ms

# Après (timestamp >= 1706097600)
{ service.name = "api" && start_time >= 2024-01-24T10:00:00Z } | percentile(duration, 0.95)
# P95 = 1200ms
# → Régression de 40% !

Compression et optimisation storage

Choix algorithme compression

Tempo supporte plusieurs algorithmes de compression. Le choix impacte significativement les coûts storage et performances query :

zstd (recommandé) :

storage:
  trace:
    block:
      encoding: zstd

Compression ratio : 10-15x, rapide à décompresser. C'est le meilleur choix général.

snappy :

storage:
  trace:
    block:
      encoding: snappy

Compression ratio : 3-5x, très rapide mais moins efficace. Utile si CPU est bottleneck.

gzip :

storage:
  trace:
    block:
      encoding: gzip

Compression ratio : 8-12x, lent à compresser/décompresser. Rarement optimal.

Benchmark réel (1 million de spans) :

AlgorithmTaille originaleTaille compresséeRatioTemps compressionTemps query
none850 MB850 MB1x-120ms
snappy850 MB210 MB4x2s150ms
zstd850 MB65 MB13x5s180ms
gzip850 MB80 MB10.6x12s250ms

zstd offre le meilleur compromis : excellente compression (réduction coûts S3) avec overhead query acceptable.

Block sizing et compaction

La taille des blocks impacte les performances et coûts :

Blocks trop petits (ex: 10 MB) :

  • ❌ Trop de fichiers S3 (coûts API LIST)
  • ❌ Overhead metadata élevé
  • ✅ Queries rapides (moins de data à scanner)

Blocks trop gros (ex: 5 GB) :

  • ✅ Peu de fichiers S3
  • ✅ Compression plus efficace
  • ❌ Queries lentes (doivent download blocks entiers)

Optimal : 100-200 MB (compressé) :

ingester:
  max_block_bytes: 104857600 # 100 MB
  max_block_duration: 10m

compactor:
  compaction:
    max_compaction_objects: 10 # Merge jusqu'à 10 blocks
    block_retention: 720h # 30 jours

Le compactor merge les petits blocks en blocks ~1-2 GB optimisés pour query.

S3 storage classes

Exploitez les storage classes S3 pour réduire les coûts :

Stratégie recommandée :

Traces récentes (0-7 jours)    → S3 Standard
Traces moyennement anciennes    → S3 Standard-IA (Infrequent Access)
(7-30 jours)
Traces anciennes (30-90 jours)  → S3 Intelligent-Tiering
Traces archivées (>90 jours)    → S3 Glacier Instant Retrieval

Implémentation via S3 Lifecycle policies :

{
	"Rules": [
		{
			"Id": "Tempo-Lifecycle",
			"Status": "Enabled",
			"Prefix": "tempo/",
			"Transitions": [
				{
					"Days": 7,
					"StorageClass": "STANDARD_IA"
				},
				{
					"Days": 30,
					"StorageClass": "INTELLIGENT_TIERING"
				},
				{
					"Days": 90,
					"StorageClass": "GLACIER_IR"
				}
			],
			"Expiration": {
				"Days": 365
			}
		}
	]
}

Économies typiques :

  • Standard : $0.023/GB/mois
  • Standard-IA : $0.0125/GB/mois (45% moins cher)
  • Intelligent-Tiering : $0.004-0.0125/GB/mois
  • Glacier IR : $0.004/GB/mois (83% moins cher)

Pour 10 TB de traces sur 1 an avec lifecycle optimal : ~$600/mois vs $2300/mois sur Standard pur.


Monitoring et observabilité de Tempo

Métriques Prometheus

Tempo expose des métriques Prometheus pour se monitorer lui-même :

# prometheus.yaml
scrape_configs:
  - job_name: 'tempo'
    static_configs:
      - targets: ['tempo:3200']

Métriques clés à monitorer :

# Ingestion rate (spans/sec)
rate(tempo_distributor_spans_received_total[5m])

# Queue backlog (si élevé = problème ingestion)
tempo_ingester_flush_queue_length

# Block flush rate
rate(tempo_ingester_blocks_flushed_total[5m])

# Query latency
histogram_quantile(0.95,
  rate(tempo_query_frontend_query_duration_seconds_bucket[5m])
)

# Erreurs ingestion
rate(tempo_distributor_push_errors_total[5m])

# S3 operations
rate(tempo_s3_request_duration_seconds_count[5m])
Alertes critiques

Configurez ces alertes dans Prometheus/Alertmanager :

# alerts.yaml
groups:
  - name: tempo
    rules:
      # Ingester a du mal à flusher
      - alert: TempoIngesterFlushQueueHigh
        expr: tempo_ingester_flush_queue_length > 100
        for: 15m
        annotations:
          summary: 'Ingester flush queue backed up'

      # Erreurs ingestion élevées
      - alert: TempoDistributorErrors
        expr: rate(tempo_distributor_push_errors_total[5m]) > 10
        for: 5m
        annotations:
          summary: 'High rate of distributor errors'

      # Queries échouent
      - alert: TempoQueryFailures
        expr: rate(tempo_query_frontend_failed_queries_total[5m]) > 5
        for: 5m
        annotations:
          summary: 'Queries failing'

      # S3 latence élevée
      - alert: TempoS3Slow
        expr: |
          histogram_quantile(0.95, 
            rate(tempo_s3_request_duration_seconds_bucket[5m])
          ) > 5
        for: 10m
        annotations:
          summary: 'S3 operations slow'
Dashboards Grafana

Grafana fournit des dashboards officiels pour Tempo. Importez-les :

# Dashboard Tempo Operational
# Grafana ID: 16490

# Dashboard Tempo Reads
# Grafana ID: 16491

# Dashboard Tempo Writes
# Grafana ID: 16492

Ces dashboards montrent ingestion rates, query latencies, compaction status, S3 operations, etc.


Intégration Grafana : corrélation logs/metrics/traces

Configuration datasource Tempo

Dans Grafana, ajoutez Tempo comme datasource :

# grafana-datasources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
data:
  tempo.yaml: |
    apiVersion: 1
    datasources:
    - name: Tempo
      type: tempo
      access: proxy
      url: http://tempo-query-frontend:3200
      jsonData:
        tracesToLogs:
          datasourceUid: 'loki'  # UID datasource Loki
          tags: ['cluster', 'namespace', 'pod']
          mappedTags: [{ key: 'service.name', value: 'service' }]
          mapTagNamesEnabled: true
          spanStartTimeShift: '-1m'
          spanEndTimeShift: '1m'
        tracesToMetrics:
          datasourceUid: 'prometheus'
          tags: [{ key: 'service.name', value: 'service' }]
          queries:
            - name: 'Request Rate'
              query: 'rate(http_requests_total{service="$service"}[5m])'
        serviceMap:
          datasourceUid: 'prometheus'
        search:
          enabled: true
        nodeGraph:
          enabled: true

Cette configuration crée des liens automatiques : depuis une trace, cliquer vers les logs Loki ou metrics Prometheus correspondants.

Workflow d'investigation

Scénario : Une alerte se déclenche sur latence élevée d'un service.

1. Démarrer depuis dashboard Prometheus :

# Latence P95 service checkout
histogram_quantile(0.95,
  rate(http_request_duration_seconds_bucket{service="checkout"}[5m])
)
# Pic à 3.5s détecté à 14:30

2. Cliquer "Explore traces" (bouton Grafana) → Redirige vers Tempo avec query:

{ service.name = "checkout" && start_time >= 2024-01-24T14:25:00Z && start_time <= 2024-01-24T14:35:00Z }

3. Examiner trace lente :

  • Visualiser cascade spans
  • Identifier span lent : database.query.users
  • Durée : 2.8s (normalement 50ms)

4. Cliquer "Logs for this span" (bouton Grafana) → Redirige vers Loki:

{namespace="production", pod=~"checkout-.*"}
  | json
  | trace_id="abc123..."
  | line_format "{{.message}}"

5. Logs révèlent :

ERROR: Database connection pool exhausted, waiting for available connection

6. Retour metrics Prometheus pour confirmer :

# Pool connections
database_pool_active_connections{service="checkout"}
# Saturé à 100% depuis 14:28

Cette corrélation logs-metrics-traces accélère drastiquement le troubleshooting.


Sampling et cost control

Tail sampling

Tempo supporte le tail sampling : décider de garder ou non une trace après l'avoir collectée, basé sur son contenu complet. C'est plus intelligent que le head sampling (décider à l'avance aléatoirement).

Configuration (nécessite OTel Collector) :

# otel-collector avec tail sampling
processors:
  tail_sampling:
    decision_wait: 10s # Attendre tous spans d'une trace
    num_traces: 100000 # Capacity buffer
    expected_new_traces_per_sec: 1000
    policies:
      # Garder TOUTES les erreurs
      - name: error-policy
        type: status_code
        status_code: { status_codes: [ERROR] }

      # Garder traces lentes (>1s)
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 1000 }

      # Sampling probabiliste pour le reste
      - name: probabilistic-sample
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

      # Toujours garder certains services critiques
      - name: critical-services
        type: string_attribute
        string_attribute:
          key: service.name
          values: [payment, auth]

Cette configuration garde :

  • 100% des erreurs
  • 100% des traces >1s
  • 100% des services critiques (payment, auth)
  • 10% du reste

Économie : Si initialement 10M traces/jour, avec tail sampling vous descendez à ~2M traces/jour gardées, soit 80% de réduction coûts stockage.

Dynamic sampling

Pour des économies extrêmes, implémentez du sampling dynamique basé sur la charge :

# Exemple avec API Tempo
import requests

def get_current_ingestion_rate():
    # Query Prometheus pour ingestion actuelle
    response = requests.get(
        'http://prometheus/api/v1/query',
        params={'query': 'rate(tempo_distributor_spans_received_total[5m])'}
    )
    return float(response.json()['data']['result'][0]['value'][1])

def adjust_sampling_rate():
    rate = get_current_ingestion_rate()

    # Si ingestion >100k spans/sec, augmenter sampling
    if rate > 100000:
        new_sampling = 5  # 5% sampling
    elif rate > 50000:
        new_sampling = 20
    else:
        new_sampling = 100  # Tout garder si charge basse

    # Update OTel Collector config dynamiquement
    update_collector_sampling(new_sampling)

Cette approche adaptative réduit automatiquement le volume en période de charge élevée.


Troubleshooting et bonnes pratiques

Problèmes courants

Traces incomplètes (spans manquants) :

Symptôme : Traces avec spans "orphelins" ou cascade brisée.

Causes :

  • Timeout trop court dans ingester (trace pas complètement reçue avant flush)
  • Clock skew entre services (timestamps incohérents)
  • Sampling non-consistant (certains spans samplés, d'autres non)

Solution :

ingester:
  complete_block_timeout: 60m # Augmenter timeout

# Assurer clock sync (NTP) sur tous hosts

# Tail sampling plutôt que head sampling

Queries Tempo lentes :

Symptôme : TraceQL queries prennent >10s

Causes :

  • Trop de blocks à scanner (pas assez de compaction)
  • Pas de bloom filters
  • S3 latence élevée

Solution :

compactor:
  compaction:
    compaction_window: 1h # Compacter plus fréquemment

querier:
  max_concurrent_queries: 50 # Augmenter parallélisme
  search:
    enabled: true

# Activer caching
query_frontend:
  cache:
    backend: memcached
    memcached:
      host: memcached:11211

Coûts S3 élevés :

Symptôme : Facture S3 explose

Causes :

  • Trop de petits blocks (coûts LIST operations)
  • Pas de lifecycle policies
  • Compression désactivée

Solution :

# Augmenter taille blocks
ingester:
  max_block_bytes: 209715200 # 200 MB

# Compression zstd
storage:
  trace:
    block:
      encoding: zstd
# Lifecycle S3 configurée (voir section précédente)
Checklist production

Avant de déployer Tempo en production :

□ Storage backend configuré (S3 + IAM roles)
□ Retention policy définie et S3 lifecycle créée
□ Compression zstd activée
□ Compaction schedulée (1h interval)
□ Monitoring Prometheus configuré
□ Alertes critiques créées (flush queue, erreurs, S3 latency)
□ Datasource Grafana avec corrélation logs/metrics
□ Tail sampling configuré (si volume élevé)
□ HA : 3+ distributors, 3+ ingesters, 2+ queriers
□ Backup/restore testé
□ Documentation runbooks créée

Conclusion

Grafana Tempo révolutionne le tracing distribué en rendant le stockage des traces extrêmement cost-effective tout en maintenant d'excellentes performances de recherche. Son architecture basée sur object storage (S3) réduit les coûts de 80-90% comparé aux solutions traditionnelles, permettant enfin de conserver des rétentions longues (3-12 mois) sans exploser les budgets.

Points clés :

  • Architecture object storage : Traces sur S3/GCS, coûts divisés par 10
  • TraceQL : Langage query puissant pour recherches structurées
  • OpenTelemetry natif : Intégration standard, pas de vendor lock-in
  • Compression zstd : 10-15x ratio, stockage ultra-compact
  • Corrélation native : Liens automatiques vers logs (Loki) et metrics (Prometheus)

Commandes essentielles :

# Installation
wget https://github.com/grafana/tempo/releases/download/v2.4.0/tempo_2.4.0_linux_amd64.tar.gz

# Configuration minimale
tempo -config.file=tempo.yaml

# Query TraceQL
curl 'http://tempo:3200/api/search' \
  -d '{ service.name = "checkout" && duration > 1s }'

# Métriques
curl http://tempo:3200/metrics

Avec Tempo, le tracing distribué devient enfin accessible économiquement à toutes les organisations, transformant ce qui était un luxe réservé aux GAFA en standard accessible pour l'observabilité moderne.

Compléter votre observabilité

Tempo s'intègre dans une stack observabilité complète :

Besoin d'aide sur ce sujet ?

Notre équipe d'experts est là pour vous accompagner dans vos projets d'infrastructure et d'infogérance.

Contactez-nous

Articles similaires