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)) {
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é.
Les voici sur github.comIl y en a en fait trois, mais comme nous n'avons pas utilisé SqlTransaction
, seuls deux ont fonctionné:
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>> {
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>> {
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 { });
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.