Tracing OpenTelemetry en Go pour une visibilité API de bout en bout
Le tracing OpenTelemetry en Go expliqué avec des étapes pratiques pour corréler traces, métriques et logs à travers les requêtes HTTP, les jobs en arrière-plan et les appels tiers.

Ce que signifie la traçabilité de bout en bout pour une API Go
Un trace est la chronologie d'une requête pendant qu'elle traverse votre système. Il commence quand un appel API arrive et se termine quand vous envoyez la réponse.
À l'intérieur d'un trace se trouvent des spans. Un span est une étape chronométrée, comme « parser la requête », « exécuter une requête SQL » ou « appeler le fournisseur de paiement ». Les spans peuvent aussi contenir des détails utiles, comme un code de statut HTTP, un identifiant d'utilisateur non sensible ou le nombre de lignes renvoyées par une requête.
« De bout en bout » signifie que le trace ne s'arrête pas au premier handler. Il suit la requête à travers les endroits où les problèmes se cachent habituellement : middleware, requêtes base de données, caches, jobs en arrière-plan, API tierces (paiements, e‑mail, cartographie) et autres services internes.
Le tracing est particulièrement utile quand les problèmes sont intermittents. Si une requête sur 200 est lente, les logs ressemblent souvent à ceux des requêtes rapides. Un trace rend la différence évidente : une requête a passé 800 ms à attendre un appel externe, a été relancée deux fois, puis a déclenché un job de suivi.
Les logs sont aussi difficiles à relier entre services. Vous pouvez avoir une ligne de log dans l'API, une autre dans un worker, et rien entre les deux. Avec le tracing, ces événements partagent le même trace_id, donc vous pouvez suivre la chaîne sans deviner.
Traces, métriques et logs : comment ils s'articulent
Traces, métriques et logs répondent à des questions différentes.
Les traces montrent ce qui s'est passé pour une requête réelle. Elles indiquent où le temps a été passé dans votre handler, vos appels DB, vos recherches en cache et vos requêtes tierces.
Les métriques montrent la tendance. Elles sont idéales pour les alertes parce qu'elles sont stables et peu coûteuses à agréger : percentiles de latence, taux de requêtes, taux d'erreur, profondeur des files, saturation.
Les logs donnent le « pourquoi » en texte clair : échecs de validation, entrées inattendues, cas limites et décisions prises par votre code.
Le vrai avantage, c'est la corrélation. Quand le même trace ID apparaît dans les spans et les logs structurés, vous pouvez sauter d'un log d'erreur au trace exact et voir immédiatement quelle dépendance a ralenti ou quelle étape a échoué.
Un modèle mental simple
Utilisez chaque signal pour ce pour quoi il est le meilleur :
- Les métriques vous disent que quelque chose ne va pas.
- Les traces montrent où le temps a été passé pour une requête.
- Les logs expliquent ce que votre code a décidé et pourquoi.
Exemple : votre endpoint POST /checkout commence à timeout. Les métriques montrent un pic sur le p95. Un trace montre que la majorité du temps est passée dans un appel au fournisseur de paiement. Un log corrélé à l'intérieur de ce span montre des retries à cause d'un 502, ce qui oriente vers les réglages de backoff ou un incident en amont.
Avant d'ajouter du code : nommage, échantillonnage et quoi tracer
Un peu de planification en amont rendra les traces recherchables plus tard. Sans cela, vous collecterez quand même des données, mais des questions basiques deviennent difficiles : « Était-ce staging ou prod ? », « Quel service a déclenché le problème ? »
Commencez par une identité cohérente. Choisissez un service.name clair pour chaque API Go (par exemple checkout-api) et un champ d'environnement unique comme deployment.environment=dev|staging|prod. Gardez ces valeurs stables. Si les noms changent en milieu de semaine, les graphiques et les recherches sembleront provenir de systèmes différents.
Ensuite, décidez de l'échantillonnage. Tracer chaque requête est bien en développement, mais souvent trop coûteux en production. Une approche courante est d'échantillonner un petit pourcentage du trafic normal et de conserver systématiquement les traces des erreurs et des requêtes lentes. Si vous savez déjà que certains endpoints sont très fréquentés (health checks, polling), tracez-les moins ou pas du tout.
Enfin, mettez-vous d'accord sur ce que vous tagguez sur les spans et ce que vous n'allez jamais collecter. Gardez une courte allowlist d'attributs qui aident à relier les événements entre services et écrivez des règles simples de confidentialité.
De bons tags incluent généralement des ID stables et des informations grossières sur la requête (template de route, méthode, code de statut). Évitez complètement les payloads sensibles : mots de passe, données de paiement, emails complets, tokens d'authentification et corps de requête bruts. Si vous devez inclure des valeurs liées à l'utilisateur, hachez‑les ou redigez‑les avant de les ajouter.
Pas à pas : ajouter OpenTelemetry à une API HTTP en Go
Vous configurerez un tracer provider une fois au démarrage. Il décide où vont les spans et quels attributs de ressource sont attachés à chaque span.
1) Initialiser OpenTelemetry
Assurez‑vous de définir service.name. Sans cela, les traces provenant de services différents peuvent se mélanger et les graphiques deviennent difficiles à lire.
// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())
res, _ := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceName("checkout-api"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
C'est la base du tracing OpenTelemetry en Go. Ensuite, vous aurez besoin d'un span par requête entrante.
2) Ajouter du middleware HTTP et capturer les champs clés
Utilisez un middleware HTTP qui démarre automatiquement un span et enregistre le code de statut et la durée. Nommez le span avec le template de route (par exemple /users/:id), pas l'URL brute, sinon vous vous retrouverez avec des milliers de chemins uniques.
Visez une ligne de base propre : un span serveur par requête, des noms de span basés sur la route, le statut HTTP capturé, les échecs de handler reflétés comme erreurs de span, et la durée visible dans votre visualiseur de traces.
3) Rendre les échecs évidents
Quand quelque chose tourne mal, renvoyez une erreur et marquez le span courant comme échoué. Cela fait ressortir le trace avant même que vous n'ouvriez les logs.
Dans les handlers, vous pouvez faire :
span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
4) Vérifier les trace IDs localement
Lancez l'API et appelez un endpoint. Journalisez le trace ID depuis le contexte de la requête une fois pour confirmer qu'il change à chaque requête. S'il est toujours vide, votre middleware n'utilise pas le même context que celui que reçoit votre handler.
Transporter le contexte à travers la BD et les appels tiers
La visibilité de bout en bout se casse dès que vous perdez le context.Context. Le contexte entrant d'une requête doit être le fil que vous passez à chaque appel DB, requête HTTP et helper. Si vous le remplacez par context.Background() ou oubliez de le transmettre, votre trace se transforme en travaux séparés et non liés.
Pour les requêtes HTTP sortantes, utilisez un transport instrumenté afin que chaque Do(req) devienne un span enfant du requête courante. Transmettez les en‑têtes W3C (traceparent style) sur les requêtes sortantes pour que les services en aval puissent attacher leurs spans au même trace.
Les appels base de données nécessitent le même traitement. Utilisez un driver instrumenté ou encapsulez les appels avec des spans autour de QueryContext et ExecContext. N'enregistrez que des détails sûrs. Vous voulez identifier les requêtes lentes sans divulguer de données.
Des attributs utiles et peu risqués incluent un nom d'opération (par exemple SELECT user_by_id), le nom de la table ou du modèle, le nombre de lignes (compte seulement), la durée, le nombre de retries et un type d'erreur grossier (timeout, canceled, contrainte).
Les timeouts font partie de l'histoire, pas seulement des échecs. Fixez‑les avec context.WithTimeout pour les appels DB et tiers, et laissez les annulations remonter. Quand un appel est annulé, marquez le span comme erreur et ajoutez une raison courte comme deadline_exceeded.
Tracer les jobs en arrière-plan et les queues
Le travail en arrière‑plan est souvent l'endroit où les traces s'arrêtent. Une requête HTTP se termine, puis un worker prend un message plus tard sur une machine différente sans contexte partagé. Si vous ne faites rien, vous obtenez deux récits : le trace API et un trace job qui semble commencer de nulle part.
La solution est simple : quand vous mettez en file un job, capturez le contexte de trace courant et stockez‑le dans les métadonnées du job (payload, en‑têtes ou attributs, selon votre queue). Quand le worker démarre, extrayez ce contexte et démarrez un nouveau span en tant qu'enfant de la requête originale.
Propager le contexte en sécurité
Copiez seulement le contexte de trace, pas les données utilisateur.
- Injectez uniquement les identifiants de trace et les drapeaux d'échantillonnage (style W3C traceparent).
- Gardez‑les séparés des champs métier (par exemple un champ dédié "otel" ou "trace").
- Traitez‑les comme des données non fiables lors de leur lecture (validez le format, gérez les données manquantes).
- Évitez de mettre des tokens, emails ou corps de requête dans les métadonnées du job.
Spans à ajouter (sans transformer les traces en bruit)
Les traces lisibles ont généralement quelques spans significatifs, pas des dizaines de tout petits spans. Créez des spans autour des frontières et des « points d'attente ». Un bon point de départ est un span enqueue dans le handler API et un span job.run dans le worker.
Ajoutez un petit nombre d'attributs : numéro de tentative, nom de la queue, type de job et taille du payload (pas le contenu). Si des retries ont lieu, enregistrez‑les comme spans séparés ou événements afin de voir les délais de backoff.
Les tâches planifiées ont aussi besoin d'un parent. S'il n'y a pas de requête entrante, créez une nouvelle racine pour chaque exécution et taggez‑la avec un nom de schedule.
Corréler les logs avec les traces (et garder les logs sûrs)
Les traces vous disent où le temps a été passé. Les logs vous disent ce qui s'est passé et pourquoi. La façon la plus simple de les relier est d'ajouter trace_id et span_id à chaque entrée de log comme champs structurés.
En Go, récupérez le span actif depuis le context.Context et enrichissez votre logger une fois par requête (ou par job). Ainsi, chaque ligne de log pointe vers un trace précis.
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)
C'est suffisant pour sauter d'une entrée de log au span exact qui tournait au moment où elle a été écrite. Cela rend aussi le manque de contexte évident : trace_id sera vide.
Garder les logs utiles sans divulguer de PII
Les logs vivent souvent plus longtemps et voyagent plus loin que les traces, soyez donc plus stricts. Préférez des identifiants stables et des résultats : user_id, order_id, payment_provider, status, et error_code. Si vous devez logger une saisie utilisateur, redigez‑la d'abord et limitez les longueurs.
Faciliter le regroupement des erreurs
Utilisez des noms d'événements et des types d'erreur cohérents afin de pouvoir les compter et les rechercher. Si le libellé change à chaque fois, le même problème semblera être plusieurs problèmes différents.
Ajouter des métriques qui aident vraiment à trouver les problèmes
Les métriques sont votre système d'alerte précoce. Dans une configuration qui utilise déjà le tracing OpenTelemetry en Go, les métriques doivent répondre : combien, à quel point, depuis quand.
Commencez par un petit ensemble qui fonctionne pour presque toutes les API : nombre de requêtes, nombre d'erreurs (par classe de statut), percentiles de latence (p50, p95, p99), requêtes en cours, et latence des dépendances pour votre DB et vos appels tiers clés.
Pour aligner les métriques avec les traces, utilisez les mêmes templates de route et noms. Si vos spans utilisent /users/{id}, vos métriques devraient en faire de même. Alors quand un graphique montre « p95 pour /checkout en hausse », vous pouvez aller directement aux traces filtrées sur cette route.
Faites attention aux labels (attributs). Un mauvais label peut exploser les coûts et rendre les tableaux de bord inutilisables. Template de route, méthode, classe de statut et nom du service sont généralement sûrs. Les user IDs, emails, URLs complètes et messages d'erreur bruts le sont rarement.
Ajoutez quelques métriques personnalisées pour les événements critiques métier (par exemple checkout démarré/terminé, échecs de paiement par groupe de code résultat, job en arrière‑plan succès vs retry). Gardez l'ensemble petit et retirez ce que vous n'utilisez jamais.
Exporter la télémétrie et déployer en toute sécurité
L'exportation est l'endroit où OpenTelemetry devient concret. Votre service doit envoyer spans, métriques et logs quelque part de fiable sans ralentir les requêtes.
En développement local, gardez ça simple. Un exporter console (ou OTLP vers un collector local) vous permet de voir rapidement les traces et de valider les noms de spans et les attributs. En production, préférez OTLP vers un agent ou un OpenTelemetry Collector proche du service. Cela vous donne un point unique pour gérer retries, routage et filtrage.
Le batching est important. Envoyez la télémétrie par lots à intervalles courts, avec des timeouts serrés pour qu'un réseau bloqué n'empêche pas votre appli. La télémétrie ne doit pas être sur le chemin critique. Si l'exporter ne suit pas, il doit lâcher des données plutôt que de consommer toute la mémoire.
L'échantillonnage maintient les coûts prévisibles. Commencez par un échantillonnage head‑based (par exemple 1–10% des requêtes), puis ajoutez des règles simples : toujours échantillonner les erreurs et les requêtes lentes au‑delà d'un seuil. Si vous avez des jobs en arrière‑plan très volumineux, échantillonnez‑les à des taux plus faibles.
Déployez par étapes : dev avec 100% d'échantillonnage, staging avec du trafic réaliste et un échantillonnage plus faible, puis prod avec un échantillonnage prudent et des alertes sur les échecs d'exporter.
Erreurs courantes qui ruinent la visibilité de bout en bout
La visibilité de bout en bout échoue le plus souvent pour des raisons simples : les données existent, mais elles ne se connectent pas.
Les problèmes qui cassent le tracing distribué en Go sont habituellement :
- Perdre le contexte entre les couches. Un handler crée un span, mais un appel DB, un client HTTP ou une goroutine utilise
context.Background()au lieu du contexte de la requête. - Retourner des erreurs sans marquer les spans. Si vous n'enregistrez pas l'erreur et ne mettez pas le statut du span, les traces semblent « vertes » alors que les utilisateurs voient des 500.
- Instrumenter tout et n'importe quoi. Si chaque helper devient un span, les traces deviennent du bruit et coûtent plus cher.
- Ajouter des attributs à haute cardinalité. URLs complètes avec IDs, emails, valeurs SQL brutes, corps de requête ou chaînes d'erreur brutes peuvent générer des millions de valeurs uniques.
- Juger la performance par la moyenne. Les incidents apparaissent dans les percentiles (p95/p99) et le taux d'erreur, pas dans la latence moyenne.
Un contrôle de sanity rapide : prenez une requête réelle et suivez‑la à travers les frontières. Si vous ne voyez pas un trace ID circuler dans la requête entrante, la requête DB, l'appel tiers et le worker asynchrone, vous n'avez pas encore la visibilité de bout en bout.
Une checklist pratique pour considérer la tâche comme "terminée"
Vous êtes proche du but quand vous pouvez partir d'un rapport utilisateur jusqu'à la requête exacte, puis la suivre à travers chaque étape.
- Prenez une ligne de log API et localisez le trace exact par
trace_id. Confirmez que des logs plus profonds de la même requête (DB, client HTTP, worker) portent le même contexte de trace. - Ouvrez le trace et vérifiez la hiérarchie : un span serveur HTTP en haut, avec des spans enfants pour les appels DB et les API tierces. Une liste plate signifie souvent que le contexte a été perdu.
- Déclenchez un job en arrière‑plan depuis une requête API (comme l'envoi d'un reçu par e‑mail) et confirmez que le span du worker se rattache à la requête.
- Vérifiez les métriques de base : nombre de requêtes, taux d'erreur et percentiles de latence. Assurez‑vous de pouvoir filtrer par route ou opération.
- Scannez les attributs et les logs pour la sécurité : pas de mots de passe, tokens, numéros de carte complets ou données personnelles brutes.
Un test simple : simulez un checkout lent où le fournisseur de paiement répond avec retard. Vous devriez voir un trace unique avec un span externe clairement nommé, plus un pic de métrique sur le p95 de la route checkout.
Si vous générez des backends Go (par exemple avec AppMaster), il est utile d'intégrer cette checklist dans votre routine de release pour que les nouveaux endpoints et workers restent traçables au fur et à mesure que l'application grandit. AppMaster (appmaster.io) génère de vrais services Go, donc vous pouvez standardiser une configuration OpenTelemetry et la réutiliser sur les services et jobs en arrière‑plan.
Exemple : déboguer un checkout lent à travers plusieurs services
Un client signale : « Le checkout bloque parfois. » Vous ne pouvez pas forcément le reproduire à la demande, c'est exactement quand le tracing OpenTelemetry en Go est utile.
Commencez par les métriques pour comprendre la forme du problème. Regardez le taux de requêtes, le taux d'erreurs et la latence p95 ou p99 pour l'endpoint checkout. Si le ralentissement arrive par rafales et seulement pour une partie des requêtes, cela pointe généralement vers une dépendance, de la mise en file ou un comportement de retry plutôt que le CPU.
Ensuite, ouvrez un trace lent dans la même fenêtre temporelle. Un seul trace suffit souvent. Un checkout sain peut faire 300 à 600 ms de bout en bout. Un mauvais peut faire 8 à 12 secondes, avec la plupart du temps dans un seul span.
Un schéma courant : le handler API est rapide, le travail DB est globalement correct, puis un span du fournisseur de paiement montre des retries avec backoff, et un appel en aval attend derrière un verrou ou une queue. La réponse peut même renvoyer 200, donc des alertes basées uniquement sur les erreurs ne se déclenchent jamais.
Les logs corrélés vous donnent ensuite le chemin exact en clair : « retrying Stripe charge: timeout », suivi de « db tx aborted: serialization failure », puis « retry checkout flow ». C'est un signal clair que plusieurs petits problèmes combinés créent une mauvaise expérience utilisateur.
Une fois le goulot identifié, la cohérence garde la lecture claire sur la durée. Standardisez les noms de spans, les attributs (hash d'user_id sûr, order_id, nom de dépendance) et les règles d'échantillonnage entre services pour que tout le monde lise les traces de la même façon.


