Utilisation de DiagnosticSource dans .NET Core: théorie

DiagnosticSource est un ensemble d'API simple mais trÚs utile (disponible dans le package NuGet System.Diagnostics.DiagnosticSource ), qui, d'une part, permet à diverses bibliothÚques d'envoyer des événements nommés à propos de leur travail, et d'autre part, permet aux applications de s'abonner à ces événements et processus eux.


Chacun de ces Ă©vĂ©nements contient des informations supplĂ©mentaires (charge utile), et puisque les Ă©vĂ©nements sont traitĂ©s dans le mĂȘme processus que l'envoi, ces informations peuvent contenir presque tous les objets sans avoir besoin de sĂ©rialisation / dĂ©sĂ©rialisation.


DiagnosticSource est dĂ©jĂ  utilisĂ© dans AspNetCore, EntityFrameworkCore, HttpClient et SqlClient, ce qui donne aux dĂ©veloppeurs la possibilitĂ© d'intercepter les requĂȘtes http entrantes / sortantes, les requĂȘtes de base de donnĂ©es, les objets d'accĂšs tels que HttpContext , DbConnection , DbCommand , HttpRequestMessage et bien d'autres objets si nĂ©cessaire.


J'ai dĂ©cidĂ© de diviser mon histoire sur DiagnosticSource en deux articles. Dans cet article, nous analyserons le principe de fonctionnement du mĂ©canisme avec un exemple simple, et dans le prochain, je parlerai des Ă©vĂ©nements qui existent dans .NET qui peuvent ĂȘtre traitĂ©s Ă  l'aide de celui-ci et montrer quelques exemples de son utilisation dans OZON.ru.


Exemple


Pour mieux comprendre le fonctionnement de DiagnosticSource, considĂ©rons un petit exemple d'interception de requĂȘtes de base de donnĂ©es. Imaginez que nous ayons une application console simple qui fait une demande Ă  la base de donnĂ©es et affiche le rĂ©sultat dans la console.


 public static class Program { public const string ConnectionString = @"Data Source=localhost;Initial Catalog=master;User ID=sa;Password=Password12!;"; public static async Task Main() { var answer = await GetAnswerAsync(); Console.WriteLine(answer); } public static async Task<int> GetAnswerAsync() { using (var connection = new SqlConnection(ConnectionString)) { // using Dapper return await connection.QuerySingleAsync<int>("SELECT 42;"); } } } 

Par souci de simplicité, j'ai élevé SQL Server dans un conteneur Docker.


docker run
 docker run --rm --detach --name mssql-server \ --publish 1433:1433 \ --env ACCEPT_EULA=Y \ --env SA_PASSWORD=Password12! \ mcr.microsoft.com/mssql/server:2017-latest 

Imaginez maintenant que nous avons une tĂąche: nous devons mesurer le temps d'exĂ©cution de toutes les requĂȘtes vers la base de donnĂ©es Ă  l'aide de Stopwatch et afficher les paires «Request» - «Runtime» dans la console.


Bien sĂ»r, vous pouvez simplement QuerySingleAsync appel QuerySingleAsync code qui crĂ©e et dĂ©marre l'instance Stopwatch , l'arrĂȘte aprĂšs l'exĂ©cution de la requĂȘte et affiche le rĂ©sultat, mais il y a plusieurs difficultĂ©s Ă  la fois:


  • Que faire si la demande contient plusieurs demandes?
  • Que se passe-t-il si le code qui exĂ©cute la demande est dĂ©jĂ  compilĂ©, est connectĂ© Ă  l'application en tant que package NuGet et que nous n'avons aucun moyen de le modifier?
  • Que se passe-t-il si la requĂȘte vers la base de donnĂ©es n'est pas effectuĂ©e via Dapper, mais via EntityFramework, par exemple, et que nous n'avons pas accĂšs Ă  l'objet DbCommand ou au texte de requĂȘte gĂ©nĂ©rĂ© qui sera rĂ©ellement exĂ©cutĂ©?

Essayons de résoudre ce problÚme en utilisant DiagnosticSource.


Utilisation du package NuGet System.Diagnostics.DiagnosticSource


La premiÚre chose à faire aprÚs avoir connecté le package NuGet de System.Diagnostics.DiagnosticSource est de créer une classe qui gérera les événements qui nous intéressent:


 public sealed class ExampleDiagnosticObserver { } 

Pour démarrer le traitement des événements, vous devez créer une instance de cette classe et l'enregistrer en tant qu'observateur dans l'objet statique DiagnosticListener.AllListeners (situé dans l'espace de noms System.Diagnostics ). Nous le faisons au tout début de la fonction Main :


 public static async Task Main() { var observer = new ExampleDiagnosticObserver(); IDisposable subscription = DiagnosticListener.AllListeners.Subscribe(observer); var answer = await GetAnswerAsync(); Console.WriteLine(answer); } 

Dans ce cas, le compilateur nous indiquera à juste titre que la classe ExampleDiagnosticObserver doit implémenter l' IObserver<DiagnosticListener> . Implémentons-le:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { Console.WriteLine(diagnosticListener.Name); } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { } } 

Si nous exécutons ce code maintenant, nous verrons que ce qui suit sera affiché dans la console:


 SqlClientDiagnosticListener SqlClientDiagnosticListener 42 

Cela signifie que quelque part dans .NET deux objets de type DiagnosticListener sont enregistrés avec le nom "SqlClientDiagnosticListener" qui fonctionnait lorsque ce code a été exécuté.



La IObserver<DiagnosticListener>.OnNext sera appelée une fois lors de la premiÚre utilisation pour chaque instance de DiagnosticListener créée dans l'application (généralement, elles sont créées en tant que propriétés statiques). Maintenant, nous venons d'afficher le nom des instances de DiagnosticListener dans la console, mais en pratique, cette méthode doit vérifier ce nom et, si nous sommes intéressés par le traitement des événements de cette instance, abonnez-vous en utilisant la méthode Subscribe .


Je tiens Ă©galement Ă  noter que lorsque nous appelons DiagnosticListener.AllListeners.Subscribe nous obtiendrons un objet d' subscription en consĂ©quence, qui implĂ©mente l'interface IDisposable . L'appel de la mĂ©thode Dispose sur cet objet entraĂźnera la dĂ©sinscription, qui doit ĂȘtre implĂ©mentĂ©e dans la IObserver<DiagnosticListener>.OnCompleted .


IObserver<DiagnosticListener> nouveau IObserver<DiagnosticListener> :


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { private readonly List<IDisposable> _subscriptions = new List<IDisposable>(); void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.Subscribe(this); _subscriptions.Add(subscription); } } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { _subscriptions.ForEach(x => x.Dispose()); _subscriptions.Clear(); } } 

