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

Rename hosting, routing and error handling metrics to be consistent with OpenTelemetry #49743

Merged
merged 13 commits into from
Aug 5, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,11 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp
httpContext.Request.Host,
route,
httpContext.Response.StatusCode,
reachedPipelineEnd,
exception,
customTags,
startTimestamp,
currentTimestamp);

if (reachedPipelineEnd)
{
_metrics.UnhandledRequest();
}
}

if (reachedPipelineEnd)
Expand Down
121 changes: 92 additions & 29 deletions src/Hosting/Hosting/src/Internal/HostingMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Frozen;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Http;

Expand All @@ -13,26 +14,22 @@ internal sealed class HostingMetrics : IDisposable
public const string MeterName = "Microsoft.AspNetCore.Hosting";

private readonly Meter _meter;
private readonly UpDownCounter<long> _currentRequestsCounter;
private readonly UpDownCounter<long> _activeRequestsCounter;
private readonly Histogram<double> _requestDuration;
private readonly Counter<long> _unhandledRequestsCounter;

public HostingMetrics(IMeterFactory meterFactory)
{
_meter = meterFactory.Create(MeterName);

_currentRequestsCounter = _meter.CreateUpDownCounter<long>(
"http-server-current-requests",
_activeRequestsCounter = _meter.CreateUpDownCounter<long>(
"http.server.active_requests",
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
unit: "{request}",
description: "Number of HTTP requests that are currently active on the server.");

_requestDuration = _meter.CreateHistogram<double>(
"http-server-request-duration",
"http.server.request.duration",
unit: "s",
description: "The duration of HTTP requests on the server.");

_unhandledRequestsCounter = _meter.CreateCounter<long>(
"http-server-unhandled-requests",
description: "Number of HTTP requests that reached the end of the middleware pipeline without being handled by application code.");
description: "Measures the duration of inbound HTTP requests.");
}

// Note: Calling code checks whether counter is enabled.
Expand All @@ -41,35 +38,43 @@ public void RequestStart(bool isHttps, string scheme, string method, HostString
// Tags must match request end.
var tags = new TagList();
InitializeRequestTags(ref tags, isHttps, scheme, method, host);
_currentRequestsCounter.Add(1, tags);
_activeRequestsCounter.Add(1, tags);
}

public void RequestEnd(string protocol, bool isHttps, string scheme, string method, HostString host, string? route, int statusCode, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp)
public void RequestEnd(string protocol, bool isHttps, string scheme, string method, HostString host, string? route, int statusCode, bool unhandledRequest, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp)
{
var tags = new TagList();
InitializeRequestTags(ref tags, isHttps, scheme, method, host);

// Tags must match request start.
if (_currentRequestsCounter.Enabled)
if (_activeRequestsCounter.Enabled)
{
_currentRequestsCounter.Add(-1, tags);
_activeRequestsCounter.Add(-1, tags);
}

if (_requestDuration.Enabled)
{
tags.Add("protocol", protocol);
tags.Add("network.protocol.name", "http");
if (TryGetHttpVersion(protocol, out var httpVersion))
{
tags.Add("network.protocol.version", httpVersion);
}
if (unhandledRequest)
{
tags.Add("aspnetcore.request.is_unhandled", true);
}

// Add information gathered during request.
tags.Add("status-code", GetBoxedStatusCode(statusCode));
tags.Add("http.response.status_code", GetBoxedStatusCode(statusCode));
if (route != null)
{
tags.Add("route", route);
tags.Add("http.route", route);
}
// This exception is only present if there is an unhandled exception.
// An exception caught by ExceptionHandlerMiddleware and DeveloperExceptionMiddleware isn't thrown to here. Instead, those middleware add exception-name to custom tags.
// An exception caught by ExceptionHandlerMiddleware and DeveloperExceptionMiddleware isn't thrown to here. Instead, those middleware add exception.type to custom tags.
if (exception != null)
{
tags.Add("exception-name", exception.GetType().FullName);
tags.Add("exception.type", exception.GetType().FullName);
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
}
if (customTags != null)
{
Expand All @@ -84,36 +89,37 @@ public void RequestEnd(string protocol, bool isHttps, string scheme, string meth
}
}

public void UnhandledRequest()
{
_unhandledRequestsCounter.Add(1);
}

public void Dispose()
{
_meter.Dispose();
}

public bool IsEnabled() => _currentRequestsCounter.Enabled || _requestDuration.Enabled || _unhandledRequestsCounter.Enabled;
public bool IsEnabled() => _activeRequestsCounter.Enabled || _requestDuration.Enabled;

private static void InitializeRequestTags(ref TagList tags, bool isHttps, string scheme, string method, HostString host)
{
tags.Add("scheme", scheme);
tags.Add("method", method);
tags.Add("url.scheme", scheme);
tags.Add("http.request.method", ResolveHttpMethod(method));

_ = isHttps;
_ = host;
// TODO: Support configuration for enabling host header annotations
/*
if (host.HasValue)
{
tags.Add("host", host.Host);
tags.Add("server.address", host.Host);
JamesNK marked this conversation as resolved.
Show resolved Hide resolved

// Port is parsed each time it's accessed. Store part in local variable.
if (host.Port is { } port)
{
// Add port tag when not the default value for the current scheme
if ((isHttps && port != 443) || (!isHttps && port != 80))
{
tags.Add("port", port);
tags.Add("server.port", port);
}
}
}
*/
}

// Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
Expand Down Expand Up @@ -197,4 +203,61 @@ private static object GetBoxedStatusCode(int statusCode)

return statusCode;
}

private static readonly FrozenDictionary<string, string> KnownMethods = FrozenDictionary.ToFrozenDictionary(new[]
{
KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect),
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete),
KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get),
KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head),
KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options),
KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch),
KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post),
KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put),
KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace)
}, StringComparer.OrdinalIgnoreCase);

