ITSkillsCenter
Business Digital

Correler trace_id entre logs, metriques et traces dans Grafana pas-a-pas

11 min de lecture

📍 Article principal : Observabilité applicative en 2026 : OpenTelemetry, traces distribuées et stack LGTM — pour le contexte conceptuel et l’architecture d’ensemble.

Pourquoi corréler les trois signaux est le vrai gain

Avoir des logs, des métriques et des traces dans trois outils séparés ne fait pas une plateforme d’observabilité. Ce qui transforme un assemblage en plateforme, c’est la capacité à sauter d’un signal à l’autre en un clic. Une métrique de latence p99 qui dépasse un seuil ouvre directement un échantillon de trace anormale. Cette trace expose un span en erreur sur lequel on clique pour voir les logs de la requête. Les logs montrent le payload exact qui a posé problème. Cette navigation, qui prenait des dizaines de minutes en 2018, prend maintenant trente secondes.

Le ciment qui rend cette navigation possible est le trace_id propagé partout : injecté dans les logs au moment de l’émission, attaché aux métriques sous forme d’exemplars, et porté par la trace elle-même. Ce tutoriel construit pas-à-pas la configuration Grafana qui matérialise ces sauts entre Tempo, Loki et Mimir, à partir d’une stack LGTM déjà en place.

Prérequis

  • Une stack Grafana + Loki + Tempo + Mimir fonctionnelle
  • Au moins une application instrumentée OTel qui produit les trois signaux — voir tutoriels Node.js, Python ou Go
  • Un Collector qui exporte vers les trois backends — voir Configurer un OpenTelemetry Collector
  • Connaissance de base de la configuration Grafana (datasources, dashboards)
  • Temps estimé : 30 à 40 minutes

Étape 1 — Vérifier que le trace_id est présent dans les logs

Avant de configurer Grafana, on s’assure que les logs portent bien le trace_id. Cette injection est automatique dès que le SDK OTel est correctement câblé, mais il arrive qu’elle soit manquante à cause d’un bootstrap incomplet ou d’un logger non instrumenté.

# requête LogQL : filtrer les logs récents avec trace_id non vide
curl -G "http://127.0.0.1:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={service_name="otel-go-demo"} | trace_id != ""' \
  --data-urlencode 'limit=5' | jq

Si la réponse contient des entrées avec un champ trace_id non vide, l’injection fonctionne. Si tous les logs ont un trace_id vide, c’est que le span actif n’est pas correctement propagé au moment de l’émission du log — symptôme typique d’un logger non instrumenté ou d’un span qui n’a pas été ouvert correctement avant l’émission.

Étape 2 — Configurer le datasource Loki avec un derived field trace_id

Le derived field est le mécanisme Grafana qui transforme une portion de log en lien cliquable vers une autre datasource. Pour le trace_id stocké en structured metadata côté Loki, on configure un derived field qui détecte la clé et propose un saut vers Tempo.

# grafana datasource Loki
apiVersion: 1
datasources:
  - name: Loki
    type: loki
    uid: loki
    url: http://loki:3100
    jsonData:
      derivedFields:
        - name: trace_id
          matcherType: label
          matcherRegex: trace_id
          datasourceUid: tempo
          url: '${__value.raw}'
          urlDisplayLabel: 'Voir la trace dans Tempo'

Le matcherType: label dit à Grafana de chercher la valeur dans les labels et les structured metadata. matcherRegex: trace_id est la clé. datasourceUid: tempo dirige vers la datasource Tempo précédemment provisionnée. Une fois Grafana rechargé, chaque ligne de log dans Explore expose une icône de saut au survol du trace_id.

Étape 3 — Activer le saut traces → logs côté Tempo

Le saut inverse, depuis une trace dans Tempo vers les logs correspondants dans Loki, se configure via la propriété tracesToLogsV2 de la datasource Tempo. La logique est de ramener les logs filtrés par trace_id sur la fenêtre temporelle de la trace.

# grafana datasource Tempo
apiVersion: 1
datasources:
  - name: Tempo
    type: tempo
    uid: tempo
    url: http://tempo:3200
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: '-1m'
        spanEndTimeShift: '1m'
        tags:
          - { key: service.name, value: service_name }
        filterByTraceID: true
        filterBySpanID: false
        customQuery: true
        query: '{$${__tags}} | trace_id="$${__span.traceId}"'

