📍 Article principal : Observabilité applicative en 2026 : OpenTelemetry, traces distribuées et stack LGTM — pour le contexte conceptuel et l’architecture d’ensemble.
Pourquoi le tail-based sampling est la stratégie qui paie
Au-delà d’un certain volume, garder 100 % des traces devient économiquement inacceptable. Le réflexe naïf est de réduire le débit côté SDK avec un sampler probabiliste à 1 ou 5 %. Cette approche, dite head-based sampling, présente un défaut majeur : elle décide de garder ou non une trace dès le premier span, sans connaître la suite. On jette à l’aveugle 95 ou 99 % du trafic, y compris les traces qui auraient terminé en erreur ou avec une latence anormale — précisément celles qu’on aurait voulu garder pour investiguer.
Le tail-based sampling inverse la logique : on attend la fin de la trace, on examine ce qu’elle contient (durée totale, présence d’erreurs, attributs métier), et on décide alors de la garder ou non. Une politique typique conserve 100 % des traces en erreur, 100 % des traces lentes, et un échantillon de 5 % des autres. Le résultat est une réduction massive du volume sans perte d’information utile pour l’investigation.
Ce tutoriel met en place le tail-based sampling dans un OpenTelemetry Collector, d’abord en mode mono-instance pour comprendre les politiques, puis en mode distribué à deux couches qui passe à l’échelle.
Prérequis
- Un Collector OpenTelemetry contrib fonctionnel — voir le tutoriel Configurer un OpenTelemetry Collector
- Une instance Tempo qui ingère les traces — voir Envoyer les traces vers Tempo
- Une application qui produit des traces avec une diversité de durées et de statuts
- Notions de YAML et compréhension de l’architecture du Collector
- Temps estimé : 40 à 55 minutes
Étape 1 — Comprendre la contrainte d’unicité de l’instance
Le processor tail_sampling a une contrainte structurelle simple à formuler mais essentielle à respecter : tous les spans d’une trace donnée doivent arriver sur la même instance de Collector. Sans cette garantie, l’instance ne voit qu’une partie de la trace et prend une décision incohérente, ou pire, plusieurs instances prennent des décisions différentes pour la même trace.
En mode mono-instance (un seul Collector), la contrainte est triviale : tout arrive au même endroit. En mode multi-instances pour la haute disponibilité ou la montée en charge, il faut une couche en amont qui route par trace_id vers une instance précise — c’est ce que fait l’exporter loadbalancing, qu’on configure à l’étape 6.
Étape 2 — Configuration du processor en mono-instance
On commence par le cas simple : un Collector unique qui reçoit les traces, les passe au processor tail_sampling, puis exporte vers Tempo. La configuration de base définit la fenêtre d’attente, la capacité, et les politiques.
# otelcol.yaml (extrait)
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 500
- name: sample-rest
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resource, tail_sampling, batch]
exporters: [otlp/tempo]
Trois paramètres clés gouvernent le comportement. decision_wait est le délai après lequel une trace est considérée comme terminée et soumise aux politiques — 10 secondes est un défaut raisonnable pour des requêtes web typiques. num_traces est la capacité maximale de traces simultanément en mémoire ; au-delà, les nouvelles traces sont rejetées. expected_new_traces_per_sec aide le Collector à pré-allouer les structures internes.
Important : l’ordre dans la liste de processors place tail_sampling avant batch. C’est cohérent — on prend la décision puis on bat, pas l’inverse. Mettre batch avant briserait la sémantique du sampling.
Étape 3 — Anatomie des politiques
Les politiques s’évaluent dans l’ordre. Dès qu’une politique vote sample, la trace est conservée — pas besoin d’évaluer les suivantes. Si toutes votent not_sample, la trace est rejetée. Cette logique permet d’organiser les politiques du plus prioritaire au plus permissif.
Dans la configuration ci-dessus :
- errors-policy garde 100 % des traces dont au moins un span a un statut ERROR
- slow-traces-policy garde 100 % des traces dont la durée totale dépasse 500 ms
- sample-rest est la politique de fond qui garde 5 % des traces restantes par échantillonnage probabiliste sur le
trace_id
Cette combinaison capture l’essentiel : tout ce qui erreure, tout ce qui est lent, et un échantillon représentatif du trafic normal pour les besoins de tendance et de comparaison. Elle réduit typiquement le volume de 90 % sans perdre l’utile.
Étape 4 — Politiques avancées par attribut
Les politiques ci-dessus couvrent les cas génériques. Pour des besoins métier précis (garder 100 % des traces d’un endpoint critique, exclure systématiquement le bruit d’un health check), on utilise des politiques par attribut.
policies:
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
- name: keep-slow
type: latency
latency:
threshold_ms: 500
- name: keep-payment-route
type: string_attribute
string_attribute:
key: http.route
values: ["/api/payment", "/api/checkout"]
- name: drop-healthcheck
type: drop
drop:
drop_sub_policy:
- name: is-healthcheck
type: string_attribute
string_attribute:
key: http.route
values: ["/health", "/ready"]
- name: sample-rest
type: probabilistic
probabilistic:
sampling_percentage: 5
La politique keep-payment-route garde 100 % des traces qui contiennent un span dont l’attribut http.route est /api/payment ou /api/checkout — utile pour les flux critiques où l’on veut une visibilité totale. La politique drop-healthcheck de type drop exclut explicitement les traces de health check : les politiques drop ont précédence sur les politiques sample, donc une trace qui matche un drop_sub_policy est rejetée même si une autre politique voterait pour la conserver. C’est le mécanisme officiel pour exclure des routes du sampling, et il diffère d’une approche naïve par and + probabilistic 0 qui ne fonctionne pas (les autres politiques top-level peuvent toujours voter sample).
Étape 5 — Surveiller le processor lui-même
Le Collector expose des métriques internes sur le tail sampling qu’il faut superviser. Les compteurs clés :
otelcol_processor_tail_sampling_global_count_traces_sampled— total trace décidées par politiqueotelcol_processor_tail_sampling_count_traces_sampled— par décision (sampled/not_sampled) et par politiqueotelcol_processor_tail_sampling_sampling_decision_timer_latency— temps de décisionotelcol_processor_tail_sampling_traces_on_memory— nombre de traces actuellement en mémoire
Ces métriques permettent de vérifier que les politiques se déclenchent comme prévu et que le processor n’est pas en saturation. Une saturation se manifeste par des traces rejetées (traces_on_memory qui plafonne à num_traces), ce qui demande soit d’augmenter la capacité, soit d’ajouter une instance et de passer à la couche distribuée.
Étape 6 — Architecture distribuée à deux couches
Quand le volume dépasse ce qu’une instance unique tient, on passe à une architecture en deux couches. La première couche (load balancer layer) reçoit toutes les traces et les redirige par trace_id vers la seconde couche (tail sampling layer) qui exécute les politiques. Cette topologie garantit que tous les spans d’une trace arrivent sur la même instance de la seconde couche, condition nécessaire pour des décisions cohérentes.
# Couche 1 : load balancer
# otelcol-lb.yaml
exporters:
loadbalancing:
routing_key: traceID
protocol:
otlp:
tls:
insecure: true
resolver:
static:
hostnames:
- tail-sampling-1:4317
- tail-sampling-2:4317
- tail-sampling-3:4317
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loadbalancing]
La clé est routing_key: traceID. L’exporter calcule un hash du trace_id et choisit l’instance de destination de manière déterministe. Tous les spans d’une trace finissent sur la même instance de la couche tail. Le resolver static liste les destinataires manuellement ; en Kubernetes, le resolver k8s ou dns détecte les pods de la couche tail à la volée et adapte la liste.
# Couche 2 : tail sampling
# otelcol-tail.yaml
processors:
tail_sampling:
decision_wait: 10s
num_traces: 200000
expected_new_traces_per_sec: 5000
policies:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 500
- name: sample-rest
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
La couche 2 est identique au mode mono-instance, à un détail près : la capacité num_traces et expected_new_traces_per_sec sont dimensionnées pour la part du trafic qu’une instance reçoit, pas pour le total. Avec trois instances tail-sampling, chacune gère environ un tiers du trafic.
Étape 7 — Choisir le decision_wait
Le decision_wait est le compromis le plus délicat à régler. Trop court, on prend la décision avant la fin de certaines traces et on fragmente les arbres. Trop long, on retient des traces en mémoire et on consomme des ressources. La règle empirique est decision_wait = p99_durée_trace + marge.
Pour des requêtes web typiques (durée majoritairement sous 1 seconde), 10 secondes laissent une marge confortable. Pour des batchs ou des opérations longues (export de fichier, traitement asynchrone), il faut monter à 60 secondes voire plus. À chaque environnement de mesurer la distribution réelle des durées de trace et d’ajuster.
Étape 8 — Vérifier l’efficacité du sampling
Le test final consiste à valider que les politiques se comportent comme attendu sur un échantillon de trafic réel. On compare le nombre de traces ingérées à la couche LB (avant sampling) au nombre de traces qui atteignent Tempo (après sampling). Le ratio donne directement l’efficacité du sampling.
# Métriques typiques à comparer
# couche LB
sum(rate(otelcol_receiver_accepted_spans{job="otel-lb"}[5m]))
# couche tail (après sampling)
sum(rate(otelcol_exporter_sent_spans{job="otel-tail"}[5m]))
Avec une politique conservant 100 % erreurs + 100 % lentes + 5 % du reste, on s’attend à un ratio d’environ 5-15 % selon la prévalence d’erreurs et de traces lentes dans le trafic. Si le ratio est plus élevé, l’une des politiques garde plus que prévu — vérifier que threshold_ms n’est pas trop bas. Si le ratio est plus bas, c’est que le trafic est exceptionnellement propre, ou que sampling_percentage est trop conservateur pour les besoins d’analyse.
Erreurs fréquentes
Activer tail_sampling sans la couche load balancer
Avec plusieurs Collectors derrière un load balancer round-robin classique, les spans d’une même trace se répartissent au hasard. Chaque instance ne voit qu’une partie et prend des décisions incohérentes. Symptôme : traces fragmentées dans Tempo, des spans présents et d’autres absents pour la même trace_id. La fix est obligatoire : passer à la topologie à deux couches avec routing_key: traceID.
decision_wait trop court par rapport aux opérations longues
Si l’application contient des spans longs (uploads, batchs), un decision_wait de 10 secondes prend la décision pendant que la trace n’est pas terminée. Les spans postérieurs à la décision ne sont pas pris en compte par la politique. Augmenter decision_wait à 60 ou 120 secondes pour couvrir le pire cas raisonnable.
num_traces sous-dimensionné
Si la capacité maximale est dépassée, les nouvelles traces sont rejetées et le sampling devient incohérent. Surveiller traces_on_memory et l’ajuster en fonction du trafic réel : num_traces ≈ debit × decision_wait × 1.5 pour avoir une marge.
Politique probabiliste avant les politiques spécifiques
L’ordre des politiques compte : la première à voter sample emporte la décision. Mettre la politique probabiliste de 5 % avant errors-policy peut faire perdre des traces en erreur quand le tirage probabiliste tombe sur not_sample. Mettre toujours les politiques de garde absolue (erreurs, latence, routes critiques) en premier.
Confondre tail sampling et head sampling SDK
Activer un sampler 1 % côté SDK puis du tail sampling côté Collector revient à appliquer les deux taux successivement : on ne garde que 5 % du déjà-1 %, soit 0,05 %. Désactiver le head sampling côté SDK (OTEL_TRACES_SAMPLER=always_on) et laisser toute la décision au tail sampling.
Tutoriels associés
- Configurer un OpenTelemetry Collector
- Envoyer les traces vers Tempo
- Corréler trace_id entre logs, métriques et traces dans Grafana
- 🔝 Retour à l’article principal — Observabilité applicative et stack LGTM
Ressources et références officielles
- Tail sampling processor : github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor
- Loadbalancing exporter : github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/loadbalancingexporter
- Tail sampling — documentation Grafana : grafana.com/docs/tempo/latest/set-up-for-tracing/instrument-send/set-up-collector/tail-sampling
- Politiques tail sampling — détail : grafana.com/docs/tempo/latest/set-up-for-tracing/instrument-send/set-up-collector/tail-sampling/policies-strategies
- Spec OpenTelemetry — sampling : opentelemetry.io/docs/specs/otel/trace/sdk/#sampling
FAQ
Tail sampling vs head sampling, lequel choisir ?
Tail dans la majorité des cas, parce qu’il prend des décisions informées. Head est plus simple et n’exige pas de Collector centralisé, mais perd l’information sur le résultat de la trace. La combinaison la plus pragmatique est : pas de head sampling (100 % côté SDK), tail sampling dans le Collector quand le volume justifie l’effort.
Combien d’instances tail-sampling pour quel débit ?
Une instance tient confortablement 5 000 à 10 000 traces simultanées en mémoire avec 1 vCPU et 2 Go RAM. Pour 100 000 traces/s sur des durées moyennes de 1 seconde, prévoir 3 à 5 instances avec une légère marge. La supervision de traces_on_memory donne le signal de scale-out.
Le sampling fonctionne-t-il aussi pour les métriques ?
Pas au sens du tail sampling — les métriques sont par construction agrégées et il n’y a pas de « décision a posteriori » à prendre. Pour réduire le volume de métriques, on agit sur la cardinalité (filtrer ou réécrire les attributs côté Collector) et sur la fréquence d’export, pas par sampling.
Comment garder le contexte entre traces samplées et métriques ?
Activer le générateur de span metrics côté Tempo, qui produit des métriques RED à partir de toutes les traces ingérées avant le sampling, ou positionner le générateur dans la couche LB. De cette manière, les métriques reflètent le trafic complet, et le sampling n’affecte que le stockage des traces individuelles.
Peut-on avoir plusieurs politiques de probabilité ?
Oui. On peut par exemple définir 10 % pour les traces du service A et 1 % pour les traces du service B en combinant string_attribute et probabilistic via la composition and. Cette finesse est utile quand certains services génèrent un volume disproportionné par rapport à leur valeur diagnostique.