Partager via


Ajout d’une instrumentation de suivi distribué

Cet article s’applique à : ✔️ .NET Core 2.1 et versions ultérieures ✔️ .NET Framework 4.5 et versions ultérieures

Les applications .NET peuvent être instrumentées à l’aide de l’API System.Diagnostics.Activity pour produire des données de télémétrie de suivi distribuées. Certaines instrumentations sont intégrées aux bibliothèques .NET standard, mais vous pouvez en ajouter d’autres pour faciliter le diagnostic de votre code. Dans ce didacticiel, vous allez ajouter une nouvelle instrumentation de suivi distribué personnalisée. Consultez le didacticiel de collecte pour en savoir plus sur l’enregistrement des données de télémétrie produites par cette instrumentation.

Prérequis

Créer une application initiale

Tout d’abord, vous allez créer un exemple d’application qui collecte les données de télémétrie à l’aide d’OpenTelemetry, mais qui n’a pas encore d’instrumentation.

dotnet new console

Les API de suivi distribuées nécessaires sont déjà incluses dans les applications qui ciblent .NET 5 et versions ultérieures. Pour les applications ciblant des versions antérieures de .NET, ajoutez le package NuGet System.Diagnostics.DiagnosticSource version 5 ou ultérieure.

dotnet add package System.Diagnostics.DiagnosticSource

Ajoutez les packages NuGet OpenTelemetry et OpenTelemetry.Exporter.Console permettant de collecter les données de télémétrie.

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console

Remplacez le contenu du fichier Program.cs généré par cet exemple de source :

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using var tracerProvider = Sdk.CreateTracerProviderBuilder()
                .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
                .AddSource("Sample.DistributedTracing")
                .AddConsoleExporter()
                .Build();

            await DoSomeWork("banana", 8);
            Console.WriteLine("Example work done");
        }

        // All the functions below simulate doing some arbitrary work
        static async Task DoSomeWork(string foo, int bar)
        {
            await StepOne();
            await StepTwo();
        }

        static async Task StepOne()
        {
            await Task.Delay(500);
        }

        static async Task StepTwo()
        {
            await Task.Delay(1000);
        }
    }
}

L’application n’ayant pas encore d’instrumentation, il n’y a pas d’informations de trace à afficher :

> dotnet run
Example work done

Meilleures pratiques

Seuls les développeurs d’applications doivent référencer une bibliothèque tierce facultative pour collecter les données de télémétrie de trace distribuée, comme OpenTelemetry dans cet exemple. Les auteurs de bibliothèque .NET peuvent s’appuyer exclusivement sur les API dans System.Diagnostics.DiagnosticSource, qui fait partie du runtime .NET. Cela garantit que les bibliothèques s’exécutent dans un large éventail d’applications .NET, quelles que soient les préférences du développeur d’applications concernant la bibliothèque ou le fournisseur à utiliser pour collecter des données de télémétrie.

Ajouter une instrumentation de base

Les applications et les bibliothèques ajoutent l’instrumentation de suivi distribué à l’aide des classes System.Diagnostics.ActivitySource et System.Diagnostics.Activity.

ActivitySource

Commencez par créer une instance d’ActivitySource. ActivitySource fournit des API pour créer et démarrer des objets Activity. Ajoutez la variable ActivitySource statique au-dessus de Main() et using System.Diagnostics; aux instructions using.

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        private static ActivitySource source = new ActivitySource("Sample.DistributedTracing", "1.0.0");

        static async Task Main(string[] args)
        {
            ...

Meilleures pratiques

  • Créez l’instance ActivitySource une fois, stockez-la dans une variable statique et utilisez cette instance aussi longtemps que nécessaire. Chaque sous-composant de bibliothèque ou bibliothèque peut (et doit souvent) créer sa propre source. Envisagez de créer une nouvelle source plutôt que de réutiliser une source existante si vous pensez que les développeurs d’applications apprécieront de pouvoir activer et désactiver la télémétrie de l’activité indépendamment dans les sources.

  • Le nom source passé au constructeur doit être unique pour éviter les conflits avec d’autres sources. S’il existe plusieurs sources dans le même assembly, utilisez un nom hiérarchique qui contient le nom de l’assembly et éventuellement un nom de composant, par exemple Microsoft.AspNetCore.Hosting. Si un assembly ajoute une instrumentation pour le code dans un deuxième assembly indépendant, le nom doit être basé sur l’assembly qui définit l’ActivitySource, et non sur l’assembly dont le code est instrumenté.

  • Le paramètre version est facultatif. Nous vous recommandons de fournir la version au cas où vous publiez plusieurs versions de la bibliothèque et apportez des modifications à la télémétrie instrumentée.

Notes

OpenTelemetry utilise d’autres termes comme « Tracer » et « Span ». Dans .NET, « ActivitySource » est l’implémentation de Tracer, Activity celle de « Span ». Le type d’activité de .NET est bien antérieur à la spécification OpenTelemetry et le nom original de .NET a été conservé pour des raisons de cohérence au sein de l’écosystème .NET et de compatibilité avec les applications .NET.

Activité

Utilisez l’objet ActivitySource pour démarrer et arrêter des objets d’activité autour d’unités de travail significatives. Mettez à jour DoSomeWork() avec le code indiqué ici :

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                await StepOne();
                await StepTwo();
            }
        }

