[ASP.NET Core] Remove OTEL_SEMCONV_STABILITY_OPT_IN (#5066)

This commit is contained in:
Vishwesh Bankwar 2023-11-21 13:42:10 -08:00 committed by GitHub
parent a55341a50b
commit 0c4f065484
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 149 additions and 1154 deletions

View File

@ -18,7 +18,6 @@
using System.Diagnostics.Metrics;
using System.Reflection;
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Instrumentation.AspNetCore;
@ -46,11 +45,10 @@ internal sealed class AspNetCoreMetrics : IDisposable
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
private readonly Meter meter;
internal AspNetCoreMetrics(AspNetCoreMetricsInstrumentationOptions options)
internal AspNetCoreMetrics()
{
Guard.ThrowIfNull(options);
this.meter = new Meter(InstrumentationName, InstrumentationVersion);
var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter, options);
var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter);
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent);
this.diagnosticSourceSubscriber.Subscribe();
}

View File

@ -1,45 +0,0 @@
// <copyright file="AspNetCoreMetricsInstrumentationOptions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace OpenTelemetry.Instrumentation.AspNetCore;
/// <summary>
/// Options for metrics requests instrumentation.
/// </summary>
internal sealed class AspNetCoreMetricsInstrumentationOptions
{
internal readonly HttpSemanticConvention HttpSemanticConvention;
/// <summary>
/// Initializes a new instance of the <see cref="AspNetCoreMetricsInstrumentationOptions"/> class.
/// </summary>
public AspNetCoreMetricsInstrumentationOptions()
: this(new ConfigurationBuilder().AddEnvironmentVariables().Build())
{
}
internal AspNetCoreMetricsInstrumentationOptions(IConfiguration configuration)
{
Debug.Assert(configuration != null, "configuration was null");
this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration);
}
}

View File

