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) :
| Algorithm | Taille originale | Taille compressée | Ratio | Temps compression | Temps query |
| none | 850 MB | 850 MB | 1x | - | 120ms |
| snappy | 850 MB | 210 MB | 4x | 2s | 150ms |
| zstd | 850 MB | 65 MB | 13x | 5s | 180ms |
| gzip | 850 MB | 80 MB | 10.6x | 12s | 250ms |
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 :
- Combinez Tempo avec Prometheus et Grafana pour couvrir métriques, traces et alerting
- Centralisez vos logs avec Loki pour une observabilité three-pillar complète
- Explorez OpenTelemetry pour normaliser votre instrumentation applicative