Trois éléments importants. spanStartTimeShift et spanEndTimeShift élargissent la fenêtre temporelle pour ne pas manquer les logs proches du span. tags mappe l’attribut OTel service.name vers le label Loki service_name (rappel : les points deviennent des underscores). customQuery avec filterByTraceID: true construit la requête LogQL qui filtre par trace_id exact dans les structured metadata.

Avec cette configuration, ouvrir une trace dans Grafana Explore expose un bouton « Logs for this span » qui ouvre Loki avec les logs de la requête, sans avoir à recopier manuellement le trace_id.

Étape 4 — Activer les exemplars sur Mimir

Les exemplars sont la troisième jambe de la corrélation : ils attachent à un point d’histogramme ou de compteur un échantillon de trace_id représentatif. Concrètement, sur un graphique de latence dans Grafana, certains points affichent un petit losange — cliquer ce losange ouvre la trace correspondante.

Côté SDK, les exemplars sont attachés automatiquement aux métriques quand l’instrument est mis à jour à l’intérieur d’un span actif. Côté Collector, le processor resource et la chaîne d’export OTLP les transmettent intactes. Côté Mimir, l’option doit être activée pour qu’ils soient stockés et requêtables.

# mimir.yaml
limits:
  max_global_exemplars_per_user: 100000
  exemplar_age_limit: 5m

La limite par tenant protège contre une saturation mémoire si le nombre d’exemplars devient déraisonnable. La durée d’âge maximum (5 minutes) limite la profondeur de l’historique conservé — c’est suffisant pour la corrélation interactive.

Étape 5 — Configurer le datasource Mimir pour les exemplars

Côté Grafana, on indique au datasource Prometheus qu’il pointe sur Tempo pour résoudre les trace_id trouvés dans les exemplars.

apiVersion: 1
datasources:
  - name: Mimir
    type: prometheus
    uid: mimir
    url: http://mimir:9009/prometheus
    jsonData:
      exemplarTraceIdDestinations:
        - name: trace_id
          datasourceUid: tempo
          urlDisplayLabel: 'Voir trace dans Tempo'

Une fois cette configuration en place et un dashboard ouvert, les graphiques d’histogrammes affichent des marqueurs d’exemplars. Sur un graphique de p99 de latence, ces marqueurs représentent des traces réelles aux instants où la latence a effectivement dépassé le percentile. Cliquer un marqueur ouvre la trace dans Tempo.

Étape 6 — Vérifier les trois sauts

À ce stade, les trois corrélations sont câblées. On vérifie qu’elles fonctionnent toutes par un parcours type :

  • Logs → Trace : Grafana Explore sur Loki, requête sur le service, repérer un trace_id, cliquer le derived field, vérifier que la trace s’ouvre dans Tempo
  • Trace → Logs : Grafana Explore sur Tempo, ouvrir une trace, cliquer « Logs for this span », vérifier que Loki affiche les logs filtrés par trace_id
  • Métrique → Trace : Grafana panel sur une métrique d’histogramme, vérifier la présence de marqueurs d’exemplars, cliquer un marqueur, vérifier que la trace s’ouvre dans Tempo

Si l’un des sauts échoue, le diagnostic remonte généralement à un mismatch d’attribut (service.name vs service_name), à une absence de trace_id dans le signal source, ou à une datasource UID mal référencée. Les outils de debug de Grafana (panel Inspector, requêtes brutes en bas du panel) aident à localiser la cassure exacte.

Étape 7 — Construire un dashboard de corrélation

Une fois les sauts en place, on construit un dashboard typique d’investigation qui combine les trois signaux. Le pattern est : ligne du haut avec les métriques RED par service, ligne du milieu avec les exemplars de latence par route, ligne du bas avec un panel logs filtré par variable de service.

// extrait JSON Grafana (simplifié)
{
  "title": "Service overview",
  "templating": {
    "list": [{
      "name": "service",
      "type": "query",
      "datasource": "Mimir",
      "query": "label_values(http_server_request_duration_seconds_count, service_name)"
    }]
  },
  "panels": [
    { "title": "Request rate", "targets": [{ "expr": "sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=\"$service\"}[5m]))" }] },
    { "title": "p99 latency (with exemplars)", "targets": [{ "expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=\"$service\"}[5m])))", "exemplar": true }] },
    { "title": "Logs", "type": "logs", "datasource": "Loki", "targets": [{ "expr": "{service_name=\"$service\"}" }] }
  ]
}

