.NET CoreでのDiagnosticSourceの使用:理論

DiagnosticSourceはシンプルですが非常に便利なAPIのセットです(NuGetパッケージSystem.Diagnostics.DiagnosticSourceで利用可能)。これにより、さまざまなライブラリが作業に関する名前付きイベントを送信できるようになり、一方で、アプリケーションがこれらのイベントとプロセスをサブスクライブできるようになりますそれら。







このような各イベントには追加情報(ペイロード)が含まれ、イベントは送信と同じプロセスで処理されるため、この情報にはシリアル化/逆シリアル化を必要とせずにほとんどすべてのオブジェクトを含めることができます。







DiagnosticSourceはすでにAspNetCore、EntityFrameworkCore、HttpClient、およびSqlClientで使用されており、実際に開発者は着信/発信http要求、データベース要求、 HttpContext



DbConnection



DbCommand



HttpRequestMessage



などのオブジェクトにアクセスできます。必要に応じてオブジェクト。







DiagnosticSourceについてのストーリーを2つの記事に分けることにしました。 この記事では、簡単な例を使用してメカニズムの動作の原理を分析し、次に、それを使用して処理できる.NETに存在するイベントについて説明し、OZON.ruでの使用例をいくつか示します。









DiagnosticSourceの仕組みをよりよく理解するために、データベースクエリのインターセプトの小さな例を考えてみましょう。 データベースにリクエストを送信し、コンソールに結果を表示するシンプルなコンソールアプリケーションがあるとします。







 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;"); } } }
      
      





簡単にするために、SQL Serverをdockerコンテナーで上げました。







ドッカーラン
 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
      
      





タスクがあることを想像してください。 Stopwatch



を使用してデータベースへのすべてのリクエストの実行時間を測定し、コンソールに「リクエスト」と「ランタイム」のペアを表示する必要があります。







もちろん、 QuerySingleAsync



インスタンスを作成および開始し、クエリの実行後に停止して結果を表示するコードでQuerySingleAsync



呼び出しQuerySingleAsync



ラップするだけでもかまいませんが、いくつかの困難があります。









DiagnosticSourceを使用してこの問題を解決してみましょう。







NuGet System.Diagnostics.DiagnosticSourceパッケージの使用



System.Diagnostics.DiagnosticSourceのNuGetパッケージを接続した後に最初に行うことは、関心のあるイベントを処理するクラスを作成することです。







 public sealed class ExampleDiagnosticObserver { }
      
      





イベントの処理を開始するには、このクラスのインスタンスを作成し、静的オブジェクトDiagnosticListener.AllListeners



System.Diagnostics



名前空間にある)にオブザーバーとして登録する必要があります。 これは、 Main



関数の最初に行います:







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





この場合、コンパイラはExampleDiagnosticObserver



クラスがIObserver<DiagnosticListener>



インターフェイスを実装する必要があることを正しく示します。 それを実装しましょう:







 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() { } }
      
      





このコードを実行すると、コンソールに次が表示されることがわかります。







 SqlClientDiagnosticListener SqlClientDiagnosticListener 42
      
      





これは、.NETのどこかに、 DiagnosticListener



タイプの2つのオブジェクトが、このコードが実行されたときに機能する"SqlClientDiagnosticListener"



という名前で登録されていることを意味します。







こちらはgithub.comにあります

実際には3つありますが、 SqlTransaction



使用しなかったため、2つだけが機能しました。









IObserver<DiagnosticListener>.OnNext



は、アプリケーションで作成されたDiagnosticListener



各インスタンスの最初の使用時に1回呼び出されます(通常、静的プロパティとして作成されます)。 コンソールにDiagnosticListener



インスタンスの名前を表示しましたが、実際にはこのメソッドはこの名前を確認する必要があり、このインスタンスからのイベントの処理に関心がある場合は、 Subscribe



メソッドを使用してSubscribe



ライブします。







またDiagnosticListener.AllListeners.Subscribe



を呼び出すと、結果として、 IDisposable



インターフェイスを実装するsubscription



オブジェクトを取得することに注意してください。 このオブジェクトでDispose



メソッドを呼び出すと、 IObserver<DiagnosticListener>.OnCompleted



が解除されます。これは、 IObserver<DiagnosticListener>.OnCompleted



で実装する必要があります。







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(); } }
      
      





これでコンパイラは、 ExampleDiagnosticObserver



クラスがIObserver<KeyValuePair<string, object>>



インターフェイスも実装する必要があることを通知します。 ここで、 IObserver<KeyValuePair<string, object>>.OnNext



を実装する必要がありIObserver<KeyValuePair<string, object>>.OnNext



は、パラメーターとしてKeyValuePair<string, object>



取りKeyValuePair<string, object>



。ここで、キーはイベントの名前で、値は(通常)使用できる任意のパラメーターを持つ匿名オブジェクトですその裁量で。 このインターフェイスの実装を追加しましょう。







 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(); } }
      
      





