Skip to content

Commit

Permalink
Add metrics to ASP.NET Core (dotnet#46834)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Apr 12, 2023
1 parent 06ff899 commit 7d0c273
Show file tree
Hide file tree
Showing 90 changed files with 2,384 additions and 316 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" />
</ItemGroup>

<!-- Temporary hack to make prototype Metrics DI integration types available -->
<!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -->
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" />
<InternalsVisibleTo Include="InMemory.FunctionalTests" />
<InternalsVisibleTo Include="Sockets.BindTests" />
<InternalsVisibleTo Include="Sockets.FunctionalTests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
Expand Down
15 changes: 9 additions & 6 deletions src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options
#pragma warning restore CS0618 // Type or member is obsolete

services.Configure<GenericWebHostServiceOptions>(options =>
{
// Set the options
options.WebHostOptions = webHostOptions;
// Store and forward any startup errors
options.HostingStartupExceptions = _hostingStartupErrors;
});
{
// Set the options
options.WebHostOptions = webHostOptions;
// Store and forward any startup errors
options.HostingStartupExceptions = _hostingStartupErrors;
});

// REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up
// We need to flow this differently
Expand All @@ -80,6 +80,9 @@ public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();

services.AddMetrics();
services.TryAddSingleton<HostingMetrics>();

// IMPORTANT: This needs to run *before* direct calls on the builder (like UseStartup)
_hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);

Expand Down
7 changes: 5 additions & 2 deletions src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
IApplicationBuilderFactory applicationBuilderFactory,
IEnumerable<IStartupFilter> startupFilters,
IConfiguration configuration,
IWebHostEnvironment hostingEnvironment)
IWebHostEnvironment hostingEnvironment,
HostingMetrics hostingMetrics)
{
Options = options.Value;
Server = server;
Expand All @@ -40,6 +41,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
StartupFilters = startupFilters;
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
HostingMetrics = hostingMetrics;
}

public GenericWebHostServiceOptions Options { get; }
Expand All @@ -55,6 +57,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
public IEnumerable<IStartupFilter> StartupFilters { get; }
public IConfiguration Configuration { get; }
public IWebHostEnvironment HostingEnvironment { get; }
public HostingMetrics HostingMetrics { get; }

public async Task StartAsync(CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -153,7 +156,7 @@ static string ExpandPorts(string ports, string scheme)
application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex);
}

var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory);
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics);

await Server.StartAsync(httpApplication, cancellationToken);
HostingEventSource.Log.ServerReady();
Expand Down
3 changes: 3 additions & 0 deletions src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public SlimWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();

services.AddMetrics();
services.TryAddSingleton<HostingMetrics>();
});
}

Expand Down
14 changes: 9 additions & 5 deletions src/Hosting/Hosting/src/Internal/HostingApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ public HostingApplication(
DiagnosticListener diagnosticSource,
ActivitySource activitySource,
DistributedContextPropagator propagator,
IHttpContextFactory httpContextFactory)
IHttpContextFactory httpContextFactory,
HostingEventSource eventSource,
HostingMetrics metrics)
{
_application = application;
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator);
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator, eventSource, metrics);
if (httpContextFactory is DefaultHttpContextFactory factory)
{
_defaultHttpContextFactory = factory;
Expand Down Expand Up @@ -110,7 +112,7 @@ public void DisposeContext(Context context, Exception? exception)
_httpContextFactory!.Dispose(httpContext);
}

HostingApplicationDiagnostics.ContextDisposed(context);
_diagnostics.ContextDisposed(context);

// Reset the context as it may be pooled
context.Reset();
Expand Down Expand Up @@ -139,9 +141,10 @@ public Activity? Activity

public long StartTimestamp { get; set; }
internal bool HasDiagnosticListener { get; set; }
public bool EventLogEnabled { get; set; }
public bool EventLogOrMetricsEnabled { get; set; }

internal IHttpActivityFeature? HttpActivityFeature;
internal HttpMetricsTagsFeature? MetricsTagsFeature;

public void Reset()
{
Expand All @@ -153,7 +156,8 @@ public void Reset()

StartTimestamp = 0;
HasDiagnosticListener = false;
EventLogEnabled = false;
EventLogOrMetricsEnabled = false;
MetricsTagsFeature?.Tags.Clear();
}
}
}
72 changes: 56 additions & 16 deletions src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Hosting;

internal sealed class HostingApplicationDiagnostics
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;

// internal so it can be used in tests
internal const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn";
private const string ActivityStartKey = ActivityName + ".Start";
Expand All @@ -28,28 +27,46 @@ internal sealed class HostingApplicationDiagnostics
private readonly ActivitySource _activitySource;
private readonly DiagnosticListener _diagnosticListener;
private readonly DistributedContextPropagator _propagator;
private readonly HostingEventSource _eventSource;
private readonly HostingMetrics _metrics;
private readonly ILogger _logger;

