.NET CoreでDiagnosticSourceを使用する:練習

前の記事で 、DiagnosticSourceメカニズムについて説明し、それを使用してSqlConnection



クラスとSqlCommand



クラスを介してデータベースへの要求をインターセプトし、実行時間を測定する方法を簡単な例で示しました。







現在、DiagnosticSourceは既にAspNetCore、EntityFrameworkCore、HttpClient、およびSqlClientで使用されています-それぞれが独自のイベントを送信し、それらはインターセプトおよび処理できます。







この記事では、実際にASP.NET CoreアプリケーションでDiagnosticSourceを使用する方法のいくつかの例を検討します。









さらに、この記事では、処理に使用でき、アプリケーションで使用できるイベントのリストを収集することにしました。また、プロジェクトでDiagnosticSourceメカニズムを使用する場合に発生する可能性のある落とし穴についても説明します。







既存のイベント



例の検討に移る前に、DiagnosticSourceを介してイベントを送信するコンポーネント、およびこれらのイベントの名前を理解する必要があります。 残念ながら、イベントの完全なリストはドキュメントのどこにも記載されておらず、GitHubのソースコードでのみ見つけることができます。







したがって、存在するイベントを理解する最も簡単な方法は、 IObserver<DiagnosticListener>



およびIObserver<KeyValuePair<string, object>>



インターフェイスを実装するクラスを作成し、その中のDiagnosticListener



インスタンスをサブスクライブし、アプリケーションでキャッチされるイベントを確認することです。 同様に、各イベントで送信されるパラメーターを判別できます。







タスクを簡素化するために、4つのコンポーネントの最も有用なイベント(これは完全なリストではありません)のいくつかを既に収集しています。







Microsoft.AspNetCore

Microsoft.AspNetCore



コンポーネントのイベントを使用すると、ASP.NET Coreでのhttp要求処理のライフイベントをインターセプトできます。







  • Microsoft.AspNetCore.Hosting.HttpRequestIn.Start
  • Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop


これらのイベントは、http要求の処理の最初と最後に発生します。







  • Microsoft.AspNetCore.Diagnostics.UnhandledException


未処理の例外で発生します。 これは、このコンポーネントの例外を処理できる唯一の場所です。







  • Microsoft.AspNetCore.Mvc.BeforeAction
  • Microsoft.AspNetCore.Mvc.AfterAction


UseMvc



を使用するときに追加されるミドルウェアでhttp要求を処理する前後に発生しUseMvc



。 実質的に、次のイベントはすべて2つの間で発生します。







  • Microsoft.AspNetCore.Mvc.BeforeOnAuthorization
  • Microsoft.AspNetCore.Mvc.AfterOnAuthorization


許可の前後に発生します。







  • Microsoft.AspNetCore.Mvc.BeforeActionMethod
  • Microsoft.AspNetCore.Mvc.AfterActionMethod


コントローラーメソッドの実行の前後に発生します。







  • Microsoft.AspNetCore.Mvc.BeforeActionResult
  • Microsoft.AspNetCore.Mvc.AfterActionResult


コントローラーメソッドから返されたExecuteResultAsync



インスタンスでExecuteResultAsync



を呼び出す前後に発生します。 これには、たとえば、jsonで結果をシリアル化することが含まれます。







  • Microsoft.AspNetCore.Mvc.BeforeHandlerMethod
  • Microsoft.AspNetCore.Mvc.AfterHandlerMethod


ASP.NETページで使用されます。 ページモデルメソッドの実行前後に発生します。







  • Microsoft.AspNetCore.Mvc.BeforeView
  • Microsoft.AspNetCore.Mvc.AfterView


ビューのレンダリングの前後に発生します。







Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore



コンポーネントのイベントを使用すると、EntityFrameworkCoreを介してデータベースアクセスイベントをインターセプトできます。







  • Microsoft.EntityFrameworkCore.Infrastructure.ContextInitialized
  • Microsoft.EntityFrameworkCore.Infrastructure.ContextDisposed


DbContext



インスタンスを使用する前後に発生します







  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpening
  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpened
  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionError


データベース接続を開く前後に発生します。 接続が正常に開かれた場合、 ConnectionOpened



イベントが発生します。 接続を開くときにエラーが発生すると、 ConnectionError



イベントが発生します。







  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosing
  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosed
  • Microsoft.EntityFrameworkCore.Database.Connection.ConnectionError


同様に、データベース接続を閉じる前後に発生します。







  • Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting
  • Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted
  • Microsoft.EntityFrameworkCore.Database.Command.CommandError