L’exécution de l’application affiche maintenant la nouvelle activité enregistrée :

> dotnet run
Activity.Id:          00-f443e487a4998c41a6fd6fe88bae644e-5b7253de08ed474f-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:36:51.4720202Z
Activity.Duration:    00:00:01.5025842
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 067f4bb5-a5a8-4898-a288-dec569d6dbef

Notes

  • ActivitySource.StartActivity crée et démarre l’activité en même temps. Le modèle de code répertorié utilise le bloc using, qui supprime automatiquement l’objet Activity créé après l’exécution du bloc. La suppression de l’objet Activity l’arrête de sorte que le code n’ait pas besoin d’appeler Activity.Stop()explicitement. Cela simplifie le modèle de codage.

  • ActivitySource.StartActivity détermine en interne s’il existe des écouteurs qui enregistrent l’activité. S’il n’y a pas d’écouteurs inscrits ou s’il y a des écouteurs qui ne sont pas intéressés, StartActivity() retourne null et évite de créer l’objet Activity. Il s’agit d’une optimisation des performances afin que le modèle de code puisse toujours être utilisé dans les fonctions qui sont appelées fréquemment.

Facultatif : remplir des balises

Les activités prennent en charge les données clé-valeur appelées Balises, couramment utilisées pour stocker tous les paramètres du travail qui peuvent être utiles pour les diagnostics. Mettez à jour DoSomeWork() pour les inclure :

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                await StepTwo();
            }
        }
> dotnet run
Activity.Id:          00-2b56072db8cb5a4496a4bfb69f46aa06-7bc4acda3b9cce4d-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:37:31.4949570Z
Activity.Duration:    00:00:01.5417719
Activity.TagObjects:
    foo: banana
    bar: 8
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 25bbc1c3-2de5-48d9-9333-062377fea49c

Example work done

Meilleures pratiques

  • Comme mentionné ci-dessus, activity retourné par ActivitySource.StartActivity peut être null. En C#, l’opérateur de coalescence nulle ?. est un moyen pratique d’appeler Activity.SetTag uniquement si activity n’est pas null. Le comportement est identique à l’écriture :
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry fournit un ensemble de conventions recommandées pour définir des balises sur des activités qui représentent des types courants de travail d’application.

  • Si vous instrumentez des fonctions avec des exigences hautes performances, Activity.IsAllDataRequested indique si l’un des codes à l’écoute des activités a l’intention de lire des informations auxiliaires telles que des balises. Si aucun écouteur ne le lit, il n’est pas nécessaire pour le code instrumenté de dépenser des cycles CPU pour le remplir. Par souci de simplicité, cet exemple n’applique pas cette optimisation.

Facultatif : ajouter des événements

Les événements sont des messages horodatés qui peuvent attacher un flux arbitraire de données de diagnostic supplémentaires aux activités. Ajoutez des événements à l’Activity :

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));
            }
        }
> dotnet run
Activity.Id:          00-82cf6ea92661b84d9fd881731741d04e-33fff2835a03c041-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:39:10.6902609Z
Activity.Duration:    00:00:01.5147582
Activity.TagObjects:
    foo: banana
    bar: 8
