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 8 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
2 changes: 2 additions & 0 deletions src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* TODO: UPDATE THIS

## 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 @@ -44,6 +47,10 @@ internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstru
this.options = options;
this.httpServerDuration = meter.CreateHistogram<double>(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests.");

// TODO: This needs to be changed to "s" (seconds). This is blocked until we can change the default histogram.
// See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4797
this.httpServerRequestDuration = meter.CreateHistogram<double>(HttpServerRequestDurationMetricName, "ms", "Measures the duration of inbound HTTP requests.");
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved

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

this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New);
Expand Down Expand Up @@ -110,16 +117,6 @@ public override void OnEventWritten(string name, object payload)
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));
}
}
}

#if NET6_0_OR_GREATER
Expand All @@ -144,7 +141,17 @@ public override void OnEventWritten(string name, object payload)
// 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);
if (this.emitNewAttributes)
{
// TODO: This needs to be changed to TotalSeconds. This is blocked until we can change the default histogram.
// See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4797
this.httpServerRequestDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags);
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved
}

if (this.emitOldAttributes)
{
this.httpServerDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags);
}
}
}
}
21 changes: 15 additions & 6 deletions src/OpenTelemetry.Instrumentation.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,22 @@ 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/4bbb8c907402caa90bc077214e8a2c78807c1ab9/docs/http/http-metrics.md).

| Name | Instrument Type | Unit | Description |
|-------|-----------------|------|-------------|
| `http.server.duration` | Histogram | `ms` | Measures the duration of inbound HTTP requests. |
Have you opt-ed into the new Http Semantic Conventions?
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved

* If yes, the instrumentation supports the following metric.
Copy link
Member

Choose a reason for hiding this comment

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

this is not clear. The env variable can take none,http,http/dup as values, This doc should also be structured in that way. i.e what if the env variable is set to "none", (also the default)
what is the env varaible is set to "http"
what is the env varaible is set to "http/dup" - both metric are emitted


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

* If no, the instrumentation supports the following metric.

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

## Advanced configuration

Expand Down
163 changes: 117 additions & 46 deletions test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
public class MetricTests
: IClassFixture<WebApplicationFactory<Program>>, IDisposable
{
public const string SemanticConventionOptInKeyName = "OTEL_SEMCONV_STABILITY_OPT_IN";

private const int StandardTagsCount = 6;

private readonly WebApplicationFactory<Program> factory;
Expand All @@ -47,47 +49,81 @@ public void AddAspNetCoreInstrumentation_BadArgs()
Assert.Throws<ArgumentNullException>(() => builder.AddAspNetCoreInstrumentation());
}

[Fact]
public async Task RequestMetricIsCaptured()
[Theory]
[InlineData(null, true, false, 6)] // emits old metric & attributes
[InlineData("http", false, true, 5)] // emits new metric & attributes
[InlineData("http/dup", true, true, 10)] // emits both old & new
public async Task RequestMetricIsCaptured(string environmentVarValue, bool validateOldSemConv, bool validateNewSemConv, int expectedTagsCount)
{
var metricItems = new List<Metric>();
try
{
Environment.SetEnvironmentVariable(SemanticConventionOptInKeyName, environmentVarValue);

this.meterProvider = Sdk.CreateMeterProviderBuilder()
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
var metricItems = new List<Metric>();

using (var client = this.factory
.WithWebHostBuilder(builder =>
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();

using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false);
using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false);
using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false);
using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

not sure why we need two calls here. Could be simplified by making just one call

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was here before my changes. It appears to be testing with and without paramaters.
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },


response1.EnsureSuccessStatusCode();
response2.EnsureSuccessStatusCode();
}
response1.EnsureSuccessStatusCode();
response2.EnsureSuccessStatusCode();
}

// We need to let End callback execute as it is executed AFTER response was returned.
// In unit tests environment there may be a lot of parallel unit tests executed, so
// giving some breezing room for the End callback to complete
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
// We need to let End callback execute as it is executed AFTER response was returned.
// In unit tests environment there may be a lot of parallel unit tests executed, so
// giving some breezing room for the End callback to complete
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

this.meterProvider.Dispose();
this.meterProvider.Dispose();

var requestMetrics = metricItems
.Where(item => item.Name == "http.server.duration")
.ToArray();
if (validateOldSemConv)
{
var requestMetrics = metricItems
.Where(item => item.Name == "http.server.duration")
.ToArray();

var metric = Assert.Single(requestMetrics);
var metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);
var metric = Assert.Single(requestMetrics);
Assert.Equal("ms", metric.Unit);
var metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);

AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values", expectedTagsCount: expectedTagsCount, validateOldSemConv: true);
AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}", expectedTagsCount: expectedTagsCount, validateOldSemConv: true);
}

AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values");
AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}");
if (validateNewSemConv)
{
var requestMetrics = metricItems
.Where(item => item.Name == "http.server.request.duration")
.ToArray();

var metric = Assert.Single(requestMetrics);

// TODO: This needs to be changed to "s" (seconds). This is blocked until we can change the default histogram.
// See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4797
Assert.Equal("ms", metric.Unit);
var metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);

AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values", expectedTagsCount: expectedTagsCount, validateNewSemConv: true);
AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}", expectedTagsCount: expectedTagsCount, validateNewSemConv: true);
}
}
finally
{
Environment.SetEnvironmentVariable(SemanticConventionOptInKeyName, null);
}
}

[Fact]
Expand Down Expand Up @@ -133,7 +169,7 @@ void ConfigureTestServices(IServiceCollection services)

// Assert single because we filtered out one route
var metricPoint = Assert.Single(GetMetricPoints(metric));
AssertMetricPoint(metricPoint);
AssertMetricPoint(metricPoint, validateOldSemConv: true);
}

[Fact]
Expand Down Expand Up @@ -187,7 +223,7 @@ void ConfigureTestServices(IServiceCollection services)
var metric = Assert.Single(requestMetrics);
var metricPoint = Assert.Single(GetMetricPoints(metric));

var tags = AssertMetricPoint(metricPoint, expectedTagsCount: StandardTagsCount + 2);
var tags = AssertMetricPoint(metricPoint, expectedTagsCount: StandardTagsCount + 2, validateOldSemConv: true);

Assert.Contains(tagsToAdd[0], tags);
Assert.Contains(tagsToAdd[1], tags);
Expand Down Expand Up @@ -215,7 +251,9 @@ private static List<MetricPoint> GetMetricPoints(Metric metric)
private static KeyValuePair<string, object>[] AssertMetricPoint(
MetricPoint metricPoint,
string expectedRoute = "api/Values",
int expectedTagsCount = StandardTagsCount)
int expectedTagsCount = StandardTagsCount,
bool validateNewSemConv = false,
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved
bool validateOldSemConv = false)
{
var count = metricPoint.GetHistogramCount();
var sum = metricPoint.GetHistogramSum();
Expand All @@ -230,20 +268,53 @@ private static KeyValuePair<string, object>[] AssertMetricPoint(
attributes[i++] = tag;
}

var method = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, "GET");
var scheme = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, "http");
var statusCode = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, 200);
var flavor = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, "1.1");
var host = new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostName, "localhost");
var route = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, expectedRoute);
Assert.Contains(method, attributes);
Assert.Contains(scheme, attributes);
Assert.Contains(statusCode, attributes);
Assert.Contains(flavor, attributes);
Assert.Contains(host, attributes);
Assert.Contains(route, attributes);
// Inspect Attributes
Assert.Equal(expectedTagsCount, attributes.Length);

if (validateNewSemConv)
{
var method = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, "GET");
var scheme = new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, "http");
var statusCode = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, 200);
var flavor = new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, "1.1");
var route = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, expectedRoute);
Assert.Contains(method, attributes);
Assert.Contains(scheme, attributes);
Assert.Contains(statusCode, attributes);
Assert.Contains(flavor, attributes);
Assert.Contains(route, attributes);
}

if (validateOldSemConv)
{
var method = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, "GET");
var scheme = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, "http");
var statusCode = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, 200);
var flavor = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, "1.1");
var host = new KeyValuePair<string, object>(SemanticConventions.AttributeNetHostName, "localhost");
var route = new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRoute, expectedRoute);
Assert.Contains(method, attributes);
Assert.Contains(scheme, attributes);
Assert.Contains(statusCode, attributes);
Assert.Contains(flavor, attributes);
Assert.Contains(host, attributes);
Assert.Contains(route, attributes);
}

// Inspect Histogram Bounds
var histogramBuckets = metricPoint.GetHistogramBuckets();
var histogramBounds = new List<double>();
foreach (var t in histogramBuckets)
{
histogramBounds.Add(t.ExplicitBound);
}

// TODO: This will need to test for the new histograms. This is blocked until we can change the default histogram.
// See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4797
Assert.Equal(
expected: new List<double> { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000, double.PositiveInfinity },
actual: histogramBounds);

return attributes;
}
}