Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics to ASP.NET Core #46834

Merged
merged 9 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 -->
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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
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
Expand Up @@ -26,7 +26,8 @@ internal sealed partial class GenericWebHostService : IHostedService
IApplicationBuilderFactory applicationBuilderFactory,
IEnumerable<IStartupFilter> startupFilters,
IConfiguration configuration,
IWebHostEnvironment hostingEnvironment)
IWebHostEnvironment hostingEnvironment,
HostingMetrics hostingMetrics)
{
Options = options.Value;
Server = server;
Expand All @@ -40,6 +41,7 @@ internal sealed partial class GenericWebHostService : IHostedService
StartupFilters = startupFilters;
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
HostingMetrics = hostingMetrics;
}

public GenericWebHostServiceOptions Options { get; }
Expand All @@ -55,6 +57,7 @@ internal sealed partial class GenericWebHostService : IHostedService
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
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
Expand Up @@ -23,10 +23,12 @@ internal sealed class HostingApplication : IHttpApplication<HostingApplication.C
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 @@ internal sealed class Context

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
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
Expand Up @@ -20,7 +20,7 @@ internal sealed class HostingEventSource : EventSource
private long _failedRequests;

internal HostingEventSource()
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
: 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