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

HttpSemanticConventions - new metric in AspNetCore Instrumentation. #4802

Merged
merged 51 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
91f08c8
update AspNetCore for new metric 'http.server.request.duration'
TimothyMothra Aug 24, 2023
c111eea
cleanup
TimothyMothra Aug 24, 2023
32d8946
remove attributes
TimothyMothra Aug 24, 2023
3208431
comment
TimothyMothra Aug 24, 2023
d2f7ef4
readme
TimothyMothra Aug 24, 2023
1a49dab
comment
TimothyMothra Aug 24, 2023
7c11551
update readme
TimothyMothra Aug 24, 2023
93fe6ad
cleanup
TimothyMothra Aug 24, 2023
3842aa8
check flag in ctor
TimothyMothra Aug 25, 2023
ec2b825
Merge branch 'main' into 4484_newMetrics
TimothyMothra Aug 25, 2023
5f74488
update tags on metrics
TimothyMothra Aug 29, 2023
9fb75e5
refactor
TimothyMothra Aug 29, 2023
c335f65
Merge branch 'main' into 4484_newMetrics
TimothyMothra Aug 29, 2023
6c86a4a
fix name in log
TimothyMothra Aug 29, 2023
3bcefae
another fix
TimothyMothra Aug 29, 2023
e588453
more fixes
TimothyMothra Aug 29, 2023
88355cd
big refactor, split Old and New into two methods
TimothyMothra Aug 29, 2023
6a3f651
investigating test fix
TimothyMothra Aug 30, 2023
1a72356
final fix for test issue
TimothyMothra Aug 30, 2023
b9d194c
Merge branch 'main' into 4484_newMetrics
TimothyMothra Aug 30, 2023
ac76993
Merge branch 'main' into 4484_newMetrics
TimothyMothra Sep 5, 2023
b57320d
change unit to seconds
TimothyMothra Sep 5, 2023
7fdc29f
update Readme
TimothyMothra Sep 5, 2023
a782b51
cleanup if condition
TimothyMothra Sep 7, 2023
3788d15
fix indentation
TimothyMothra Sep 7, 2023
275de4e
update readme
TimothyMothra Sep 7, 2023
c6d5d76
change test to use IConfiguration
TimothyMothra Sep 7, 2023
3789fdd
markdown lint
TimothyMothra Sep 8, 2023
f110b29
split test methods
TimothyMothra Sep 9, 2023
6a52fcf
Merge branch 'main' into 4484_newMetrics
TimothyMothra Sep 11, 2023
7c11aac
Update src/OpenTelemetry.Instrumentation.AspNetCore/README.md
TimothyMothra Sep 11, 2023
9ab958c
cleanup Readme after suggestion conflict
TimothyMothra Sep 12, 2023
3646823
update changelog
TimothyMothra Sep 12, 2023
314c9d9
update changelog
TimothyMothra Sep 12, 2023
255b2da
refactor tests
TimothyMothra Sep 12, 2023
12d10cf
Update src/OpenTelemetry.Instrumentation.AspNetCore/README.md
TimothyMothra Sep 12, 2023
c38a93e
update changelog
TimothyMothra Sep 12, 2023
26ea138
Merge branch '4484_newMetrics' of https://github.com/TimothyMothra/op…
TimothyMothra Sep 12, 2023
1fd022b
Merge branch 'main' into 4484_newMetrics
TimothyMothra Sep 12, 2023
1f787be
markdownlint
TimothyMothra Sep 12, 2023
0b257af
rewrite changelog
TimothyMothra Sep 13, 2023
4987540
rewrite Readme
TimothyMothra Sep 13, 2023
0cfa424
markdownlint
TimothyMothra Sep 13, 2023
163d4d7
Merge branch 'main' into 4484_newMetrics
TimothyMothra Sep 13, 2023
80df848
rewrite changelog
TimothyMothra Sep 14, 2023
9f83898
Merge branch '4484_newMetrics' of https://github.com/TimothyMothra/op…
TimothyMothra Sep 14, 2023
744a980
edit changelog & readme
TimothyMothra Sep 14, 2023
1d694bc
update
TimothyMothra Sep 14, 2023
390d563
Merge branch 'main' into 4484_newMetrics
TimothyMothra Sep 14, 2023
6d4bb65
Merge branch '4484_newMetrics' of https://github.com/TimothyMothra/op…
TimothyMothra Sep 14, 2023
0015a20
feedback
TimothyMothra Sep 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## Unreleased

* Introduced a new metric, `http.server.request.duration`, for users who opt-in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduced a new metric, http.server.request.duration, for users who opt-in
to the new semantic convention by configuring the OTEL_SEMCONV_STABILITY_OPT_IN
environment variable to value "fillthis". This metric measures time in seconds, but histogram buckets are adjusted
automatically for this metric by OTel SDK which specially treat this metric. This metric follows the OTel Semantic Conventions from (add link).

  • Metric Name: http.server.request.duration
  • Unit: seconds
  • Histogram Buckets: 0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 (due to OpenTelemetry SDK's special treatment)
  • Replaces old metric: http.server.duration
    • Unit: miliseconds
    • Histogram Buckets: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 (the general default of Histogram buckets)

OTEL_SEMCONV_STABILITY_OPT_IN may be set to "fillthis", to help an easier transition, at the cost of emitting both old and new metrics.

^did some refactoring. Feel free to take it with modifications.
A user who do not have any experience with OTel sem. conventions and the new vs old, should find it easy to follow the changelog. Key things I'd prefer to convey here are:

  1. No breaking changes if you just update to this new version.
  2. Opt-in feature to emit a new metric that follows a new convention, which despite using "s", still allows good histogram precision due to SDK treating it specially.
  3. Hint at emitting both to smooth transition.
  4. Hint that the new metric is the long term bet, as it'll be the one getting stabilizied by Otel sem convention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the refactor. I'll take another pass at this today.

Do we typically put instructions in the changelog? I was trying to keep the changelog succinct, and use the readme for the full information. The challenge is that this change has a LOT of details :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just pushed my rewrite. I think I've addressed all your points. please re-review.

to the new semantic convention by configuring the `OTEL_SEMCONV_STABILITY_OPT_IN`
environment variable. This metric measures time in seconds and uses unique
histogram buckets as defined by the semantic conventions for Http
[metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md)
While the convention recommends using custom histogram buckets, this feature
is not yet available via .NET Metrics API. A
[workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820)
has been included in OTel SDK starting version `1.6.0` which applies
recommended buckets by default for this metric.
([#4802](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4802))
* New metric: `http.server.request.duration`
* Unit: `seconds`
* Histogram Buckets: `0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5,
0.75, 1, 2.5, 5, 7.5, 10`
* Replaces old metric: `http.server.duration`
* Unit: `miliseconds`
* Histogram Buckets: `0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500,
5000, 7500, 10000`

## 1.5.1-beta.1

Released 2023-Jul-20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation;

internal sealed class HttpInMetricsListener : ListenerHandler
{
private const string HttpServerDurationMetricName = "http.server.duration";
internal const string HttpServerDurationMetricName = "http.server.duration";
internal const string HttpServerRequestDurationMetricName = "http.server.request.duration";

private const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop";
private const string EventName = "OnStopActivity";

private readonly Meter meter;
private readonly AspNetCoreMetricsInstrumentationOptions options;
private readonly Histogram<double> httpServerDuration;
private readonly Histogram<double> httpServerRequestDuration;
private readonly bool emitOldAttributes;
private readonly bool emitNewAttributes;

Expand All @@ -42,109 +45,172 @@ internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstru
{
this.meter = meter;
this.options = options;
this.httpServerDuration = meter.CreateHistogram<double>(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests.");

this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old);

this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New);

if (this.emitOldAttributes)
{
this.httpServerDuration = meter.CreateHistogram<double>(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests.");
}

if (this.emitNewAttributes)
{
this.httpServerRequestDuration = meter.CreateHistogram<double>(HttpServerRequestDurationMetricName, "s", "Measures the duration of inbound HTTP requests.");
}
}

public override void OnEventWritten(string name, object payload)
{
if (name == OnStopEvent)
{
var context = payload as HttpContext;
if (context == null)
if (this.emitOldAttributes)
{
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName);
return;
this.OnEventWritten_Old(name, payload);
}

try
if (this.emitNewAttributes)
{
if (this.options.Filter?.Invoke(HttpServerDurationMetricName, context) == false)
{
AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName);
return;
}
this.OnEventWritten_New(name, payload);
}
catch (Exception ex)
}
}

public void OnEventWritten_Old(string name, object payload)
{
var context = payload as HttpContext;
if (context == null)
{
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName);
return;
}

try
{
if (this.options.Filter?.Invoke(HttpServerDurationMetricName, context) == false)
{
AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex);
AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName);
return;
}
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex);
return;
}

// TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this.
// Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too).
// If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope.
if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics"))
{
return;
}

TagList tags = default;

// TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this.
// Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too).
// If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope.
if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics"))
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, context.Request.Scheme));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, context.Request.Method));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode)));