Un dashboard de ce type devient un point d’entrée naturel pour l’investigation : on choisit le service, on voit l’état RED, on clique un exemplar de latence pour ouvrir une trace anormale, on saute aux logs depuis la trace. Ce parcours est ce qui matérialise concrètement la valeur diagnostique de la stack LGTM.

Étape 8 — Valider la couverture

La corrélation ne vaut que si les trois signaux sont émis pour les mêmes requêtes. Un service qui produit des traces mais pas de logs, ou des métriques mais pas de traces, casse la chaîne. Une bonne hygiène est de définir une checklist d’observabilité par service avant qu’il ne passe en production :

  • Le service produit des traces avec service.name et deployment.environment
  • Le service produit des logs structurés JSON avec trace_id et span_id injectés
  • Le service expose au moins les métriques RED (rate, errors, duration) sur les routes principales
  • Au moins un dashboard service-level reprend les trois signaux et permet la navigation croisée
  • Le test de bout en bout (un trafic généré, observé dans les trois sources) est documenté et reproduit en CI/CD

Erreurs fréquentes

Mismatch d’attribut entre Tempo et Loki

Tempo expose service.name (avec point), Loki indexe service_name (avec underscore). Le mappage tags dans tracesToLogsV2 doit être explicite. Sans lui, le saut Trace → Logs filtre sur un attribut qui n’existe pas et retourne vide.

Logger non instrumenté

Si le service utilise un logger maison ou une vieille version sans support OTel, le trace_id n’est pas injecté dans les logs. Côté Python, vérifier que logging.getLogger est utilisé et instrumenté. Côté Node.js, brancher Pino ou Winston via le transport OTel adéquat. Côté Go, utiliser slog récent qui supporte les attributs contextuels.

Exemplars désactivés côté SDK

Certains SDK désactivent les exemplars par défaut dans certains profils. Vérifier la documentation du SDK utilisé et activer explicitement si nécessaire. Sans exemplars côté source, aucune corrélation Métrique → Trace ne sera possible côté Grafana, indépendamment de la configuration Mimir.

Datasource UID incohérents

La référence datasourceUid: tempo dans la datasource Loki doit correspondre exactement au uid: tempo de la datasource Tempo. Une faute de frappe ou un UID auto-généré qui n’est pas explicité produit des liens cassés. Toujours fixer le UID dans le provisioning.

Fenêtre temporelle trop étroite

Si spanStartTimeShift et spanEndTimeShift sont à zéro, le saut Trace → Logs cherche dans la fenêtre exacte du span, ce qui peut manquer des logs émis juste avant ou juste après. Les valeurs -1m et +1m sont un compromis raisonnable.

Tutoriels associés

Ressources et références officielles

FAQ

La corrélation fonctionne-t-elle aussi sans Tempo (juste logs + métriques) ?

Oui, mais on perd l’information de chemin de la requête. Les exemplars ne pointent vers rien d’utile, et le saut depuis un log d’erreur s’arrête à la fenêtre Loki. Tempo n’est pas obligatoire mais sa valeur ajoutée diagnostique est telle qu’on l’ajoute presque toujours dès que l’application a plus d’un service.

Quel est le coût en stockage des exemplars ?

Très faible. Mimir stocke un nombre fixe d’exemplars par série temporelle, en général moins de 1 % du coût total. Le bénéfice diagnostique est sans commune mesure avec ce surcoût.

Peut-on faire la corrélation hors stack LGTM ?

Oui, dès que la datasource cible expose une URL paramétrable. Grafana est agnostique du backend tant que le mécanisme de derived field ou de tracesToLogsV2 trouve une URL valide à construire. Plusieurs combinaisons (Loki + Jaeger + Prometheus, ou Elasticsearch + Tempo + Mimir) fonctionnent avec les mêmes principes.

Comment tester la chaîne en CI ?

Un test d’intégration qui démarre la stack en docker-compose, lance un peu de trafic, puis vérifie via les API Loki/Tempo/Mimir la présence cohérente des trois signaux pour le même trace_id. Cette vérification automatisée empêche une régression silencieuse de l’instrumentation.

La corrélation marche-t-elle pour les logs émis hors d’un span ?

Non par construction : un log émis sans span actif n’a pas de trace_id à attacher. Pour qu’un log d’erreur de démarrage applicatif soit corrélable, il faut soit ouvrir un span racine artificiel autour du démarrage, soit accepter que ces logs n’auront pas de saut associé.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité