web-dev-qa-db-ja.com

実際のリクエスト実行時間を取得する方法

次のミドルウェアがあるとします。

public class RequestDurationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestDurationMiddleware> _logger;

    public RequestDurationMiddleware(RequestDelegate next, ILogger<RequestDurationMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var watch = Stopwatch.StartNew();
        await _next.Invoke(context);
        watch.Stop();

        _logger.LogTrace("{duration}ms", watch.ElapsedMilliseconds);
    }
}

パイプラインがあるため、パイプラインの終了前に発生し、さまざまな時間でログに記録されます。

WebApi.Middlewares.RequestDurationMiddleware 2018-01-10 15:00:16.372 -02:00 [Verbose]  382ms
Microsoft.AspNetCore.Server.Kestrel 2018-01-10 15:00:16.374 -02:00 [Debug]  Connection id ""0HLAO9CRJUV0C"" completed keep alive response.
Microsoft.AspNetCore.Hosting.Internal.WebHost 2018-01-10 15:00:16.391 -02:00 [Information]  "Request finished in 405.1196ms 400 application/json; charset=utf-8"

この場合、WebHost(例では405.1196ms)の値から実際のリクエスト実行時間を取得するにはどうすればよいですか?この値をデータベースに保存するか、他の場所で使用したい。

10
natenho

この質問は本当に興味深いと思ったので、WebHostが実際にそのリクエスト時間を測定して表示している方法を理解するために少し調べました。結論は次のとおりです。この情報を取得するための良い方法も、簡単な方法も、きれいな方法もありません。すべてがハックのように感じられます。それでも興味がある場合は、フォローしてください。

アプリケーションが起動すると、WebHostBuilderWebHostを作成し、HostingApplicationを作成します。これは基本的に、着信要求に応答する役割を担うルートコンポーネントです。リクエストが来たときにミドルウェアパイプラインを呼び出すコンポーネントです。

これはalso作成するコンポーネント HostingApplicationDiagnostics であり、リクエスト処理に関する診断を収集できます。リクエストの最初にHostingApplicationHostingApplicationDiagnostics.BeginRequestを呼び出し、リクエストの最後にHostingApplicationDiagnostics.RequestEndを呼び出します。

当然のことながら、HostingApplicationDiagnosticsは、リクエストの継続時間を測定し、表示されているWebHostのメッセージも記録するものです。したがって、これは、情報を取得する方法を理解するために、より詳細に検査する必要があるクラスです。

診断オブジェクトが診断情報を報告するために使用するものは2つあります。ロガーと DiagnosticListener です。

診断リスナー

DiagnosticListenerは興味深いものです。基本的に、イベントを発生させることができる一般的な event sink です。その後、他のオブジェクトがサブスクライブして、これらのイベントをリッスンできます。だから、これは私たちの目的にぴったりです!

DiagnosticListenerが使用するHostingApplicationDiagnosticsオブジェクトはWebHostによって渡され、実際には 依存関係の注入から解決される になります。 WebHostBuilderによってシングルトンとして登録されている )であるため、依存関係の注入からリスナーを解決し、そのイベントにサブスクライブすることができます。それでは、Startupでそれを実行してみましょう。

public void ConfigureServices(IServiceCollection services)
{
    // …

    // register our observer
    services.AddSingleton<DiagnosticObserver>();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    // we inject both the DiagnosticListener and our DiagnosticObserver here
    DiagnosticListener diagnosticListenerSource, DiagnosticObserver diagnosticObserver)
{
    // subscribe to the listener
    diagnosticListenerSource.Subscribe(diagnosticObserver);

    // …
}

DiagnosticObserverを実行するには、これで十分です。オブザーバーはIObserver<KeyValuePair<string, object>>を実装する必要があります。イベントが発生すると、キーと値のペアが取得されます。キーはイベントの識別子であり、値はHostingApplicationDiagnosticsによって渡されるカスタムオブジェクトです。

しかし、オブザーバーを実装する前に、実際にどのようなイベント HostingApplicationDiagnostics が発生するかを実際に確認する必要があります。