同様に、データベースへのクエリの前後に発生します。







  • Microsoft.EntityFrameworkCore.Database.Command.DataReaderDisposing


DbDataReader



インスタンスから読み取った後に発生します。







SqlClientDiagnosticListener

SqlClientDiagnosticListener



コンポーネントのイベントを使用すると、対応するADO.NETプロバイダーを介してSQL Serverデータベースにアクセスするイベントをインターセプトできます。







  • System.Data.SqlClient.WriteConnectionOpenBefore
  • System.Data.SqlClient.WriteConnectionOpenAfter
  • System.Data.SqlClient.WriteConnectionOpenError


データベース接続を開く前後に発生します。 接続が正常に開かれた場合、 WriteConnectionOpenAfter



イベントWriteConnectionOpenAfter



。 接続を開くときにエラーが発生すると、 WriteConnectionOpenError



イベントWriteConnectionOpenError









  • System.Data.SqlClient.WriteConnectionCloseBefore
  • System.Data.SqlClient.WriteConnectionCloseAfter
  • System.Data.SqlClient.WriteConnectionCloseError


同様に、データベース接続を閉じる前後に発生します。







  • System.Data.SqlClient.WriteCommandBefore
  • System.Data.SqlClient.WriteCommandAfter
  • System.Data.SqlClient.WriteCommandError


同様に、データベースへのクエリの前後に発生します。







HttpHandlerDiagnosticListener

HttpHandlerDiagnosticListener



コンポーネントのイベントをHttpHandlerDiagnosticListener



すると、たとえばHttpClient



クラスを使用する場合に、発信HTTP要求をインターセプトHttpHandlerDiagnosticListener



ます。







  • System.Net.Http.HttpRequestOut.Start
  • System.Net.Http.HttpRequestOut.Stop


発信HTTP要求の前後に発生します。







  • System.Net.Http.Exception


発信HTTP要求中にエラーが発生した場合に発生します。







ちなみに、 DiagnosticSourceのイベントの命名に関する推奨事項と規則について説明しているDiagnosticSource User's Guideもあります







簡単に推測できるように、Microsoftはこれらの推奨事項に従わず、反対のことを行います=)(わかりました、誇張します。DiagnosticSourceユーザーガイドが登場する前に、DiagnosticSourceが.NET Coreコンポーネントで使用され始めました)







共通コード



以下で検討するすべての例は、ASP.NET Coreアプリケーションで使用されるものと想定され(これは必須ではありません)、基本クラスDiagnosticObserverBase



を使用してDiagnosticObserverBase



からイベントをサブスクライブして処理します。







このクラスは、 前回の記事の ExampleDiagnosticObserver



クラスに基づいており、その操作の説明を参照できます。 イベントをサブスクライブして処理するために、このクラスはMicrosoft.Extensions.DiagnosticAdapterの NuGetパッケージのSubscribeWithAdapter



メソッドを使用します







 public abstract class DiagnosticObserverBase : IObserver<DiagnosticListener> { private readonly List<IDisposable> _subscriptions = new List<IDisposable>(); protected abstract bool IsMatch(string name); void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (IsMatch(diagnosticListener.Name)) { 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(); } }
      
      





特定のコンポーネントからイベントをサブスクライブするには、新しいクラスを作成し、 DiagnosticObserverBase



から継承し、サブスクライブするコンポーネントに対してtrue



を返すIsMatch



メソッドを再定義し、イベントを処理するメソッドを追加し、名前を指定するDiagnosticNameAttribute