Le compilateur va maintenant nous dire que notre classe ExampleDiagnosticObserver devrait Ă©galement implĂ©menter l' IObserver<KeyValuePair<string, object>> . Ici, nous devons implĂ©menter IObserver<KeyValuePair<string, object>>.OnNext , qui en tant que paramĂštre accepte KeyValuePair<string, object> , oĂč la clĂ© est le nom de l'Ă©vĂ©nement, et la valeur est un objet anonyme (gĂ©nĂ©ralement) avec des paramĂštres arbitraires que nous pouvons utiliser Ă  sa discrĂ©tion. Ajoutons une implĂ©mentation de cette interface:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> { // IObserver<DiagnosticListener> implementation // ... void IObserver<KeyValuePair<string, object>>.OnNext(KeyValuePair<string, object> pair) { Write(pair.Key, pair.Value); } void IObserver<KeyValuePair<string, object>>.OnError(Exception error) { } void IObserver<KeyValuePair<string, object>>.OnCompleted() { } private void Write(string name, object value) { Console.WriteLine(name); Console.WriteLine(value); Console.WriteLine(); } } 

Si nous exécutons maintenant le code résultant, les éléments suivants seront affichés dans la console:


 System.Data.SqlClient.WriteConnectionOpenBefore { OperationId = 3da1b5d4-9ce1-4f28-b1ff-6a5bfc9d64b8, Operation = OpenAsync, Connection = System.Data.SqlClient.SqlConnection, Timestamp = 26978341062 } System.Data.SqlClient.WriteConnectionOpenAfter { OperationId = 3da1b5d4-9ce1-4f28-b1ff-6a5bfc9d64b8, Operation = OpenAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978631500 } System.Data.SqlClient.WriteCommandBefore { OperationId = 5c6d300c-bc49-4f80-9211-693fa1e2497c, Operation = ExecuteReaderAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Command = System.Data.SqlClient.SqlComman d } System.Data.SqlClient.WriteCommandAfter { OperationId = 5c6d300c-bc49-4f80-9211-693fa1e2497c, Operation = ExecuteReaderAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Command = System.Data.SqlClient.SqlComman d, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978709490 } System.Data.SqlClient.WriteConnectionCloseBefore { OperationId = 3f6bfd8f-e5f6-48b7-82c7-41aeab881142, Operation = Close, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Stat istics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978760625 } System.Data.SqlClient.WriteConnectionCloseAfter { OperationId = 3f6bfd8f-e5f6-48b7-82c7-41aeab881142, Operation = Close, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Stat istics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978772888 } 42 

Au total, nous verrons six événements. Deux d'entre eux sont exécutés avant et aprÚs l'ouverture de la connexion à la base de données, deux - avant et aprÚs l'exécution de la commande, et deux autres - avant et aprÚs la fermeture de la connexion à la base de données.


Chaque Ă©vĂ©nement contient un ensemble de paramĂštres, tels que OperationId , ConnectionId , Connection , Command , qui sont gĂ©nĂ©ralement transmis en tant que propriĂ©tĂ©s d'un objet anonyme. Vous pouvez obtenir des valeurs saisies pour ces propriĂ©tĂ©s, par exemple, en utilisant la rĂ©flexion. (En pratique, l'utilisation de la rĂ©flexion peut ne pas ĂȘtre trĂšs souhaitable. Nous utilisons DynamicMethod pour obtenir les paramĂštres d'Ă©vĂ©nement.)


Nous sommes maintenant prĂȘts Ă  rĂ©soudre le problĂšme initial - Ă  mesurer le temps d'exĂ©cution de toutes les demandes Ă  la base de donnĂ©es et Ă  l'afficher dans la console avec la demande d'origine.


Modifiez l'implémentation de la méthode Write comme suit:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> { // IObserver<DiagnosticListener> implementation // ... // IObserver<KeyValuePair<string, object>> implementation // ... private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>(); private void Write(string name, object value) { switch (name) { case "System.Data.SqlClient.WriteCommandBefore": { //           _stopwatch.Value = Stopwatch.StartNew(); break; } case "System.Data.SqlClient.WriteCommandAfter": { //           var stopwatch = _stopwatch.Value; stopwatch.Stop(); var command = GetProperty<SqlCommand>(value, "Command"); Console.WriteLine($"CommandText: {command.CommandText}"); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); Console.WriteLine(); break; } } } private static T GetProperty<T>(object value, string name) { return (T) value.GetType() .GetProperty(name) .GetValue(value); } } 

Ici, nous interceptons les Ă©vĂ©nements de dĂ©but et de fin de la demande vers la base de donnĂ©es. Avant d'exĂ©cuter la requĂȘte, nous crĂ©ons et dĂ©marrons le chronomĂštre, en l'enregistrant dans une variable de type AsyncLocal<Stopwatch> , afin de le rĂ©cupĂ©rer plus tard. AprĂšs avoir exĂ©cutĂ© la demande, nous obtenons le chronomĂštre prĂ©cĂ©demment lancĂ©, l'arrĂȘtons, obtenons la commande exĂ©cutĂ©e Ă  partir du paramĂštre de value via la rĂ©flexion et imprimons le rĂ©sultat sur la console.


Si nous exécutons maintenant le code résultant, les éléments suivants seront affichés dans la console:


 CommandText: SELECT 42; Elapsed: 00:00:00.0341357 42 

Il semblerait que nous ayons dĂ©jĂ  rĂ©solu notre problĂšme, mais un petit dĂ©tail demeure. Le fait est que lorsque nous nous abonnons aux Ă©vĂ©nements DiagnosticListener , nous commençons Ă  en recevoir mĂȘme les Ă©vĂ©nements qui ne nous intĂ©ressent pas, et puisque chaque Ă©vĂ©nement est envoyĂ©, un objet anonyme avec des paramĂštres est créé, cela peut crĂ©er une charge supplĂ©mentaire sur le GC.


Pour Ă©viter cette situation et vous dire quels Ă©vĂ©nements de DiagnosticListener nous allons traiter, nous pouvons spĂ©cifier un dĂ©lĂ©guĂ© spĂ©cial du type Predicate<string> lors de la souscription, qui prend le nom de l'Ă©vĂ©nement comme paramĂštre et renvoie true si cet Ă©vĂ©nement doit ĂȘtre traitĂ©.


IObserver<DiagnosticListener>.OnNext dans notre classe:


 void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.Subscribe(this, IsEnabled); _subscriptions.Add(subscription); } } private bool IsEnabled(string name) { return name == "System.Data.SqlClient.WriteCommandBefore" || name == "System.Data.SqlClient.WriteCommandAfter"; } 