残念ながら、リクエストが終了すると、診断リスターで発生したイベントは単に渡されます 終了タイムスタンプ なので、発生したイベントをリッスンして、開始タイムスタンプを読み取るリクエストの 最初 をリッスンする必要もあります。しかし、これはオブザーバーに状態を導入します。これは、ここでは避けたいものです。さらに、実際のイベント名定数は 接頭辞Deprecated です。これは、これらを使用しないようにする必要があることを示すインジケータになる場合があります。

好ましい方法は activities を使用することです。これも診断オブザーバーと密接に関連しています。アクティビティは明らかに、アプリケーションに表示されるアクティビティを追跡する状態です。それらは、ある時点で開始および停止され、すでに自分で実行した時間も記録されています。そのため、オブザーバーにアクティビティの停止イベントをリッスンさせるだけで、完了時に通知を受け取ることができます。

public class DiagnosticObserver : IObserver<KeyValuePair<string, object>>
{
    private readonly ILogger<DiagnosticObserver> _logger;
    public DiagnosticObserver(ILogger<DiagnosticObserver> logger)
    {
        _logger = logger;
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }

    public void OnNext(KeyValuePair<string, object> value)
    {
        if (value.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop")
        {
            var httpContext = value.Value.GetType().GetProperty("HttpContext")?.GetValue(value.Value) as HttpContext;
            var activity = Activity.Current;

            _logger.LogWarning("Request ended for {RequestPath} in {Duration} ms",
                httpContext.Request.Path, activity.Duration.TotalMilliseconds);
        }
    }
}

残念ながら欠点はなく、解決策はありません…この解決策は並列リクエストに対して非常に不正確であることがわかりました(たとえば、画像やスクリプトもあるページを開く場合)並行してリクエストされます)。これは、アクティビティを取得するために静的Activity.Currentを使用しているためと考えられます。ただし、単一のリクエストのアクティビティのみを取得する方法は実際にはないようです。渡されたキーと値のペアから。

そこで、私は戻って、これらの非推奨イベントを使用して、元のアイデアを再試行しました。私が理解した方法は、ところでです。すぐに削除されるのではなく、アクティビティの使用が推奨されるため、これらは廃止されているだけです(もちろん、ここでは実装の詳細と内部クラスを使用しているため、これらはいつでも変更される可能性があります)並行性の問題を回避するには、(クラスフィールドではなく)HTTPコンテキスト内に状態を保存する必要があります。

private const string StartTimestampKey = "DiagnosticObserver_StartTimestamp";

public void OnNext(KeyValuePair<string, object> value)
{
    if (value.Key == "Microsoft.AspNetCore.Hosting.BeginRequest")
    {
        var httpContext = (HttpContext)value.Value.GetType().GetProperty("httpContext").GetValue(value.Value);
        httpContext.Items[StartTimestampKey] = (long)value.Value.GetType().GetProperty("timestamp").GetValue(value.Value);
    }
    else if (value.Key == "Microsoft.AspNetCore.Hosting.EndRequest")
    {
        var httpContext = (HttpContext)value.Value.GetType().GetProperty("httpContext").GetValue(value.Value);
        var endTimestamp = (long)value.Value.GetType().GetProperty("timestamp").GetValue(value.Value);
        var startTimestamp = (long)httpContext.Items[StartTimestampKey];

        var duration = new TimeSpan((long)((endTimestamp - startTimestamp) * TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency));
        _logger.LogWarning("Request ended for {RequestPath} in {Duration} ms",
            httpContext.Request.Path, duration.TotalMilliseconds);
    }
}

これを実行すると、実際には正確な結果が得られ、リクエストの識別に使用できるHttpContextにもアクセスできます。もちろん、ここで発生するオーバーヘッドは非常に明白です。プロパティ値にアクセスするためのリフレクション。情報をHttpContext.Itemsに保存する必要があります。これは、オブザーバー全体のことです。

診断ソースとアクティビティの詳細: DiagnosticSource Users Guid および アクティビティユーザーガイド

ロギング

上記のどこかで、HostingApplicationDiagnosticsも情報をロギング機能に報告することを述べました。もちろん、結局のところ、これがコンソールに表示されているものです。そして 実装を見る の場合、これはすでに適切な期間をここで計算していることがわかります。これは構造化ロギングであるため、これを使用してその情報を取得できます。

それでは、 その正確な状態オブジェクト をチェックして、何ができるかを確認するカスタムロガーを作成してみましょう。