Activity.Events:
    Part way there [3/18/2021 10:39:11 AM +00:00]
    Done now [3/18/2021 10:39:12 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: ea7f0fcb-3673-48e0-b6ce-e4af5a86ce4f

Example work done

Meilleures pratiques

  • Les événements sont stockés dans une liste en mémoire jusqu’à ce qu’ils puissent être transmis, ce qui fait que ce mécanisme ne convient que pour l’enregistrement d’un nombre peu important d’événements. Pour un volume important ou indépendant d’événements, il est préférable d’utiliser une API de journalisation axée sur cette tâche, telle que ILogger. ILogger garantit également que les informations de journalisation seront disponibles, que le développeur de l’application choisisse d’utiliser le suivi distribué. ILogger prend en charge la capture automatique des ID d’activité actifs afin que les messages enregistrés via cette API puissent toujours être corrélés avec la trace distribuée.

Facultatif : ajouter un état

OpenTelemetry permet à chaque activité de signaler un état qui représente le résultat de réussite/échec du travail. .NET n’a actuellement pas d’API fortement typée à cet effet, mais il existe une convention établie à l’aide de balises :

  • otel.status_code est le nom de balise utilisé pour stocker StatusCode. Les valeurs de la balise StatusCode doivent être l’une des chaînes « UNSET », « OK » ou « ERROR », qui correspondent respectivement aux énumérations Unset, Ok et Error à partir de StatusCode.
  • otel.status_description est le nom de balise utilisé pour stocker le nom facultatif Description

Mettez à jour DoSomeWork() pour définir l’état :

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));

                // Pretend something went wrong
                activity?.SetTag("otel.status_code", "ERROR");
                activity?.SetTag("otel.status_description", "Use this text give more information about the error");
            }
        }

Facultatif : ajouter des activités supplémentaires

Les activités peuvent être imbriquées pour décrire les parties d’une plus grande unité de travail. Cela peut être utile autour de portions de code qui pourraient ne pas s’exécuter rapidement ou pour mieux localiser les défaillances qui proviennent de dépendances externes spécifiques. Bien que cet exemple utilise une Activity dans chaque méthode, c’est uniquement parce que le code supplémentaire a été réduit au minimum. Dans un projet plus vaste et plus réaliste, l’utilisation d’une activité dans chaque méthode produirait des traces extrêmement détaillées. Cette approche n’est donc pas recommandée.

Mettez à jour StepOne et StepTwo pour ajouter davantage de suivi autour de ces étapes distinctes :

        static async Task StepOne()
        {
            using (Activity activity = source.StartActivity("StepOne"))
            {
                await Task.Delay(500);
            }
        }

        static async Task StepTwo()
        {
            using (Activity activity = source.StartActivity("StepTwo"))
            {
                await Task.Delay(1000);
            }
        }
> dotnet run
Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-39cac574e8fda44b-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepOne
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4278822Z
Activity.Duration:    00:00:00.5051364
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-4ccccb6efdc59546-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepTwo
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.9441095Z
Activity.Duration:    00:00:01.0052729
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4256627Z
Activity.Duration:    00:00:01.5286408
Activity.TagObjects:
    foo: banana
    bar: 8
    otel.status_code: ERROR
    otel.status_description: Use this text give more information about the error
Activity.Events:
    Part way there [3/18/2021 10:40:51 AM +00:00]
    Done now [3/18/2021 10:40:52 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Example work done

Notez que StepOne et StepTwo incluent un ParentId qui fait référence à SomeWork. La console ne permet pas de bien visualiser les arbres de travail imbriqués, mais de nombreuses visionneuses de l’interface graphique utilisateur telles que Zipkin permettent de les afficher sous la forme d’un diagramme de Gantt :

Zipkin Gantt chart

Facultatif : ActivityKind

Les activités ont une propriété Activity.Kind qui décrit la relation entre l’activité, son parent et ses enfants. Par défaut, toutes les nouvelles activités sont définies sur Internal, ce qui convient aux activités qui constituent une opération interne au sein d’une application sans parent ou enfant distant. D’autres types peuvent être définis à l’aide du paramètre de type sur ActivitySource.StartActivity. Pour d’autres options, consultez System.Diagnostics.ActivityKind.

Lorsque le travail se produit dans des systèmes de traitement par lots, une seule activité peut représenter le travail pour le compte de nombreuses demandes différentes simultanément, chacune ayant son propre id de trace. Bien que l’activité soit limitée à un parent unique, elle peut être liée à des id de trace supplémentaires à l’aide de System.Diagnostics.ActivityLink. Chaque ActivityLink est rempli avec un ActivityContext qui stocke les informations d’ID sur l’Activity lié. ActivityContext peut être récupéré à partir d’objets Activity in-process à l’aide de Activity.Context ou peut être analysé à partir d’informations d’ID sérialisées à l’aide de ActivityContext.Parse(String, String).

void DoBatchWork(ActivityContext[] requestContexts)
{
    // Assume each context in requestContexts encodes the trace-id that was sent with a request
    using(Activity activity = s_source.StartActivity(name: "BigBatchOfWork",
                                                     kind: ActivityKind.Internal,
                                                     parentContext: default,
                                                     links: requestContexts.Select(ctx => new ActivityLink(ctx))
    {
        // do the batch of work here
    }
}

Contrairement aux événements et balises qui peuvent être ajoutés à la demande, les liens doivent être ajoutés pendant StartActivity() et sont immuables par la suite.