@ -2,6 +2,12 @@
## Unreleased
* Removed support for `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. The
library will now emit only the
[stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http)
semantic conventions.
([#5066](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5066))
## 1.6.0-beta.3
Released 2023-Nov-17

View File

@ -32,7 +32,6 @@ using OpenTelemetry.Instrumentation.GrpcNetClient;
#endif
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation;
@ -66,8 +65,6 @@ internal class HttpInListener : ListenerHandler
private readonly PropertyFetcher<string> beforeActionTemplateFetcher = new("Template");
#endif
private readonly AspNetCoreInstrumentationOptions options;
private readonly bool emitOldAttributes;
private readonly bool emitNewAttributes;
public HttpInListener(AspNetCoreInstrumentationOptions options)
: base(DiagnosticSourceName)
@ -75,10 +72,6 @@ internal class HttpInListener : ListenerHandler
Guard.ThrowIfNull(options);
this.options = options;
this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old);
this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New);
}
public override void OnEventWritten(string name, object payload)
@ -197,67 +190,36 @@ internal class HttpInListener : ListenerHandler
var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/";
activity.DisplayName = this.GetDisplayName(request.Method);
// see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
if (this.emitOldAttributes)
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md
if (request.Host.HasValue)
{
if (request.Host.HasValue)
activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host);
if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443)
{
activity.SetTag(SemanticConventions.AttributeNetHostName, request.Host.Host);
if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443)
{
activity.SetTag(SemanticConventions.AttributeNetHostPort, request.Host.Port);
}
}
activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method);
activity.SetTag(SemanticConventions.AttributeHttpScheme, request.Scheme);
activity.SetTag(SemanticConventions.AttributeHttpTarget, path);
activity.SetTag(SemanticConventions.AttributeHttpUrl, GetUri(request));
activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol));
if (request.Headers.TryGetValue("User-Agent", out var values))
{
var userAgent = values.Count > 0 ? values[0] : null;
if (!string.IsNullOrEmpty(userAgent))
{
activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent);
}
activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port);
}
}
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
if (this.emitNewAttributes)
if (request.QueryString.HasValue)
{
if (request.Host.HasValue)
// QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571
activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value);
}
RequestMethodHelper.SetHttpMethodTag(activity, request.Method);
activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme);
activity.SetTag(SemanticConventions.AttributeUrlPath, path);
activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol));
if (request.Headers.TryGetValue("User-Agent", out var values))
{
var userAgent = values.Count > 0 ? values[0] : null;
if (!string.IsNullOrEmpty(userAgent))
{
activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host);
if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443)
{
activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port);
}
}
if (request.QueryString.HasValue)
{
// QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571
activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value);
}
RequestMethodHelper.SetHttpMethodTag(activity, request.Method);
activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme);
activity.SetTag(SemanticConventions.AttributeUrlPath, path);
activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol));
if (request.Headers.TryGetValue("User-Agent", out var values))
{
var userAgent = values.Count > 0 ? values[0] : null;
if (!string.IsNullOrEmpty(userAgent))
{
activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent);
}
activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent);
}
}
@ -294,15 +256,7 @@ internal class HttpInListener : ListenerHandler
}
#endif
if (this.emitOldAttributes)
{
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
}
if (this.emitNewAttributes)
{
activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
}
activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
#if !NETSTANDARD2_0
if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod))
@ -366,10 +320,7 @@ internal class HttpInListener : ListenerHandler
return;
}
if (this.emitNewAttributes)
{
activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName);
}
activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName);
if (this.options.RecordException)
{
@ -454,9 +405,7 @@ internal class HttpInListener : ListenerHandler
private string GetDisplayName(string httpMethod, string httpRoute = null)
{
var normalizedMethod = this.emitNewAttributes
? RequestMethodHelper.GetNormalizedHttpMethod(httpMethod)
: httpMethod;
var normalizedMethod = RequestMethodHelper.GetNormalizedHttpMethod(httpMethod);
return string.IsNullOrEmpty(httpRoute)
? normalizedMethod
@ -474,27 +423,14 @@ internal class HttpInListener : ListenerHandler
activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc);
if (this.emitOldAttributes)
{
if (context.Connection.RemoteIpAddress != null)
{
// TODO: This attribute was changed in v1.13.0 https://github.com/open-telemetry/opentelemetry-specification/pull/2614
activity.SetTag(SemanticConventions.AttributeNetPeerIp, context.Connection.RemoteIpAddress.ToString());
}
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md
activity.SetTag(SemanticConventions.AttributeNetPeerPort, context.Connection.RemotePort);
if (context.Connection.RemoteIpAddress != null)
{
activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString());
}
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/rpc/rpc-spans.md
if (this.emitNewAttributes)
{
if (context.Connection.RemoteIpAddress != null)
{
activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString());
}
activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort);
}
activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort);
bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status);
if (validConversion)

View File

@ -24,13 +24,11 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Routing;
#endif
using OpenTelemetry.Trace;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation;
internal sealed class HttpInMetricsListener : ListenerHandler
{
internal const string HttpServerDurationMetricName = "http.server.duration";
internal const string HttpServerRequestDurationMetricName = "http.server.request.duration";
internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException";
@ -43,31 +41,13 @@ internal sealed class HttpInMetricsListener : ListenerHandler
private static readonly object ErrorTypeHttpContextItemsKey = new();
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;
internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstrumentationOptions options)
internal HttpInMetricsListener(string name, Meter meter)
: base(name)
{
this.meter = meter;
this.options = options;
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", "Duration of HTTP server requests.");
}
this.httpServerRequestDuration = meter.CreateHistogram<double>(HttpServerRequestDurationMetricName, "s", "Duration of HTTP server requests.");
}
public override void OnEventWritten(string name, object payload)
@ -77,24 +57,13 @@ internal sealed class HttpInMetricsListener : ListenerHandler
case OnUnhandledDiagnosticsExceptionEvent:
case OnUnhandledHostingExceptionEvent:
{
if (this.emitNewAttributes)
{
this.OnExceptionEventWritten(name, payload);
}
this.OnExceptionEventWritten(name, payload);
}
break;
case OnStopEvent:
{
if (this.emitOldAttributes)
{
this.OnEventWritten_Old(name, payload);
}
if (this.emitNewAttributes)
{
this.OnEventWritten_New(name, payload);
}
this.OnStopEventWritten(name, payload);
}
break;
@ -106,7 +75,7 @@ internal sealed class HttpInMetricsListener : ListenerHandler
// We need to use reflection here as the payload type is not a defined public type.
if (!TryFetchException(payload, out Exception exc) || !TryFetchHttpContext(payload, out HttpContext ctx))
{
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnExceptionEventWritten), HttpServerDurationMetricName);
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnExceptionEventWritten), HttpServerRequestDurationMetricName);
return;
}
@ -127,56 +96,7 @@ internal sealed class HttpInMetricsListener : ListenerHandler
=> HttpContextPropertyFetcher.TryFetch(payload, out ctx) && ctx != null;
}
public void OnEventWritten_Old(string name, object payload)
{
var context = payload as HttpContext;
if (context == null)
{
AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName);
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;
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));
}
}
#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
// 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)
public void OnStopEventWritten(string name, object payload)
{
var context = payload as HttpContext;
if (context == null)

View File

@ -15,8 +15,6 @@
// </copyright>
#if !NET8_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Instrumentation.AspNetCore;
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
#endif
@ -46,22 +44,9 @@ public static class MeterProviderBuilderExtensions
_ = TelemetryHelper.BoxedStatusCodes;
_ = RequestMethodHelper.KnownMethods;
builder.ConfigureServices(services =>
{
services.RegisterOptionsFactory(configuration => new AspNetCoreMetricsInstrumentationOptions(configuration));
});
builder.AddMeter(AspNetCoreMetrics.InstrumentationName);
builder.AddInstrumentation(sp =>
{
var options = sp.GetRequiredService<IOptionsMonitor<AspNetCoreMetricsInstrumentationOptions>>().Get(Options.DefaultName);
// TODO: Add additional options to AspNetCoreMetricsInstrumentationOptions ?
// RecordException - probably doesn't make sense for metric instrumentation
// EnableGrpcAspNetCoreSupport - this instrumentation will also need to also handle gRPC requests
return new AspNetCoreMetrics(options);
});
builder.AddInstrumentation(new AspNetCoreMetrics());
return builder;
#endif

View File

@ -94,7 +94,7 @@ public sealed class BasicTests
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode));
Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
ValidateAspNetCoreActivity(activity, "/api/values");
}
@ -643,7 +643,7 @@ public sealed class BasicTests
Assert.Equal(activityName, middlewareActivity.DisplayName);
// tag http.method should be added on activity started by asp.net core
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpMethod) as string);
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName);
}
@ -761,7 +761,7 @@ public sealed class BasicTests
Assert.Equal(activityName, middlewareActivity.DisplayName);
// tag http.method should be added on activity started by asp.net core
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpMethod) as string);
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName);
}
@ -1091,7 +1091,7 @@ public sealed class BasicTests
Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name);
Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version);
#endif
Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeHttpTarget) as string);
Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string);
}
private static void AssertException(List<Activity> exportedItems)

View File

@ -39,11 +39,11 @@ public class IncomingRequestsCollectionsIsAccordingToTheSpecTests
}
[Theory]
[InlineData("/api/values", null, "user-agent", 503, "503")]
[InlineData("/api/values", "?query=1", null, 503, null)]
[InlineData("/api/values", null, "user-agent", 200, null)]
[InlineData("/api/values", "?query=1", null, 200, null)]
[InlineData("/api/exception", null, null, 503, null)]
[InlineData("/api/exception", null, null, 503, null, true)]
public async Task SuccessfulTemplateControllerCallGeneratesASpan_Old(
public async Task SuccessfulTemplateControllerCallGeneratesASpan_New(
string urlPath,
string query,
string userAgent,
@ -51,102 +51,94 @@ public class IncomingRequestsCollectionsIsAccordingToTheSpecTests
string reasonPhrase,
bool recordException = false)
{
try
var exportedItems = new List<Activity>();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices((IServiceCollection services) =>
{
services.AddSingleton<CallbackMiddleware.CallbackMiddlewareImpl>(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase));
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation(options => options.RecordException = recordException)
.AddInMemoryExporter(exportedItems));
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "none");
var exportedItems = new List<Activity>();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices((IServiceCollection services) =>
{
services.AddSingleton<CallbackMiddleware.CallbackMiddlewareImpl>(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase));
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation(options => options.RecordException = recordException)
.AddInMemoryExporter(exportedItems));
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
try
{
try
if (!string.IsNullOrEmpty(userAgent))
{
if (!string.IsNullOrEmpty(userAgent))
{
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
// Act
var path = urlPath;
if (query != null)
{
path += query;
}
using var response = await client.GetAsync(path);
}
catch (Exception)
{
// ignore errors
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
for (var i = 0; i < 10; i++)
// Act
var path = urlPath;
if (query != null)
{
if (exportedItems.Count == 1)
{
break;
}
// 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));
path += query;
}
using var response = await client.GetAsync(path);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName));
Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod));
Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeHttpScheme));
Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeHttpTarget));
Assert.Equal($"http://localhost{urlPath}{query}", activity.GetTagValue(SemanticConventions.AttributeHttpUrl));
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode));
if (statusCode == 503)
catch (Exception)
{
Assert.Equal(ActivityStatusCode.Error, activity.Status);
// ignore errors
}
else
for (var i = 0; i < 10; i++)
{
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
if (exportedItems.Count == 1)
{
break;
}
// 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));
}
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
Assert.Null(activity.StatusDescription);
if (recordException)
{
Assert.Single(activity.Events);
Assert.Equal("exception", activity.Events.First().Name);
}
ValidateTagValue(activity, SemanticConventions.AttributeHttpUserAgent, userAgent);
activity.Dispose();
}
finally
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress));
Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme));
Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath));
Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery));
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
if (statusCode == 503)
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null);
Assert.Equal(ActivityStatusCode.Error, activity.Status);
Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType));
}
else
{
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
}
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
Assert.Null(activity.StatusDescription);
if (recordException)
{
Assert.Single(activity.Events);
Assert.Equal("exception", activity.Events.First().Name);
}
ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent);
activity.Dispose();
}
private static void ValidateTagValue(Activity activity, string attribute, string expectedValue)

View File

@ -1,196 +0,0 @@
// <copyright file="IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;
using TestApp.AspNetCore;
using Xunit;
namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> factory;
public IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe(WebApplicationFactory<Program> factory)
{
this.factory = factory;
}
[Theory]
[InlineData("/api/values", null, "user-agent", 503, "503")]
[InlineData("/api/values", "?query=1", null, 503, null)]
[InlineData("/api/exception", null, null, 503, null)]
[InlineData("/api/exception", null, null, 503, null, true)]
public async Task SuccessfulTemplateControllerCallGeneratesASpan_Dupe(
string urlPath,
string query,
string userAgent,
int statusCode,
string reasonPhrase,
bool recordException = false)
{
try
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup");
var exportedItems = new List<Activity>();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices((IServiceCollection services) =>
{
services.AddSingleton<CallbackMiddleware.CallbackMiddlewareImpl>(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase));
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation(options => options.RecordException = recordException)
.AddInMemoryExporter(exportedItems));
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
try
{
if (!string.IsNullOrEmpty(userAgent))
{
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
// Act
var path = urlPath;
if (query != null)
{
path += query;
}
using var response = await client.GetAsync(path);
}
catch (Exception)
{
// ignore errors
}
for (var i = 0; i < 10; i++)
{
if (exportedItems.Count == 1)
{
break;
}
// 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));
}
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress));
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName));
Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod));
Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion));
Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeHttpScheme));
Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath));
Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeHttpTarget));
Assert.Equal($"http://localhost{urlPath}{query}", activity.GetTagValue(SemanticConventions.AttributeHttpUrl));
Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery));
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode));
if (statusCode == 503)
{
Assert.Equal(ActivityStatusCode.Error, activity.Status);
}
else
{
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
}
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
Assert.Null(activity.StatusDescription);
if (recordException)
{
Assert.Single(activity.Events);
Assert.Equal("exception", activity.Events.First().Name);
}
ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent);
activity.Dispose();
}
finally
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null);
}
}
private static void ValidateTagValue(Activity activity, string attribute, string expectedValue)
{
if (string.IsNullOrEmpty(expectedValue))
{
Assert.Null(activity.GetTagValue(attribute));
}
else
{
Assert.Equal(expectedValue, activity.GetTagValue(attribute));
}
}
public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl
{
private readonly int statusCode;
private readonly string reasonPhrase;
public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase)
{
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
}
public override async Task<bool> ProcessAsync(HttpContext context)
{
context.Response.StatusCode = this.statusCode;
context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = this.reasonPhrase;
await context.Response.WriteAsync("empty");
if (context.Request.Path.Value.EndsWith("exception"))
{
throw new Exception("exception description");
}
return false;
}
}
}

View File

@ -1,190 +0,0 @@
// <copyright file="IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;
using TestApp.AspNetCore;
using Xunit;
namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_New
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> factory;
public IncomingRequestsCollectionsIsAccordingToTheSpecTests_New(WebApplicationFactory<Program> factory)
{
this.factory = factory;
}
[Theory]
[InlineData("/api/values", null, "user-agent", 200, null)]
[InlineData("/api/values", "?query=1", null, 200, null)]
[InlineData("/api/exception", null, null, 503, null)]
[InlineData("/api/exception", null, null, 503, null, true)]
public async Task SuccessfulTemplateControllerCallGeneratesASpan_New(
string urlPath,
string query,
string userAgent,
int statusCode,
string reasonPhrase,
bool recordException = false)
{
try
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http");
var exportedItems = new List<Activity>();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices((IServiceCollection services) =>
{
services.AddSingleton<CallbackMiddleware.CallbackMiddlewareImpl>(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase));
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation(options => options.RecordException = recordException)
.AddInMemoryExporter(exportedItems));
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
try
{
if (!string.IsNullOrEmpty(userAgent))
{
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
// Act
var path = urlPath;
if (query != null)
{
path += query;
}
using var response = await client.GetAsync(path);
}
catch (Exception)
{
// ignore errors
}
for (var i = 0; i < 10; i++)
{
if (exportedItems.Count == 1)
{
break;
}
// 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));
}
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress));
Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme));
Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath));
Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery));
Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
if (statusCode == 503)
{
Assert.Equal(ActivityStatusCode.Error, activity.Status);
Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType));
}
else
{
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
}
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
Assert.Null(activity.StatusDescription);
if (recordException)
{
Assert.Single(activity.Events);
Assert.Equal("exception", activity.Events.First().Name);
}
ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent);
activity.Dispose();
}
finally
{
Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null);
}
}
private static void ValidateTagValue(Activity activity, string attribute, string expectedValue)
{
if (string.IsNullOrEmpty(expectedValue))
{
Assert.Null(activity.GetTagValue(attribute));
}
else
{
Assert.Equal(expectedValue, activity.GetTagValue(attribute));
}
}
public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl
{
private readonly int statusCode;
private readonly string reasonPhrase;
public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase)
{
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
}
public override async Task<bool> ProcessAsync(HttpContext context)
{
context.Response.StatusCode = this.statusCode;
context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = this.reasonPhrase;
await context.Response.WriteAsync("empty");
if (context.Request.Path.Value.EndsWith("exception"))
{
throw new Exception("exception description");
}
return false;
}
}
}

View File

@ -26,8 +26,6 @@ using Microsoft.AspNetCore.Mvc.Testing;
#if NET8_0_OR_GREATER
using Microsoft.AspNetCore.RateLimiting;
#endif
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
@ -38,8 +36,6 @@ 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;
@ -188,16 +184,11 @@ public class MetricTests
[Theory]
[InlineData("/api/values/2", "api/Values/{id}", null, 200)]
[InlineData("/api/Error", "api/Error", "System.Exception", 500)]
public async Task RequestMetricIsCaptured_New(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode)
public async Task RequestMetricIsCaptured(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
.Build();
var metricItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
@ -237,7 +228,7 @@ public class MetricTests
var metricPoints = GetMetricPoints(metric);
Assert.Single(metricPoints);
AssertMetricPoints_New(
AssertMetricPoints(
metricPoints: metricPoints,
expectedRoutes: new List<string> { expectedRoute },
expectedErrorType,
@ -259,14 +250,9 @@ public class MetricTests
[InlineData("CUSTOM", "_OTHER")]
public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, string expectedMethod)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
.Build();
var metricItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
@ -321,129 +307,6 @@ public class MetricTests
Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal);
}
#if !NET8_0_OR_GREATER
[Fact]
public async Task RequestMetricIsCaptured_Old()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = null })
.Build();
var metricItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var response1 = await client.GetAsync("/api/values");
using var response2 = await client.GetAsync("/api/values/2");
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));
this.meterProvider.Dispose();
var requestMetrics = metricItems
.Where(item => item.Name == "http.server.duration")
.ToArray();
var metric = Assert.Single(requestMetrics);
Assert.Equal("ms", metric.Unit);
var metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);
AssertMetricPoints_Old(
metricPoints: metricPoints,
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
expectedTagsCount: 6);
}
[Fact]
public async Task RequestMetricIsCaptured_Dup()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http/dup" })
.Build();
var metricItems = new List<Metric>();
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(metricItems)
.Build();
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var response1 = await client.GetAsync("/api/values");
using var response2 = await client.GetAsync("/api/values/2");
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));
this.meterProvider.Dispose();
// Validate Old Semantic Convention
var requestMetrics = metricItems
.Where(item => item.Name == "http.server.duration")
.ToArray();
var metric = Assert.Single(requestMetrics);
Assert.Equal("ms", metric.Unit);
var metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);
AssertMetricPoints_Old(
metricPoints: metricPoints,
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
expectedTagsCount: 6);
// Validate New Semantic Convention
requestMetrics = metricItems
.Where(item => item.Name == "http.server.request.duration")
.ToArray();
metric = Assert.Single(requestMetrics);
Assert.Equal("s", metric.Unit);
metricPoints = GetMetricPoints(metric);
Assert.Equal(2, metricPoints.Count);
AssertMetricPoints_New(
metricPoints: metricPoints,
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
null,
200,
expectedTagsCount: 5);
}
#endif
public void Dispose()
{
this.meterProvider?.Dispose();
@ -463,7 +326,7 @@ public class MetricTests
return metricPoints;
}
private static void AssertMetricPoints_New(
private static void AssertMetricPoints(
List<MetricPoint> metricPoints,
List<string> expectedRoutes,
string expectedErrorType,
@ -488,7 +351,7 @@ public class MetricTests
if (metricPoint.HasValue)
{
AssertMetricPoint_New(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount);
AssertMetricPoint(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount);
}
else
{
@ -497,39 +360,7 @@ public class MetricTests
}
}
private static void AssertMetricPoints_Old(
List<MetricPoint> metricPoints,
List<string> expectedRoutes,
int expectedTagsCount)
{
// Assert that one MetricPoint exists for each ExpectedRoute
foreach (var expectedRoute in expectedRoutes)
{
MetricPoint? metricPoint = null;
foreach (var mp in metricPoints)
{
foreach (var tag in mp.Tags)
{
if (tag.Key == SemanticConventions.AttributeHttpRoute && tag.Value.ToString() == expectedRoute)
{
metricPoint = mp;
}
}
}
if (metricPoint.HasValue)
{
AssertMetricPoint_Old(metricPoint.Value, expectedRoute, expectedTagsCount);
}
else
{
Assert.Fail($"A metric for route '{expectedRoute}' was not found");
}
}
}
private static KeyValuePair<string, object>[] AssertMetricPoint_New(
private static void AssertMetricPoint(
MetricPoint metricPoint,
int expectedStatusCode,
string expectedRoute,
@ -587,56 +418,5 @@ public class MetricTests
Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds);
Assert.True(histogramBoundsMatchCorrectly);
return attributes;
}
private static KeyValuePair<string, object>[] AssertMetricPoint_Old(
MetricPoint metricPoint,
string expectedRoute = "api/Values",
int expectedTagsCount = StandardTagsCount)
{
var count = metricPoint.GetHistogramCount();
var sum = metricPoint.GetHistogramSum();
Assert.Equal(1L, count);
Assert.True(sum > 0);
var attributes = new KeyValuePair<string, object>[metricPoint.Tags.Count];
int i = 0;
foreach (var tag in metricPoint.Tags)
{
attributes[i++] = tag;
}
// Inspect Attributes
Assert.Equal(expectedTagsCount, attributes.Length);
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);
}
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;
}
}

View File

@ -49,8 +49,7 @@ public static class RoutingTestCases
continue;
}
result.Add(new object[] { testCase, true });
result.Add(new object[] { testCase, false });
result.Add(new object[] { testCase });
}
return result;

View File

@ -17,14 +17,11 @@
#nullable enable
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using RouteTests.TestApplication;
using Xunit;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace RouteTests;
@ -49,20 +46,14 @@ public class RoutingTests : IClassFixture<RoutingTestFixture>
[Theory]
[MemberData(nameof(TestData))]
public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLegacyConventions)
public async Task TestHttpRoute(RoutingTestCases.TestCase testCase)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { [SemanticConventionOptInKeyName] = useLegacyConventions ? null : "http" })
.Build();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(this.exportedActivities)
.Build()!;
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(this.exportedMetrics)
.Build()!;
@ -91,8 +82,8 @@ public class RoutingTests : IClassFixture<RoutingTestFixture>
var activity = Assert.Single(this.exportedActivities);
var metricPoint = Assert.Single(metricPoints);
GetTagsFromActivity(useLegacyConventions, activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute);
GetTagsFromMetricPoint(useLegacyConventions && Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute);
GetTagsFromActivity(activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute);
GetTagsFromMetricPoint(Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute);
Assert.Equal(testCase.ExpectedStatusCode, activityHttpStatusCode);
Assert.Equal(testCase.ExpectedStatusCode, metricHttpStatusCode);
@ -113,27 +104,23 @@ public class RoutingTests : IClassFixture<RoutingTestFixture>
Assert.Equal(expectedActivityDisplayName, activity.DisplayName);
// Only produce README files based on final semantic conventions
if (!useLegacyConventions)
var testResult = new RoutingTestResult
{
var testResult = new RoutingTestResult
{
IdealHttpRoute = testCase.ExpectedHttpRoute,
ActivityDisplayName = activity.DisplayName,
ActivityHttpRoute = activityHttpRoute,
MetricHttpRoute = metricHttpRoute,
TestCase = testCase,
RouteInfo = RouteInfo.Current,
};
IdealHttpRoute = testCase.ExpectedHttpRoute,
ActivityDisplayName = activity.DisplayName,
ActivityHttpRoute = activityHttpRoute,
MetricHttpRoute = metricHttpRoute,
TestCase = testCase,
RouteInfo = RouteInfo.Current,
};
this.fixture.AddTestResult(testResult);
}
this.fixture.AddTestResult(testResult);
}
private static void GetTagsFromActivity(bool useLegacyConventions, Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute)
private static void GetTagsFromActivity(Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute)
{
var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode;
var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod;
var expectedStatusCodeKey = HttpStatusCode;
var expectedHttpMethodKey = HttpMethod;
httpStatusCode = Convert.ToInt32(activity.GetTagItem(expectedStatusCodeKey));
httpMethod = (activity.GetTagItem(expectedHttpMethodKey) as string)!;
httpRoute = activity.GetTagItem(HttpRoute) as string ?? string.Empty;
@ -141,8 +128,8 @@ public class RoutingTests : IClassFixture<RoutingTestFixture>
private static void GetTagsFromMetricPoint(bool useLegacyConventions, MetricPoint metricPoint, out int httpStatusCode, out string httpMethod, out string? httpRoute)
{
var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode;
var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod;
var expectedStatusCodeKey = HttpStatusCode;
var expectedHttpMethodKey = HttpMethod;
httpStatusCode = 0;
httpMethod = string.Empty;

View File

@ -51,77 +51,6 @@ public partial class GrpcTests : IDisposable
[InlineData(true)]
[InlineData(false)]
public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcAspNetCoreSupport)
{
var exportedItems = new List<Activity>();
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder();
if (enableGrpcAspNetCoreSupport.HasValue)
{
tracerProviderBuilder.AddAspNetCoreInstrumentation(options =>
{
options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value;
});
}
else
{
tracerProviderBuilder.AddAspNetCoreInstrumentation();
}
using var tracerProvider = tracerProviderBuilder
.AddInMemoryExporter(exportedItems)
.Build();
var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() };
var uri = new Uri($"http://localhost:{this.server.Port}");
using var channel = GrpcChannel.ForAddress(uri);
var client = new Greeter.GreeterClient(channel);
var returnMsg = client.SayHello(new HelloRequest()).Message;
Assert.False(string.IsNullOrEmpty(returnMsg));
WaitForExporterToReceiveItems(exportedItems, 1);
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value)
{
Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem));
Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService));
Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod));
Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp), clientLoopbackAddresses);
Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode));
}
else
{
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
}
Assert.Equal(Status.Unset, activity.GetStatus());
// The following are http.* attributes that are also included on the span for the gRPC invocation.
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName));
Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeNetHostPort));
Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod));
Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget));
Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl));
Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string);
}
// Tests for v1.21.0 Semantic Conventions for database client calls.
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md
// This test emits the new attributes.
// This test method can replace the other (old) test method when this library is GA.
[Theory]
[InlineData(null)]
[InlineData(true)]
[InlineData(false)]
public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_New(bool? enableGrpcAspNetCoreSupport)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
@ -190,112 +119,6 @@ public partial class GrpcTests : IDisposable
Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string);
}
// Tests for v1.21.0 Semantic Conventions for database client calls.
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md
// This test emits both the new and older attributes.
// This test method can be deleted when this library is GA.
[Theory]
[InlineData(null)]
[InlineData(true)]
[InlineData(false)]
public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_Dupe(bool? enableGrpcAspNetCoreSupport)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http/dup" })
.Build();
var exportedItems = new List<Activity>();
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration));
if (enableGrpcAspNetCoreSupport.HasValue)
{
tracerProviderBuilder.AddAspNetCoreInstrumentation(options =>
{
options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value;
});
}
else
{
tracerProviderBuilder.AddAspNetCoreInstrumentation();
}
using var tracerProvider = tracerProviderBuilder
.AddInMemoryExporter(exportedItems)
.Build();
var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() };
var uri = new Uri($"http://localhost:{this.server.Port}");
using var channel = GrpcChannel.ForAddress(uri);
var client = new Greeter.GreeterClient(channel);
var returnMsg = client.SayHello(new HelloRequest()).Message;
Assert.False(string.IsNullOrEmpty(returnMsg));
WaitForExporterToReceiveItems(exportedItems, 1);
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(ActivityKind.Server, activity.Kind);
// OLD
if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value)
{
Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem));
Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService));
Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod));
Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp), clientLoopbackAddresses);
Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode));
}
else
{
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
}
Assert.Equal(Status.Unset, activity.GetStatus());
// The following are http.* attributes that are also included on the span for the gRPC invocation.
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName));
Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeNetHostPort));
Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod));
Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget));
Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl));
Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string);
// NEW
if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value)
{
Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem));
Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService));
Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod));
Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeClientAddress), clientLoopbackAddresses);
Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeClientPort));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode));
}
else
{
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName));
Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName));
}
Assert.Equal(Status.Unset, activity.GetStatus());
// The following are http.* attributes that are also included on the span for the gRPC invocation.
Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress));
Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort));
Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme));
Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeUrlPath));
Assert.Equal("2", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion));
Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string);
}
#if NET6_0_OR_GREATER
[Theory(Skip = "Skipping for .NET 6 and higher due to bug #3023")]
#endif