public HostingApplicationDiagnostics(
ILogger logger,
DiagnosticListener diagnosticListener,
ActivitySource activitySource,
DistributedContextPropagator propagator)
DistributedContextPropagator propagator,
HostingEventSource eventSource,
HostingMetrics metrics)
{
_logger = logger;
_diagnosticListener = diagnosticListener;
_activitySource = activitySource;
_propagator = propagator;
_eventSource = eventSource;
_metrics = metrics;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void BeginRequest(HttpContext httpContext, HostingApplication.Context context)
{
long startTimestamp = 0;

if (HostingEventSource.Log.IsEnabled())
if (_eventSource.IsEnabled() || _metrics.IsEnabled())
{
context.EventLogEnabled = true;
context.EventLogOrMetricsEnabled = true;
if (httpContext.Features.Get<IHttpMetricsTagsFeature>() is HttpMetricsTagsFeature feature)
{
context.MetricsTagsFeature = feature;
}
else
{
context.MetricsTagsFeature ??= new HttpMetricsTagsFeature();
httpContext.Features.Set<IHttpMetricsTagsFeature>(context.MetricsTagsFeature);
}

startTimestamp = Stopwatch.GetTimestamp();

// To keep the hot path short we defer logging in this function to non-inlines
RecordRequestStartEventLog(httpContext);
}
Expand Down Expand Up @@ -80,7 +97,11 @@ public void BeginRequest(HttpContext httpContext, HostingApplication.Context con
{
if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsBeginRequestKey))
{
startTimestamp = Stopwatch.GetTimestamp();
if (startTimestamp == 0)
{
startTimestamp = Stopwatch.GetTimestamp();
}

RecordBeginRequestDiagnostics(httpContext, startTimestamp);
}
}
Expand Down Expand Up @@ -121,6 +142,25 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
currentTimestamp = Stopwatch.GetTimestamp();
// Non-inline
LogRequestFinished(context, startTimestamp, currentTimestamp);

if (context.EventLogOrMetricsEnabled)
{
var route = httpContext.GetEndpoint()?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route;
var customTags = context.MetricsTagsFeature?.TagsList;

_metrics.RequestEnd(
httpContext.Request.Protocol,
httpContext.Request.IsHttps,
httpContext.Request.Scheme,
httpContext.Request.Method,
httpContext.Request.Host,
route,
httpContext.Response.StatusCode,
exception,
customTags,
startTimestamp,
currentTimestamp);
}
}

if (_diagnosticListener.IsEnabled())
Expand Down Expand Up @@ -159,18 +199,18 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
StopActivity(httpContext, activity, context.HasDiagnosticListener);
}

if (context.EventLogEnabled)
if (context.EventLogOrMetricsEnabled)
{
if (exception != null)
{
// Non-inline
HostingEventSource.Log.UnhandledException();
_eventSource.UnhandledException();
}

// Count 500 as failed requests
if (httpContext.Response.StatusCode >= 500)
{
HostingEventSource.Log.RequestFailed();
_eventSource.RequestFailed();
}
}

Expand All @@ -179,12 +219,11 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ContextDisposed(HostingApplication.Context context)
public void ContextDisposed(HostingApplication.Context context)
{
if (context.EventLogEnabled)
if (context.EventLogOrMetricsEnabled)
{
// Non-inline
HostingEventSource.Log.RequestStop();
_eventSource.RequestStop();
}
}

Expand All @@ -211,7 +250,7 @@ private void LogRequestFinished(HostingApplication.Context context, long startTi
// so check if we logged the start event
if (context.StartLog != null)
{
var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp)));
var elapsed = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);

_logger.Log(
logLevel: LogLevel.Information,
Expand Down Expand Up @@ -302,9 +341,10 @@ internal UnhandledExceptionData(HttpContext httpContext, long timestamp, Excepti
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void RecordRequestStartEventLog(HttpContext httpContext)
private void RecordRequestStartEventLog(HttpContext httpContext)
{
HostingEventSource.Log.RequestStart(httpContext.Request.Method, httpContext.Request.Path);
_metrics.RequestStart(httpContext.Request.IsHttps, httpContext.Request.Scheme, httpContext.Request.Method, httpContext.Request.Host);
_eventSource.RequestStart(httpContext.Request.Method, httpContext.Request.Path);
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand Down
3 changes: 2 additions & 1 deletion src/Hosting/Hosting/src/Internal/HostingEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class HostingEventSource : EventSource
private long _failedRequests;

internal HostingEventSource()
: this("Microsoft.AspNetCore.Hosting")
: base("Microsoft.AspNetCore.Hosting", EventSourceSettings.EtwManifestEventFormat)
{
}

Expand Down Expand Up @@ -78,6 +78,7 @@ public void ServerReady()
WriteEvent(6);
}

[NonEvent]
internal void RequestFailed()
{
Interlocked.Increment(ref _failedRequests);
Expand Down
Loading

0 comments on commit 7d0c273

Please sign in to comment.