Désormais, notre méthode Write sera appelée uniquement pour les événements "System.Data.SqlClient.WriteCommandBefore" et "System.Data.SqlClient.WriteCommandAfter" .


Utilisation de NuGet Package.Extensions.DiagnosticAdapter de Microsoft


Étant donnĂ© que les paramĂštres d'Ă©vĂ©nement que nous recevons de DiagnosticListener sont gĂ©nĂ©ralement passĂ©s comme un objet anonyme, les rĂ©cupĂ©rer par rĂ©flexion peut ĂȘtre trop coĂ»teux. Heureusement, il existe un package NuGet Microsoft.Extensions.DiagnosticAdapter qui peut le faire pour nous, en utilisant la gĂ©nĂ©ration de code d'exĂ©cution Ă  partir de l' System.Reflection.Emit .


Pour utiliser ce package lors de la souscription à des événements d'une instance de DiagnosticListener au lieu de la méthode Subscribe , vous devez utiliser la méthode d'extension SubscribeWithAdapter . L'implémentation de l' IObserver<KeyValuePair<string, object>> dans ce cas n'est plus nécessaire. Au lieu de cela, pour chaque événement que nous voulons gérer, nous devons déclarer une méthode distincte, en la marquant avec l'attribut DiagnosticNameAttribute (à partir de l'espace de noms Microsoft.Extensions.DiagnosticAdapter ). Les paramÚtres de ces méthodes seront les paramÚtres de l'événement en cours de traitement.


Si nous ExampleDiagnosticObserver notre classe ExampleDiagnosticObserver aide de ce package NuGet, nous obtenons le code suivant:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { private readonly List<IDisposable> _subscriptions = new List<IDisposable>(); void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.SubscribeWithAdapter(this); _subscriptions.Add(subscription); } } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { _subscriptions.ForEach(x => x.Dispose()); _subscriptions.Clear(); } private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>(); [DiagnosticName("System.Data.SqlClient.WriteCommandBefore")] public void OnCommandBefore() { _stopwatch.Value = Stopwatch.StartNew(); } [DiagnosticName("System.Data.SqlClient.WriteCommandAfter")] public void OnCommandAfter(DbCommand command) { var stopwatch = _stopwatch.Value; stopwatch.Stop(); Console.WriteLine($"CommandText: {command.CommandText}"); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); Console.WriteLine(); } } 