private static string ResolveHttpMethod(string method)
{
// TODO: Support configuration for configuring known methods
if (KnownMethods.TryGetValue(method, out var result))
{
// KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case.
return result;
}
return "_OTHER";
}

private static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version)
{
if (HttpProtocol.IsHttp11(protocol))
{
version = "1.1";
return true;
}
if (HttpProtocol.IsHttp2(protocol))
{
// HTTP/2 only has one version.
version = "2";
return true;
}
if (HttpProtocol.IsHttp3(protocol))
{
// HTTP/3 only has one version.
version = "3";
return true;
}
if (HttpProtocol.IsHttp10(protocol))
{
version = "1.0";
return true;
}
if (HttpProtocol.IsHttp09(protocol))
{
version = "0.9";
return true;
}
version = null;
return false;
}
}
24 changes: 12 additions & 12 deletions src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ public async Task EventCountersAndMetricsValues()
var hostingApplication1 = CreateApplication(out var features1, eventSource: hostingEventSource, meterFactory: testMeterFactory1);
var hostingApplication2 = CreateApplication(out var features2, eventSource: hostingEventSource, meterFactory: testMeterFactory2);

using var currentRequestsRecorder1 = new MetricCollector<long>(testMeterFactory1, HostingMetrics.MeterName, "http-server-current-requests");
using var currentRequestsRecorder2 = new MetricCollector<long>(testMeterFactory2, HostingMetrics.MeterName, "http-server-current-requests");
using var requestDurationRecorder1 = new MetricCollector<double>(testMeterFactory1, HostingMetrics.MeterName, "http-server-request-duration");
using var requestDurationRecorder2 = new MetricCollector<double>(testMeterFactory2, HostingMetrics.MeterName, "http-server-request-duration");
using var activeRequestsCollector1 = new MetricCollector<long>(testMeterFactory1, HostingMetrics.MeterName, "http.server.active_requests");
using var activeRequestsCollector2 = new MetricCollector<long>(testMeterFactory2, HostingMetrics.MeterName, "http.server.active_requests");
using var requestDurationCollector1 = new MetricCollector<double>(testMeterFactory1, HostingMetrics.MeterName, "http.server.request.duration");
using var requestDurationCollector2 = new MetricCollector<double>(testMeterFactory2, HostingMetrics.MeterName, "http.server.request.duration");

// Act/Assert 1
var context1 = hostingApplication1.CreateContext(features1);
Expand All @@ -75,15 +75,15 @@ public async Task EventCountersAndMetricsValues()
Assert.Equal(0, await currentRequestValues.FirstOrDefault(v => v == 0));
Assert.Equal(0, await failedRequestValues.FirstOrDefault(v => v == 0));

Assert.Collection(currentRequestsRecorder1.GetMeasurementSnapshot(),
Assert.Collection(activeRequestsCollector1.GetMeasurementSnapshot(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(currentRequestsRecorder2.GetMeasurementSnapshot(),
Assert.Collection(activeRequestsCollector2.GetMeasurementSnapshot(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder1.GetMeasurementSnapshot(),
Assert.Collection(requestDurationCollector1.GetMeasurementSnapshot(),
m => Assert.True(m.Value > 0));
Assert.Collection(requestDurationRecorder2.GetMeasurementSnapshot(),
Assert.Collection(requestDurationCollector2.GetMeasurementSnapshot(),
m => Assert.True(m.Value > 0));

// Act/Assert 2
Expand All @@ -106,20 +106,20 @@ public async Task EventCountersAndMetricsValues()
Assert.Equal(0, await currentRequestValues.FirstOrDefault(v => v == 0));
Assert.Equal(2, await failedRequestValues.FirstOrDefault(v => v == 2));

Assert.Collection(currentRequestsRecorder1.GetMeasurementSnapshot(),
Assert.Collection(activeRequestsCollector1.GetMeasurementSnapshot(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(currentRequestsRecorder2.GetMeasurementSnapshot(),
Assert.Collection(activeRequestsCollector2.GetMeasurementSnapshot(),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value),
m => Assert.Equal(1, m.Value),
m => Assert.Equal(-1, m.Value));
Assert.Collection(requestDurationRecorder1.GetMeasurementSnapshot(),
Assert.Collection(requestDurationCollector1.GetMeasurementSnapshot(),
m => Assert.True(m.Value > 0),
m => Assert.True(m.Value > 0));
Assert.Collection(requestDurationRecorder2.GetMeasurementSnapshot(),
Assert.Collection(requestDurationCollector2.GetMeasurementSnapshot(),
m => Assert.True(m.Value > 0),
m => Assert.True(m.Value > 0));
}
Expand Down
Loading