aspnetcore/src/Microsoft.AspNetCore.Hosting/Internal/HostingApplicationDiagnosti...

273 lines
11 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Hosting.Internal
{
internal class HostingApplicationDiagnostics
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
private const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn";
private const string ActivityStartKey = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start";
private const string DeprecatedDiagnosticsBeginRequestKey = "Microsoft.AspNetCore.Hosting.BeginRequest";
private const string DeprecatedDiagnosticsEndRequestKey = "Microsoft.AspNetCore.Hosting.EndRequest";
private const string DiagnosticsUnhandledExceptionKey = "Microsoft.AspNetCore.Hosting.UnhandledException";
private const string RequestIdHeaderName = "Request-Id";
private const string CorrelationContextHeaderName = "Correlation-Context";
private readonly DiagnosticListener _diagnosticListener;
private readonly ILogger _logger;
public HostingApplicationDiagnostics(ILogger logger, DiagnosticListener diagnosticListener)
{
_logger = logger;
_diagnosticListener = diagnosticListener;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void BeginRequest(HttpContext httpContext, ref HostingApplication.Context context)
{
long startTimestamp = 0;
if (HostingEventSource.Log.IsEnabled())
{
context.EventLogEnabled = true;
// To keep the hot path short we defer logging in this function to non-inlines
RecordRequestStartEventLog(httpContext);
}
if (_diagnosticListener.IsEnabled())
{
if (_diagnosticListener.IsEnabled(ActivityName, httpContext))
{
context.Activity = StartActivity(httpContext);
}
if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsBeginRequestKey))
{
startTimestamp = Stopwatch.GetTimestamp();
RecordBeginRequestDiagnostics(httpContext, startTimestamp);
}
}
// To avoid allocation, return a null scope if the logger is not on at least to some degree.
if (_logger.IsEnabled(LogLevel.Critical))
{
// Scope may be relevant for a different level of logging, so we always create it
// see: https://github.com/aspnet/Hosting/pull/944
// Scope can be null if logging is not on.
context.Scope = _logger.RequestScope(httpContext);
if (_logger.IsEnabled(LogLevel.Information))
{
if (startTimestamp == 0)
{
startTimestamp = Stopwatch.GetTimestamp();
}
// Non-inline
LogRequestStarting(httpContext);
}
}
context.StartTimestamp = startTimestamp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RequestEnd(HttpContext httpContext, Exception exception, HostingApplication.Context context)
{
// Local cache items resolved multiple items, in order of use so they are primed in cpu pipeline when used
var startTimestamp = context.StartTimestamp;
long currentTimestamp = 0;
// If startTimestamp was 0, then Information logging wasn't enabled at for this request (and calcuated time will be wildly wrong)
// Is used as proxy to reduce calls to virtual: _logger.IsEnabled(LogLevel.Information)
if (startTimestamp != 0)
{
currentTimestamp = Stopwatch.GetTimestamp();
// Non-inline
LogRequestFinished(httpContext, startTimestamp, currentTimestamp);
}
if (_diagnosticListener.IsEnabled())
{
if (currentTimestamp == 0)
{
currentTimestamp = Stopwatch.GetTimestamp();
}
if (exception == null)
{
// No exception was thrown, request was sucessful
if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsEndRequestKey))
{
// Diagnostics is enabled for EndRequest, but it may not be for BeginRequest
// so call GetTimestamp if currentTimestamp is zero (from above)
RecordEndRequestDiagnostics(httpContext, currentTimestamp);
}
}
else
{
// Exception was thrown from request
if (_diagnosticListener.IsEnabled(DiagnosticsUnhandledExceptionKey))
{
// Diagnostics is enabled for UnhandledException, but it may not be for BeginRequest
// so call GetTimestamp if currentTimestamp is zero (from above)
RecordUnhandledExceptionDiagnostics(httpContext, currentTimestamp, exception);
}
}
var activity = context.Activity;
// Always stop activity if it was started
if (activity != null)
{
StopActivity(httpContext, activity);
}
}
if (context.EventLogEnabled && exception != null)
{
// Non-inline
HostingEventSource.Log.UnhandledException();
}
// Logging Scope is finshed with
context.Scope?.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ContextDisposed(HostingApplication.Context context)
{
if (context.EventLogEnabled)
{
// Non-inline
HostingEventSource.Log.RequestStop();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void LogRequestStarting(HttpContext httpContext)
{
// IsEnabled is checked in the caller, so if we are here just log
_logger.Log(
logLevel: LogLevel.Information,
eventId: LoggerEventIds.RequestStarting,
state: new HostingRequestStartingLog(httpContext),
exception: null,
formatter: HostingRequestStartingLog.Callback);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void LogRequestFinished(HttpContext httpContext, long startTimestamp, long currentTimestamp)
{
// IsEnabled isn't checked in the caller, startTimestamp > 0 is used as a fast proxy check
// but that may be because diagnostics are enabled, which also uses startTimestamp, so check here
if (_logger.IsEnabled(LogLevel.Information))
{
var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp)));
_logger.Log(
logLevel: LogLevel.Information,
eventId: LoggerEventIds.RequestFinished,
state: new HostingRequestFinishedLog(httpContext, elapsed),
exception: null,
formatter: HostingRequestFinishedLog.Callback);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RecordBeginRequestDiagnostics(HttpContext httpContext, long startTimestamp)
{
_diagnosticListener.Write(
DeprecatedDiagnosticsBeginRequestKey,
new
{
httpContext = httpContext,
timestamp = startTimestamp
});
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RecordEndRequestDiagnostics(HttpContext httpContext, long currentTimestamp)
{
_diagnosticListener.Write(
DeprecatedDiagnosticsEndRequestKey,
new
{
httpContext = httpContext,
timestamp = currentTimestamp
});
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RecordUnhandledExceptionDiagnostics(HttpContext httpContext, long currentTimestamp, Exception exception)
{
_diagnosticListener.Write(
DiagnosticsUnhandledExceptionKey,
new
{
httpContext = httpContext,
timestamp = currentTimestamp,
exception = exception
});
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RecordRequestStartEventLog(HttpContext httpContext)
{
HostingEventSource.Log.RequestStart(httpContext.Request.Method, httpContext.Request.Path);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private Activity StartActivity(HttpContext httpContext)
{
var activity = new Activity(ActivityName);
if (httpContext.Request.Headers.TryGetValue(RequestIdHeaderName, out var requestId))
{
activity.SetParentId(requestId);
// We expect baggage to be empty by default
// Only very advanced users will be using it in near future, we encouradge them to keep baggage small (few items)
string[] baggage = httpContext.Request.Headers.GetCommaSeparatedValues(CorrelationContextHeaderName);
if (baggage != StringValues.Empty)
{
foreach (var item in baggage)
{
if (NameValueHeaderValue.TryParse(item, out var baggageItem))
{
activity.AddBaggage(baggageItem.Name.ToString(), baggageItem.Value.ToString());
}
}
}
}
if (_diagnosticListener.IsEnabled(ActivityStartKey))
{
_diagnosticListener.StartActivity(activity, new { HttpContext = httpContext });
}
else
{
activity.Start();
}
return activity;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void StopActivity(HttpContext httpContext, Activity activity)
{
_diagnosticListener.StopActivity(activity, new { HttpContext = httpContext });
}
}
}