public class RequestDurationLogger : ILogger, ILoggerProvider
{
    public ILogger CreateLogger(string categoryName) => this;
    public void Dispose() { }
    public IDisposable BeginScope<TState>(TState state) => NullDisposable.Instance;
    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (state.GetType().FullName == "Microsoft.AspNetCore.Hosting.Internal.HostingRequestFinishedLog" &&
            state is IReadOnlyList<KeyValuePair<string, object>> values &&
            values.FirstOrDefault(kv => kv.Key == "ElapsedMilliseconds").Value is double milliseconds)
        {
            Console.WriteLine($"Request took {milliseconds} ms");
        }
    }

    private class NullDisposable : IDisposable
    {
        public static readonly NullDisposable Instance = new NullDisposable();
        public void Dispose() { }
    }
}

残念ながら(おそらく、このWordを今すぐ気に入っていますよね?)、状態クラスHostingRequestFinishedLogは内部クラスなので、直接使用することはできません。したがって、リフレクションを使用してそれを識別する必要があります。ただし、名前が必要な場合は、読み取り専用リストから値を抽出できます。

あとは、ロガー(プロバイダー)をWebホストに登録するだけです。

WebHost.CreateDefaultBuilder(args)
    .ConfigureLogging(logging =>
    {
        logging.AddProvider(new RequestDurationLogger());
    })
    .UseStartup<Startup>()
    .Build();

そして、実際に、標準のログ記録とまったく同じ情報にアクセスするために必要なのはこれだけです。

ただし、2つの問題があります。ここにはHttpContextがないため、この期間が実際に属しているリクエストに関する情報を取得できません。 HostingApplicationDiagnosticsを見るとわかるように、このロギングの呼び出しは実際には ログレベルが少なくともInformation の場合にのみ行われます。

リフレクションを使用してプライベートフィールド_httpContextを読み取ることによりHttpContextを取得できますが、ログレベルに関して実行できることは何もありません。そしてもちろん、特定のロギング呼び出しから情報を取得するロガーを作成しているという事実は非常にハックであり、おそらく良い考えではありません。

結論

だから、これはすべてひどいです。 HostingApplicationDiagnosticsからこの情報を取得する明確な方法はありません。また、診断機能は実際には有効になっている場合にのみ実行されることにも注意してください。また、パフォーマンスが重要なアプリケーションは、ある時点でそれを無効にする可能性があります。いずれにしても、この情報を診断以外の目的で使用することは、一般に脆弱すぎるため、お勧めできません。

それで、より良い解決策は何ですか?診断コンテキストを超えて機能するソリューション? 早期に実行される単純なミドルウェア;あなたがすでに使用したように。はい、これは外部リクエスト処理パイプラインからいくつかのパスを除外するほど正確ではない可能性がありますが、実際のアプリケーションコードの正確な測定になります。結局のところ、フレームワークのパフォーマンスを測定したい場合は、とにかく外部から測定する必要があります。つまり、クライアントとして、要求を行う(ベンチマークが機能するように)のです。

そしてところで。これは、Stack Overflow独自の MiniProfiler が機能する方法でもあります。あなたはただ ミドルウェアを早期に登録する で、それだけです。

20
poke

いくつかの変更を加えるだけでミドルウェアを使用できます。私はこのような使用して応答ヘッダーに応答時間を追加します:

 public class ResponseTimeMiddleware
{
    // Name of the Response Header, Custom Headers starts with "X-"  
    private const string RESPONSE_HEADER_RESPONSE_TIME = "X-Response-Time-ms";
    // Handle to the next Middleware in the pipeline  
    private readonly RequestDelegate _next;
    public ResponseTimeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task InvokeAsync(HttpContext context)
    {
        // Start the Timer using Stopwatch  
        var watch = new Stopwatch();
        watch.Start();
        context.Response.OnStarting(() => {
            // Stop the timer information and calculate the time   
            watch.Stop();
            var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
            // Add the Response time information in the Response headers.   
            context.Response.Headers[RESPONSE_HEADER_RESPONSE_TIME] = responseTimeForCompleteRequest.ToString();
            return Task.CompletedTask;
        });
        // Call the next delegate/middleware in the pipeline   
        return this._next(context);
    }
}
1
Alex