if (context.Request.Host.HasValue)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostName, context.Request.Host.Host));

if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443)
{
return;
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostPort, context.Request.Host.Port));
}
}

TagList tags = default;

// see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
if (this.emitOldAttributes)
#if NET6_0_OR_GREATER
var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
if (!string.IsNullOrEmpty(route))
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, route));
}
#endif
if (this.options.Enrich != null)
{
try
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, context.Request.Scheme));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, context.Request.Method));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode)));

if (context.Request.Host.HasValue)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostName, context.Request.Host.Host));

if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostPort, context.Request.Host.Port));
}
}
this.options.Enrich(HttpServerDurationMetricName, context, ref tags);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex);
}
}

// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
if (this.emitNewAttributes)
// We are relying here on ASP.NET Core to set duration before writing the stop event.
// https://github.com/dotnet/aspnetcore/blob/d6fa351048617ae1c8b47493ba1abbe94c3a24cf/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L449
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
this.httpServerDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags);
}

public void OnEventWritten_New(string name, object payload)
{
var context = payload as HttpContext;
if (context == null)
{
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerRequestDurationMetricName);
return;
}

try
{
if (this.options.Filter?.Invoke(HttpServerRequestDurationMetricName, context) == false)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, context.Request.Scheme));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode)));

if (context.Request.Host.HasValue)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerAddress, context.Request.Host.Host));

if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443)
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerPort, context.Request.Host.Port));
}
}
AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInMetricsListener), EventName, HttpServerRequestDurationMetricName);
return;
}
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInMetricsListener), EventName, HttpServerRequestDurationMetricName, ex);
return;
}

// TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this.
// Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too).
// If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope.
if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics"))
{
return;
}

TagList tags = default;

// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, context.Request.Scheme));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method));
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode)));

#if NET6_0_OR_GREATER
var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
if (!string.IsNullOrEmpty(route))
var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
if (!string.IsNullOrEmpty(route))
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, route));
}
#endif
if (this.options.Enrich != null)
{
try
{
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, route));
this.options.Enrich(HttpServerRequestDurationMetricName, context, ref tags);
}
#endif
if (this.options.Enrich != null)
catch (Exception ex)
{
try
{
this.options.Enrich(HttpServerDurationMetricName, context, ref tags);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex);
}
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInMetricsListener), EventName, HttpServerRequestDurationMetricName, ex);
}

// We are relying here on ASP.NET Core to set duration before writing the stop event.
// https://github.com/dotnet/aspnetcore/blob/d6fa351048617ae1c8b47493ba1abbe94c3a24cf/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L449
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
this.httpServerDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags);
}

// We are relying here on ASP.NET Core to set duration before writing the stop event.
// https://github.com/dotnet/aspnetcore/blob/d6fa351048617ae1c8b47493ba1abbe94c3a24cf/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L449
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
this.httpServerRequestDuration.Record(Activity.Current.Duration.TotalSeconds, tags);
}
}
33 changes: 27 additions & 6 deletions src/OpenTelemetry.Instrumentation.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,34 @@ public void ConfigureServices(IServiceCollection services)

#### List of metrics produced

The instrumentation is implemented based on [metrics semantic
conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#metric-httpserverduration).
Currently, the instrumentation supports the following metric.
The instrumentation is implemented based on
[metrics semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md).

| Name | Instrument Type | Unit | Description |
|-------|-----------------|------|-------------|
| `http.server.duration` | Histogram | `ms` | Measures the duration of inbound HTTP requests. |
A different metric is emitted depending if a user opts-in to the new Http
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved
Semantic Conventions using `OTEL_SEMCONV_STABILITY_OPT_IN`.

* By default, the instrumentation emits the following metric.

| Name | Instrument Type | Unit | Description |
|-------|-----------------|------|-------------|
| `http.server.duration` | Histogram | `ms` | Measures the duration of inbound HTTP requests. |

* If user sets the environment variable to `http`, the instrumentation emits
the following metric.

| Name | Instrument Type | Unit | Description |
|-------|-----------------|------|-------------|
| `http.server.request.duration` | Histogram | `s` | Measures the duration of inbound HTTP requests. |

This metric is emitted in `seconds` as per the semantic convention. While
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved
the convention [recommends using custom histogram buckets](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md)
, this feature is not yet available via .NET Metrics API.
A [workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820)
has been included in OTel SDK starting version `1.6.0` which applies
recommended buckets by default for this metric.

* If user sets the environment variable to `http/dup`, the instrumentation
emits both `http.server.duration` and `http.server.request.duration`.

## Advanced configuration

Expand Down
Loading