属性でマークする必要があります処理されたイベント。 例:







 public sealed class SomeDiagnosticObserver : DiagnosticObserverBase { protected override bool IsMatch(string name) { return name == "SomeComponent"; } [DiagnosticName("SomeEvent")] public void OnSomeEvent(/* EventParameters */) { // ... } }
      
      





DIコンテナーのDiagnosticObserverBase



クラスに基づいてハンドラーを登録するには、 AddDiagnosticObserver



拡張AddDiagnosticObserver



使用します。これは、Startup.csファイルのConfigureServices



メソッドで使用されます。







 public static class DiagnosticServiceCollectionExtensions { public static void AddDiagnosticObserver<TDiagnosticObserver>( this IServiceCollection services) where TDiagnosticObserver : DiagnosticObserverBase { services.TryAddEnumerable(ServiceDescriptor .Transient<DiagnosticObserverBase, TDiagnosticObserver>()); } }
      
      





また、DiagnosticSourceからイベントをサブスクライブするには、 Configure



メソッドに次の行を追加します。







 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { var diagnosticObservers = app .ApplicationServices.GetServices<DiagnosticObserverBase>(); foreach (var diagnosticObserver in diagnosticObservers) { DiagnosticListener.AllListeners.Subscribe(diagnosticObserver); } // ... app.UseMvc(); }
      
      





おそらくこれは登録するのに最適な方法ではなく、実際には通常このような目的でIHostedService



インターフェイスを使用しますが、例で十分です。







いくつかの落とし穴



プロジェクトでDiagnosticSourceを使用することに決めた場合は、私が詳しく説明したい非自明な点に出くわすかもしれません。







存在しないイベントのダミーハンドラが必要になる場合があります。



通常、一部のコンポーネントがその作業に関するイベントを送信する場合、イベントを送信するコードは次のとおりです。







 if (_diagnosticSource.IsEnabled("SomeEvent")) _diagnosticSource.Write("SomeEvent", new { /* parameters */ });
      
      





これにより、誰もイベントを処理する予定がない場合にパラメータを持つオブジェクトを作成せず、ガベージコレクションを少し節約できます。







ただし、場合によっては、サフィックスが.Start



.Stop



イベントがペアになり、両方とも機能するかどうかが.Stop



ます。 このようなイベントを送信するコードは次のようになります。







  //  ,     . var someEventIsEnabled = _diagnosticSource.IsEnabled("SomeEvent"); if (someEventIsEnabled && _diagnosticSource.IsEnabled("SomeEvent.Start")) _diagnosticSource.Write("SomeEvent.Start", new { /* parameters */ }); // ... if (someEventIsEnabled && _diagnosticSource.IsEnabled("SomeEvent.Stop")) _diagnosticSource.Write("SomeEvent.Stop", new { /* parameters */ });
      
      





したがって、 SomeEvent.Stop



イベントとSomeEvent.Stop



イベントをサブスクライブするには、 SomeEvent



イベントのダミーハンドラーも追加する必要があります。これは呼び出されませんが、その存在はチェックされます。







いくつかのイベントはペアになり、いくつかのトリプルは



System.Net.Http.HttpRequestOut.Start



System.Net.Http.HttpRequestOut.Stop



など、一部のイベントはペアになっています。 つまり、接尾辞が.Start



イベントは何らかの操作の開始前に呼び出され、接尾辞が.Stop



イベントは最後に呼び出されます。 この場合、操作がエラーで終了したかどうかに関係なく、最後のイベントがトリガーされることが保証されます(適切なハンドラーがある場合)。







ただし、一部のイベントはトリプルです。たとえば、 System.Data.SqlClient.WriteCommandBefore



System.Data.SqlClient.WriteCommandAfter



、およびSystem.Data.SqlClient.WriteCommandError



で、最後のイベントは操作の結果に依存します。 この場合、操作が正常に完了した場合はSystem.Data.SqlClient.WriteCommandAfter



イベントのみが発生し、操作中にエラーが発生した場合はSystem.Data.SqlClient.WriteCommandError



イベントのみがSystem.Data.SqlClient.WriteCommandError



ます。







たとえば、イベントを使用して操作の時間を測定する場合、これを考慮する必要があります。 たとえば、操作の開始時にストップウォッチを開始する場合、データを失わないように2つの場所でストップウォッチを停止する必要があります。







DiagnosticSourceの例



これで、DiagnosticSourceメカニズムを実際のアプリケーションでどのように実行できるかを検討する準備がすべて整いました。







サービス間のCorrelationIDおよび転送ヘッダー



マイクロサービスの世界では、CorrelationIDという用語がよく見られます。 これは、サービスにアクセスするたびに生成される識別子であり、httpヘッダーを介してサービス間でさらに送信されます。 通常、この識別子はログに書き込まれるため、単一のトランザクションの一部として受信した複数のサービスからのメッセージをリンクできます。







ASP.NET CoreにはCorrelationId NuGetパッケージがありますが、開発者はすべての発信要求に適切なヘッダーを手動で追加する必要があるため、使用するのはあまり便利ではありません。







DiagnosticSourceを使用してCorrelationIdを実装します。 まず、識別子の保存を担当するCorrelationId



クラスを追加します。







 public static class CorrelationId { private static readonly AsyncLocal<Guid?> _current = new AsyncLocal<Guid?>(); public static Guid Current { get { var value = _current.Value; if (value == null) throw new InvalidOperationException("CorrelationId isn't assigned."); return value.Value; } set { _current.Value = value; } } }
      
      





このクラスは、タイプAsyncLocal <T>のインスタンスを使用して、現在のCorrelationId値を格納します。これは、各リクエストに対して一意ですが、非同期コードを操作するときにThreadPoolから別のスレッドに正しく転送されます







次のステップは、DiagnosticSourceからイベントハンドラーを追加することです。これにより、着信および発信HTTP要求がインターセプトされます。 受信リクエストでは、 X-Correlation-ID



ヘッダーの存在を確認し、存在しない場合は、 Guid.NewGuid()



介して新しい識別子を生成します。 発信リクエストでは、 CorrelationId.Current



を使用してヘッダーを追加するだけです。







 public sealed class CorrelationIdHandler : DiagnosticObserverBase { protected override bool IsMatch(string name) { return name == "Microsoft.AspNetCore" || name == "HttpHandlerDiagnosticListener"; } //   http  [DiagnosticName("Microsoft.AspNetCore.Hosting.HttpRequestIn")] public void OnHttpRequestIn() { } [DiagnosticName("Microsoft.AspNetCore.Hosting.HttpRequestIn.Start")] public void OnHttpRequestInStart(HttpContext httpContext) { //    CorrelationId    http . var headers = httpContext.Request.Headers; if (headers.TryGetValue("X-Correlation-ID", out var header)) { if (Guid.TryParse(header, out var correlationId)) { CorrelationId.Current = correlationId; return; } } //    CorrelationId. CorrelationId.Current = Guid.NewGuid(); } //   http  [DiagnosticName("System.Net.Http.HttpRequestOut")] public void OnHttpRequestOut() { } [DiagnosticName("System.Net.Http.HttpRequestOut.Start")] public void OnHttpRequestOutStart(HttpRequestMessage request) { //     http    CorrelationId var correlationId = CorrelationId.Current.ToString(); request.Headers.Add("X-Correlation-ID", correlationId); } }
      
      





このクラスでは、 IsMatch



メソッドIsMatch



Microsoft.AspNetCore



コンポーネント(着信HTTP要求に対応)およびHttpHandlerDiagnosticListener



(発信HTTP要求に責任)からのイベントを処理することを報告します。 ヘッダーの直接処理は、 OnHttpRequestInStart



およびOnHttpRequestOutStart



発生しOnHttpRequestOutStart









さらに、2つのダミーメソッドOnHttpRequestIn



OnHttpRequestOut



を追加するOnHttpRequestIn



ありOnHttpRequestOut



。 処理中は呼び出されませんが、 Start



ハンドラとStop



ハンドラのペアを呼び出すかどうかを決定するために使用されます。 それらがなければ、これらのイベントはトリガーされません。







Startup.csファイルにハンドラーを登録するだけです。







 services.AddDiagnosticObserver<CorrelationIdHandler>();
      
      





実際には、1つではなく特定のプレフィックス(たとえば、「X-Api-」)を持つ複数のヘッダーを転送することも有用です。これにより、いわゆるコンテキスト伝播が実装されます。 このメカニズムを使用すると、リクエスト本体を介してこの値を明示的に渡すことなく、あるサービスの特定のキーに値を設定し、別のサービスに読み込むことができます。 上記のCorrelationIdHandler



クラスに基づいて、同様のメカニズムを簡単に実装できます。







メトリックとトレースのコレクション



メトリックとトレースは、アプリケーションの重要な部分です。 メトリックを使用すると、アプリケーションの監視とダッシュボード、およびトレースを構成して、それらのボトルネックを見つけることができます。







OZON.ruでは、 Prometheusを使用してメトリックを収集し、ASP.NET Coreサービスの場合はNuGetパッケージPrometheus.Client.AspNetCoreを使用します。







トレースを収集するには、 OpenTracingJaegerを使用します。 (ご希望の場合は、DotNetMsk Meetut#30での私の講演 「.NETでのOpenTracingの使用」をご覧ください)







ただし、多くの開発者は、アプリケーションをメトリックとトレースでカバーすることにあまり熱心ではありません。これは、多くの場合、追加の統一コードを記述する必要があり、「ビジネスタスク」とあまり整合性がないためです。







幸いなことに、DiagnosticSourceを通じてイベントをディスパッチするほとんどのコンポーネントはペアのイベントを送信します。最初のイベントは特定の操作の開始を示し、2番目は特定の操作の完了を示します。 これにより、たとえば、最初にストップウォッチを開始し、次に停止して特定のメトリックを与えることができます。







たとえば、すべてのコントローラーアクションの実行時間のメトリックを収集する場合、次のクラスを使用できます。







 public sealed class AspNetCoreMetricsHandler : DiagnosticObserverBase { private readonly Histogram requestDurationSeconds; public MetricsHandler(MetricFactory metricFactory) { //  ,  NuGet  Prometheus.Client. //        . requestDurationSeconds = metricFactory.CreateHistogram( "request_duration_seconds", "", labelNames: new[] {"action_name"}); } protected override bool IsMatch(string name) { return name == "Microsoft.AspNetCore"; } [DiagnosticName("Microsoft.AspNetCore.Hosting.HttpRequestIn")] public void OnHttpRequestIn() { } [DiagnosticName("Microsoft.AspNetCore.Hosting.HttpRequestIn.Start")] public void OnHttpRequestInStart(HttpContext httpContext) { //     http   . httpContext.Items["Stopwatch"] = Stopwatch.StartNew(); } [DiagnosticName("Microsoft.AspNetCore.Mvc.BeforeAction")] public void OnBeforeAction(HttpContext httpContext, ActionDescriptor actionDescriptor) { //    , //     . httpContext.Items["ActionName"] = actionDescriptor.DisplayName; } [DiagnosticName("Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop")] public void OnHttpRequestInStop(HttpContext httpContext) { //     http    //      . if (!httpContext.Items.TryGetValue("Stopwatch", out object stopwatch)) return; if (!httpContext.Items.TryGetValue("ActionName", out object actionName)) actionName = "Unknown"; var duration = ((Stopwatch) stopwatch).Elapsed.TotalSeconds; requestDurationSeconds .WithLabels(actionName.ToString()) .Observe(duration); } }
      
      





ここで、コンストラクターでPrometheus.Clientの NuGetパッケージからヒストグラムメトリックを宣言します。 このメトリックにラベル「action_name」を追加します。これにより、コントローラーのさまざまなアクションで収集されたメトリックを区別できます。







イベント処理の開始時に( OnHttpRequestInStart



メソッド)、ストップウォッチを開始してクエリの実行時間を測定します。 また、処理されているアクションの名前( OnBeforeAction



メソッド)も覚えています。 そして最後に、リクエスト( OnHttpRequestInStop



メソッド)を処理した後、 httpContext.Items



コレクションからすべてのデータを再度取得し、メトリックに書き込みます。







MetricFactory



ファイルにハンドラーとMetricFactory



インスタンスを登録するだけです。







 services.AddSingleton(Prometheus.Client.Metrics.DefaultFactory); services.AddDiagnosticObserver<AspNetCoreMetricsHandler>();
      
      





同様の手法を使用して、NuGet OpenTracingパッケージを使用してトレースを収集できます。







ロギング



DiagnosticSourceのもう1つの非常に便利なアプリケーションは、例外ログです。 しかし、「なぜこれが必要なのか」という疑問が生じるかもしれません。 結局のところ、コードをtry-catchブロックにラップするか、未処理のすべての例外に対して1つのグローバルハンドラーを構成することもできます。







事実、DiagnosticSourceイベントによる例外処理は、例外の理由を理解するのに役立つさまざまなオブジェクトがまだ利用可能な非常に早い段階で発生するということです。 (DiagnosticSourceでは例外の処理のみが許可されますが、それ以上の伝播は妨げられないことに注意してください)







クエリテキストとそのパラメーターをログに記録しながら、データベースにアクセスするときにすべての例外を集中的に処理するとします。 DiagnosticSourceを使用して、次のようにこれを行うことができます。







 public sealed class SqlClientLoggingHandler : DiagnosticObserverBase { private readonly ILogger<SqlClientLoggingHandler> _logger; public SqlClientLoggingHandler(ILogger<SqlClientLoggingHandler> logger) { _logger = logger; } protected override bool IsMatch(string name) { return name == "SqlClientDiagnosticListener"; } [DiagnosticName("System.Data.SqlClient.WriteCommandError")] public void OnCommandError(DbCommand command, Exception exception) { var sb = new StringBuilder(); sb.AppendLine("Command: " + command.CommandText); if (command.Parameters.Count > 0) { sb.AppendLine("Parameters: "); foreach (DbParameter parameter in command.Parameters) { sb.AppendLine($"\t{parameter.ParameterName}: {parameter.Value}"); } } _logger.LogError(exception, sb.ToString()); } }
      
      





IsMatch



, SqlClientDiagnosticListener



, OnCommandError



.







Startup.cs :







 services.AddDiagnosticObserver<SqlClientLoggingHandler>();
      
      





おわりに



DiagnosticSource. , , , DiagnosticSource, ASP.NET Core.







OZON.ru . , NuGet , , .







, , DiagnosticSource .








All Articles