Observabilité .NET avec OpenTelemetry
Trois signaux, une même plomberie
trace_id , et côté .NET un span est une System.Diagnostics.Activity . Les métriques agrègent compteurs et histogrammes (taux de requêtes, latence p95) au travers d'un System.Diagnostics.Metrics.Meter . Les logs passent par ILogger , désormais rattachés au trace_id du contexte courant.
Activity et les Meter que votre code et les bibliothèques produisent déjà, puis les exporte. On l'installe via le paquet OpenTelemetry.Extensions.Hosting et on le câble au démarrage.
ConfigureResource fixe le service.name qui identifie l'app dans le backend. Le reste déclare ce qu'on écoute et où on l'envoie.
Auto-instrumentation et spans manuels
AddAspNetCoreInstrumentation crée un span par requête entrante ; AddHttpClientInstrumentation en crée un par appel sortant et y injecte le contexte. Ces instrumentations suivent les *semantic conventions* d'OpenTelemetry : les attributs portent des noms normalisés ( http.request.method , url.path , http.response.status_code ), ce qui rend une trace lisible quel que soit le backend.
ActivitySource maison ne produit rien tant que son nom n'est pas déclaré via AddSource . Sans cette ligne (présente dans la config plus haut), StartActivity renvoie null et le span disparaît en silence.
?. n'est pas de la prudence décorative : quand aucun listener n'échantillonne le span, StartActivity renvoie null , et le code doit continuer sans broncher. Les attributs ( SetTag ) transforment la trace en outil de debug : on filtre sur order.total > 1000 , on repère le span exact qui a explosé en latence, et SetStatus(Error) marque la trace comme fautive pour la faire remonter en tête des recherches.
Compter avec les métriques
Meter . En ASP.NET Core, on l'obtient par injection avec IMeterFactory plutôt qu'en champ statique, ce qui laisse le SDK gérer son cycle de vie.
SuperDev.Orders doit correspondre à l' AddMeter de la config, même règle que pour les traces. Attention au choix des dimensions : order.channel prend quelques valeurs, mais un order.id en tag ferait exploser la cardinalité, car chaque valeur distincte crée une série temporelle. Les identifiants uniques ont leur place sur un span, jamais sur un compteur.
trace_id et le span_id du contexte à chaque entrée ILogger . Depuis un span lent, on saute alors aux logs exacts de cette requête, sans grep sur un horodatage approximatif.
Propager le contexte entre services
AddHttpClientInstrumentation sérialise le contexte courant dans l'en-tête traceparent (de forme 00-{trace_id}-{span_id}-{flags} ), et l'instrumentation ASP.NET Core du service appelé le relit pour rattacher son span au bon parent. Rien à câbler tant qu'on reste en HTTP.
traceparent dans les propriétés du message à l'envoi, puis le ré-extraire à la réception, via Propagators.DefaultTextMapPropagator . Le baggage emprunte le même canal pour propager des paires clé/valeur métier (un tenant.id , par exemple) tout au long de la chaîne d'appels.
OTLP, le Collector et l'échantillonnage
OTEL_EXPORTER_OTLP_ENDPOINT ; changer de backend devient une modification de config côté Collector, pas un redéploiement.
SetSampler(new ParentBasedSampler(new TraceIdRatioBasedSampler(0.1))) garde 10 % des traces tout en respectant la décision déjà prise en amont, si bien qu'un parent échantillonné conserve ses enfants et qu'une trace reste cohérente d'un service à l'autre. Pour ne garder que les traces intéressantes (erreurs, latence haute), on déplace la décision en *tail-based* dans le Collector via le processor tail_sampling , une fois la trace complète reçue. La documentation OpenTelemetry pour .NET détaille les deux stratégies.
Instrumenter avec OpenTelemetry découple le code de l'outil de monitoring. Le SDK .NET se contente d'écouter les Activityet lesMeterque le runtime produit déjà ; migrer de Jaeger vers Azure Monitor se règle côté Collector, sans toucher au code applicatif.