結果のコードを実行すると、コンソールに次が表示されます。







 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
      
      





合計で、6つのイベントが表示されます。 それらの2つは、データベースへの接続を開く前後に実行されます。2つ-コマンドの実行前と後、さらに2つ-データベースへの接続を閉じる前と後に実行されます。







各イベントには、 OperationId



ConnectionId



Connection



Command



などのパラメーターのセットが含まれ、通常は匿名オブジェクトのプロパティとして渡されます。 たとえば、リフレクションを使用して、これらのプロパティの型指定された値を取得できます。 (実際には、リフレクションを使用することはあまり望ましくありません。DynamicMethodを使用してイベントパラメーターを取得します。)







これで、データベースへのすべてのリクエストの実行時間を測定し、元のリクエストとともにコンソールに表示するという初期問題を解決する準備がすべて整いました。







Write



メソッドの実装を次のように変更します。







 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); } }
      
      





ここでは、データベースへのリクエストの開始イベントと終了イベントをインターセプトします。 リクエストを実行する前に、ストップウォッチを作成して開始し、後で取得するためにAsyncLocal<Stopwatch>



変数に保存します。 リクエストを実行した後、以前に起動したストップウォッチを取得、停止し、反射を介してvalue



パラメーターから実行されたコマンドを取得し、結果をコンソールに出力します。







結果のコードを実行すると、コンソールに次が表示されます。







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





私たちはすでに問題を解決したように見えますが、1つの小さな詳細が残っています。 事実、 DiagnosticListener



イベントをサブスクライブすると、関心のないイベントも受信し始めます。各イベントにはパラメーター付きの匿名オブジェクトが送信されるため、GCに余分な負荷がかかる可能性があります。







この状況を回避し、 DiagnosticListener



からどのイベントを処理するかを通知するために、サブスクライブ時にPredicate<string>



タイプの特別なデリゲートを指定できます。これは、イベントの名前をパラメーターとして受け取り、このイベントを処理する必要がtrue



場合にtrue



を返しtrue









クラスIObserver<DiagnosticListener>.OnNext



IObserver<DiagnosticListener>.OnNext









 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"; }
      
      





Write



メソッドは、イベント"System.Data.SqlClient.WriteCommandBefore"



および"System.Data.SqlClient.WriteCommandAfter"



に対してのみ呼び出されます。







MicrosoftのNuGet Package.Extensions.DiagnosticAdapterを使用する



DiagnosticListener



から受け取るイベントパラメータは通常、匿名オブジェクトとして渡されるため、リフレクションを介して取得するのはコストがかかりすぎる場合があります。 幸いなことに、 System.Reflection.Emit



からのランタイムコード生成を使用してこれを行うことができるMicrosoft.Extensions.DiagnosticAdapter NuGetパッケージがありSystem.Reflection.Emit









Subscribe



メソッドの代わりにDiagnosticListener



インスタンスからイベントをサブスクライブするときにこのパッケージを使用するには、 SubscribeWithAdapter



拡張メソッドを使用する必要があります。 この場合、 IObserver<KeyValuePair<string, object>>



インターフェースを実装する必要はなくなりました。 代わりに、処理するイベントごとに、個別のメソッドを宣言し、 DiagnosticNameAttribute



属性( Microsoft.Extensions.DiagnosticAdapter



名前空間から)でマークする必要があります。 これらのメソッドのパラメーターは、処理中のイベントのパラメーターになります。







このNuGetパッケージを使用してExampleDiagnosticObserver



クラスを書き換えると、次のコードが取得されます。







 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(); } }
      
      





したがって、実際にはアプリケーション自体のコードを変更することなく、アプリケーションからデータベースへのすべてのクエリの実行時間を測定できます。







独自のDiagnosticListenerインスタンスの作成



DiagnosticSourceを実際に使用する場合、ほとんどの場合、既存のイベントをサブスクライブします。 ほとんどの場合、 DiagnosticListener



独自のインスタンスを作成し、独自のイベントを送信する必要はありません(ライブラリを開発していない場合のみ)。したがって、このセクションについては長く触れません。







DiagnosticListener



独自のインスタンスを作成するには、コードのどこかに静的変数として宣言する必要があります。







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





その後、イベントを送信するには、フォームのデザインを使用できます







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





DiagnosticListener



独自のインスタンスの作成の詳細については、 DiagnosticSourceの使用に関するベストプラクティスを詳述したDiagnosticSourceユーザーガイドを参照してください。







おわりに



私たちが調べた例は確かに非常に抽象的なものであり、実際のプロジェクトでは有用ではないでしょう。 しかし、このメカニズムを使用してアプリケーションを監視および診断する方法を完全に示しています。







次の記事では、DiagnosticSourceを介して処理できる既知のイベントのリストを提供し、その使用の実用的な例を示します。








All Articles