Ainsi, nous pouvons dĂ©sormais mesurer le temps d'exĂ©cution de toutes les requĂȘtes vers la base de donnĂ©es depuis notre application, pratiquement sans changer le code de l'application elle-mĂȘme.


Création de vos propres instances DiagnosticListener


Lorsque vous utilisez DiagnosticSource dans la pratique, dans la plupart des cas, vous vous abonnez à des événements existants. Vous n'aurez probablement pas à créer vos propres instances de DiagnosticListener et à envoyer vos propres événements (uniquement si vous ne développez aucune bibliothÚque), donc je ne m'attarderai pas sur cette section pendant longtemps.


Pour créer votre propre instance de DiagnosticListener vous devrez la déclarer en tant que variable statique quelque part dans le code:


 private static readonly DiagnosticSource _myDiagnosticSource = new DiagnosticListener("MyLibraty"); 

AprÚs cela, pour envoyer un événement, vous pouvez utiliser un design du formulaire:


 if (_myDiagnosticSource.IsEnabled("MyEvent")) _myDiagnosticSource.Write("MyEvent", new { /* parameters */ }); 

Pour plus d'informations sur la création de vos propres instances de DiagnosticListener , consultez le Guide de l'utilisateur de DiagnosticSource , qui détaille les meilleures pratiques d'utilisation de DiagnosticSource.


Conclusion


L'exemple que nous avons examinĂ© est certainement trĂšs abstrait et il est peu probable qu'il soit utile dans un vrai projet. Mais il montre parfaitement comment ce mĂ©canisme peut ĂȘtre utilisĂ© pour surveiller et diagnostiquer vos applications.


Dans le prochain article, je donnerai une liste des Ă©vĂ©nements connus qui peuvent ĂȘtre traitĂ©s via DiagnosticSource et montrer quelques exemples pratiques de son utilisation.

Source: https://habr.com/ru/post/fr435896/


All Articles