From 0343116e7781779975a1a0fe269be1778a484eeb Mon Sep 17 00:00:00 2001 From: Kyle Tully Date: Tue, 25 Jul 2023 14:25:02 -0500 Subject: [PATCH] File scoped namespaces instrumentation.grpc net client (#4692) --- .../AspNetCoreInstrumentation.cs | 53 +- .../AspNetCoreInstrumentationOptions.cs | 145 +- .../AspNetCoreMetrics.cs | 73 +- ...AspNetCoreMetricsInstrumentationOptions.cs | 95 +- .../AspNetCoreInstrumentationEventSource.cs | 83 +- .../Implementation/HttpInListener.cs | 759 ++++--- .../Implementation/HttpInMetricsListener.cs | 181 +- .../Implementation/HttpTagHelper.cs | 39 +- .../MeterProviderBuilderExtensions.cs | 111 +- .../TracerProviderBuilderExtensions.cs | 173 +- .../GrpcClientInstrumentation.cs | 37 +- .../GrpcClientInstrumentationOptions.cs | 75 +- .../GrpcTagHelper.cs | 105 +- .../GrpcClientDiagnosticListener.cs | 301 ++- .../GrpcInstrumentationEventSource.cs | 47 +- .../StatusCanonicalCode.cs | 259 ++- .../TracerProviderBuilderExtensions.cs | 101 +- .../HttpRequestMessageContextPropagation.cs | 29 +- .../DiagnosticSourceListener.cs | 49 +- .../DiagnosticSourceSubscriber.cs | 137 +- src/Shared/SpanHelper.cs | 35 +- .../AttributesExtensions.cs | 11 +- .../BasicTests.cs | 1829 ++++++++--------- .../DependencyInjectionConfigTests.cs | 113 +- .../EventSourceTest.cs | 13 +- .../InProcServerTests.cs | 101 +- ...stsCollectionsIsAccordingToTheSpecTests.cs | 251 ++- ...llectionsIsAccordingToTheSpecTests_Dupe.cs | 267 ++- ...ollectionsIsAccordingToTheSpecTests_New.cs | 251 ++- .../MetricTests.cs | 355 ++-- .../EventSourceTest.cs | 13 +- .../GrpcServer.cs | 120 +- .../GrpcTagHelperTests.cs | 89 +- .../GrpcTestHelpers/ClientTestHelpers.cs | 99 +- .../GrpcTestHelpers/ResponseUtils.cs | 93 +- .../GrpcTestHelpers/TestHttpMessageHandler.cs | 49 +- .../GrpcTestHelpers/TrailingHeadersHelpers.cs | 53 +- .../GrpcTests.client.cs | 1081 +++++----- .../GrpcTests.server.cs | 603 +++--- .../Services/GreeterService.cs | 45 +- 40 files changed, 4139 insertions(+), 4184 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs index 913f1408fa3..71087e45697 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -15,38 +15,37 @@ // using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreInstrumentation : IDisposable { - /// - /// Asp.Net Core Requests instrumentation. - /// - internal sealed class AspNetCoreInstrumentation : IDisposable + private static readonly HashSet DiagnosticSourceEvents = new() { - private static readonly HashSet DiagnosticSourceEvents = new() - { - "Microsoft.AspNetCore.Hosting.HttpRequestIn", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", - "Microsoft.AspNetCore.Mvc.BeforeAction", - "Microsoft.AspNetCore.Diagnostics.UnhandledException", - "Microsoft.AspNetCore.Hosting.UnhandledException", - }; + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + "Microsoft.AspNetCore.Mvc.BeforeAction", + "Microsoft.AspNetCore.Diagnostics.UnhandledException", + "Microsoft.AspNetCore.Hosting.UnhandledException", + }; - private readonly Func isEnabled = (eventName, _, _) - => DiagnosticSourceEvents.Contains(eventName); + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - public AspNetCoreInstrumentation(HttpInListener httpInListener) - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); - } + public AspNetCoreInstrumentation(HttpInListener httpInListener) + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, this.isEnabled); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs index cca902e2c74..28081307974 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs @@ -19,91 +19,90 @@ using Microsoft.Extensions.Configuration; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Options for requests instrumentation. +/// +public class AspNetCoreInstrumentationOptions { + internal readonly HttpSemanticConvention HttpSemanticConvention; + /// - /// Options for requests instrumentation. + /// Initializes a new instance of the class. /// - public class AspNetCoreInstrumentationOptions + public AspNetCoreInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { - internal readonly HttpSemanticConvention HttpSemanticConvention; - - /// - /// Initializes a new instance of the class. - /// - public AspNetCoreInstrumentationOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) - { - } + } - internal AspNetCoreInstrumentationOptions(IConfiguration configuration) - { - Debug.Assert(configuration != null, "configuration was null"); + internal AspNetCoreInstrumentationOptions(IConfiguration configuration) + { + Debug.Assert(configuration != null, "configuration was null"); - this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); - } + this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); + } - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// Notes: - /// - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func Filter { get; set; } + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// Notes: + /// + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func Filter { get; set; } - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the HttpRequest object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpRequest { get; set; } + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpRequest object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpRequest { get; set; } - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the HttpResponse object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpResponse { get; set; } + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpResponse object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpResponse { get; set; } - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the Exception object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithException { get; set; } + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the Exception object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithException { get; set; } - /// - /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. - /// - /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. - /// - public bool RecordException { get; set; } + /// + /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. + /// + /// + /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. + /// + public bool RecordException { get; set; } #if NETSTANDARD2_1 || NET6_0_OR_GREATER - /// - /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. Default is true. - /// - /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md. - /// - public bool EnableGrpcAspNetCoreSupport { get; set; } = true; + /// + /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. Default is true. + /// + /// + /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md. + /// + public bool EnableGrpcAspNetCoreSupport { get; set; } = true; #endif - } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs index d1a91d589ac..aff8262ea67 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs @@ -19,44 +19,43 @@ using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreMetrics : IDisposable { - /// - /// Asp.Net Core Requests instrumentation. - /// - internal sealed class AspNetCoreMetrics : IDisposable + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + + private static readonly HashSet DiagnosticSourceEvents = new() + { + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + }; + + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly Meter meter; + + internal AspNetCoreMetrics(AspNetCoreMetricsInstrumentationOptions options) + { + Guard.ThrowIfNull(options); + this.meter = new Meter(InstrumentationName, InstrumentationVersion); + var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter, options); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() { - internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); - internal static readonly string InstrumentationName = AssemblyName.Name; - internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); - - private static readonly HashSet DiagnosticSourceEvents = new() - { - "Microsoft.AspNetCore.Hosting.HttpRequestIn", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", - }; - - private readonly Func isEnabled = (eventName, _, _) - => DiagnosticSourceEvents.Contains(eventName); - - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - private readonly Meter meter; - - internal AspNetCoreMetrics(AspNetCoreMetricsInstrumentationOptions options) - { - Guard.ThrowIfNull(options); - this.meter = new Meter(InstrumentationName, InstrumentationVersion); - var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter, options); - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); - } - - /// - public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - this.meter?.Dispose(); - } + this.diagnosticSourceSubscriber?.Dispose(); + this.meter?.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs index 4b2bbb0cb31..e61558d5b8b 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs @@ -19,61 +19,60 @@ using Microsoft.Extensions.Configuration; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Options for metrics requests instrumentation. +/// +public class AspNetCoreMetricsInstrumentationOptions { + internal readonly HttpSemanticConvention HttpSemanticConvention; + /// - /// Options for metrics requests instrumentation. + /// Initializes a new instance of the class. /// - public class AspNetCoreMetricsInstrumentationOptions + public AspNetCoreMetricsInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { - internal readonly HttpSemanticConvention HttpSemanticConvention; - - /// - /// Initializes a new instance of the class. - /// - public AspNetCoreMetricsInstrumentationOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) - { - } + } - internal AspNetCoreMetricsInstrumentationOptions(IConfiguration configuration) - { - Debug.Assert(configuration != null, "configuration was null"); + internal AspNetCoreMetricsInstrumentationOptions(IConfiguration configuration) + { + Debug.Assert(configuration != null, "configuration was null"); - this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); - } + this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); + } - /// - /// Delegate for enrichment of recorded metric with additional tags. - /// - /// The name of the metric being enriched. - /// : the HttpContext object. Both Request and Response are available. - /// : List of current tags. You can add additional tags to this list. - public delegate void AspNetCoreMetricEnrichmentFunc(string name, HttpContext context, ref TagList tags); + /// + /// Delegate for enrichment of recorded metric with additional tags. + /// + /// The name of the metric being enriched. + /// : the HttpContext object. Both Request and Response are available. + /// : List of current tags. You can add additional tags to this list. + public delegate void AspNetCoreMetricEnrichmentFunc(string name, HttpContext context, ref TagList tags); - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// Notes: - /// - /// The first parameter is the name of the metric being - /// filtered. - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func Filter { get; set; } + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// Notes: + /// + /// The first parameter is the name of the metric being + /// filtered. + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func Filter { get; set; } - /// - /// Gets or sets an function to enrich a recorded metric with additional custom tags. - /// - public AspNetCoreMetricEnrichmentFunc Enrich { get; set; } - } + /// + /// Gets or sets an function to enrich a recorded metric with additional custom tags. + /// + public AspNetCoreMetricEnrichmentFunc Enrich { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs index 9b52e6bd6c7..2cf1ea5594f 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs @@ -18,58 +18,57 @@ using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-AspNetCore")] +internal sealed class AspNetCoreInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-AspNetCore")] - internal sealed class AspNetCoreInstrumentationEventSource : EventSource - { - public static AspNetCoreInstrumentationEventSource Log = new(); + public static AspNetCoreInstrumentationEventSource Log = new(); - [NonEvent] - public void RequestFilterException(string handlerName, string eventName, string operationName, Exception ex) + [NonEvent] + public void RequestFilterException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.RequestFilterException(handlerName, eventName, operationName, ex.ToInvariantString()); - } + this.RequestFilterException(handlerName, eventName, operationName, ex.ToInvariantString()); } + } - [NonEvent] - public void EnrichmentException(string handlerName, string eventName, string operationName, Exception ex) + [NonEvent] + public void EnrichmentException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(handlerName, eventName, operationName, ex.ToInvariantString()); - } + this.EnrichmentException(handlerName, eventName, operationName, ex.ToInvariantString()); } + } - [Event(1, Message = "Payload is NULL, span will not be recorded. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName, string operationName) - { - this.WriteEvent(1, handlerName, eventName, operationName); - } + [Event(1, Message = "Payload is NULL, span will not be recorded. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName, string operationName) + { + this.WriteEvent(1, handlerName, eventName, operationName); + } - [Event(2, Message = "Request is filtered out. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Verbose)] - public void RequestIsFilteredOut(string handlerName, string eventName, string operationName) - { - this.WriteEvent(2, handlerName, eventName, operationName); - } + [Event(2, Message = "Request is filtered out. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut(string handlerName, string eventName, string operationName) + { + this.WriteEvent(2, handlerName, eventName, operationName); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(3, Message = "Filter threw exception, request will not be collected. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Error)] - public void RequestFilterException(string handlerName, string eventName, string operationName, string exception) - { - this.WriteEvent(3, handlerName, eventName, operationName, exception); - } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] + [Event(3, Message = "Filter threw exception, request will not be collected. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Error)] + public void RequestFilterException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(3, handlerName, eventName, operationName, exception); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(4, Message = "Enrich threw exception. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Warning)] - public void EnrichmentException(string handlerName, string eventName, string operationName, string exception) - { - this.WriteEvent(4, handlerName, eventName, operationName, exception); - } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] + [Event(4, Message = "Enrich threw exception. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Warning)] + public void EnrichmentException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(4, handlerName, eventName, operationName, exception); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 241bbf0e35f..5e7e5c01e0e 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -31,512 +31,511 @@ using OpenTelemetry.Trace; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal class HttpInListener : ListenerHandler { - internal class HttpInListener : ListenerHandler - { - internal const string ActivityOperationName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; - internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; - internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; - internal const string OnMvcBeforeActionEvent = "Microsoft.AspNetCore.Mvc.BeforeAction"; - internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; - internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; + internal const string ActivityOperationName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; + internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; + internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; + internal const string OnMvcBeforeActionEvent = "Microsoft.AspNetCore.Mvc.BeforeAction"; + internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; #if NET7_0_OR_GREATER - // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 - internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; + // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 + internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; #endif - internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); - private const string DiagnosticSourceName = "Microsoft.AspNetCore"; - private const string UnknownHostName = "UNKNOWN-HOST"; + private const string DiagnosticSourceName = "Microsoft.AspNetCore"; + private const string UnknownHostName = "UNKNOWN-HOST"; - private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers[name]; + private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers[name]; #if !NET6_0_OR_GREATER - private readonly PropertyFetcher beforeActionActionDescriptorFetcher = new("actionDescriptor"); - private readonly PropertyFetcher beforeActionAttributeRouteInfoFetcher = new("AttributeRouteInfo"); - private readonly PropertyFetcher beforeActionTemplateFetcher = new("Template"); + private readonly PropertyFetcher beforeActionActionDescriptorFetcher = new("actionDescriptor"); + private readonly PropertyFetcher beforeActionAttributeRouteInfoFetcher = new("AttributeRouteInfo"); + private readonly PropertyFetcher beforeActionTemplateFetcher = new("Template"); #endif - private readonly PropertyFetcher stopExceptionFetcher = new("Exception"); - private readonly AspNetCoreInstrumentationOptions options; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; + private readonly PropertyFetcher stopExceptionFetcher = new("Exception"); + private readonly AspNetCoreInstrumentationOptions options; + private readonly bool emitOldAttributes; + private readonly bool emitNewAttributes; - public HttpInListener(AspNetCoreInstrumentationOptions options) - : base(DiagnosticSourceName) - { - Guard.ThrowIfNull(options); + public HttpInListener(AspNetCoreInstrumentationOptions options) + : base(DiagnosticSourceName) + { + Guard.ThrowIfNull(options); - this.options = options; + this.options = options; - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); + this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); - } + this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); + } - public override void OnEventWritten(string name, object payload) + public override void OnEventWritten(string name, object payload) + { + switch (name) { - switch (name) - { - case OnStartEvent: - { - this.OnStartActivity(Activity.Current, payload); - } + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } - break; - case OnStopEvent: - { - this.OnStopActivity(Activity.Current, payload); - } + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } - break; - case OnMvcBeforeActionEvent: - { - this.OnMvcBeforeAction(Activity.Current, payload); - } + break; + case OnMvcBeforeActionEvent: + { + this.OnMvcBeforeAction(Activity.Current, payload); + } - break; - case OnUnhandledHostingExceptionEvent: - case OnUnHandledDiagnosticsExceptionEvent: - { - this.OnException(Activity.Current, payload); - } + break; + case OnUnhandledHostingExceptionEvent: + case OnUnHandledDiagnosticsExceptionEvent: + { + this.OnException(Activity.Current, payload); + } - break; - } + break; } + } - public void OnStartActivity(Activity activity, object payload) - { - // The overall flow of what AspNetCore library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what AspNetCore library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. - if (Sdk.SuppressInstrumentation) - { - return; - } + if (Sdk.SuppressInstrumentation) + { + return; + } - HttpContext context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); - return; - } + HttpContext context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); + return; + } - // Ensure context extraction irrespective of sampling decision - var request = context.Request; - var textMapPropagator = Propagators.DefaultTextMapPropagator; - if (textMapPropagator is not TraceContextPropagator) - { - var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); + // Ensure context extraction irrespective of sampling decision + var request = context.Request; + var textMapPropagator = Propagators.DefaultTextMapPropagator; + if (textMapPropagator is not TraceContextPropagator) + { + var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); - if (ctx.ActivityContext.IsValid() - && ctx.ActivityContext != new ActivityContext(activity.TraceId, activity.ParentSpanId, activity.ActivityTraceFlags, activity.TraceStateString, true)) - { - // Create a new activity with its parent set from the extracted context. - // This makes the new activity as a "sibling" of the activity created by - // Asp.Net Core. + if (ctx.ActivityContext.IsValid() + && ctx.ActivityContext != new ActivityContext(activity.TraceId, activity.ParentSpanId, activity.ActivityTraceFlags, activity.TraceStateString, true)) + { + // Create a new activity with its parent set from the extracted context. + // This makes the new activity as a "sibling" of the activity created by + // Asp.Net Core. #if NET7_0_OR_GREATER - // For NET7.0 onwards activity is created using ActivitySource so, - // we will use the source of the activity to create the new one. - Activity newOne = activity.Source.CreateActivity(ActivityOperationName, ActivityKind.Server, ctx.ActivityContext); + // For NET7.0 onwards activity is created using ActivitySource so, + // we will use the source of the activity to create the new one. + Activity newOne = activity.Source.CreateActivity(ActivityOperationName, ActivityKind.Server, ctx.ActivityContext); #else - Activity newOne = new Activity(ActivityOperationName); - newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); + Activity newOne = new Activity(ActivityOperationName); + newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); #endif - newOne.TraceStateString = ctx.ActivityContext.TraceState; - - newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString); + newOne.TraceStateString = ctx.ActivityContext.TraceState; - // Starting the new activity make it the Activity.Current one. - newOne.Start(); + newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString); - // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity - activity.IsAllDataRequested = false; - activity = newOne; - } + // Starting the new activity make it the Activity.Current one. + newOne.Start(); - Baggage.Current = ctx.Baggage; + // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity + activity.IsAllDataRequested = false; + activity = newOne; } - // enrich Activity from payload only if sampling decision - // is favorable. - if (activity.IsAllDataRequested) + Baggage.Current = ctx.Baggage; + } + + // enrich Activity from payload only if sampling decision + // is favorable. + if (activity.IsAllDataRequested) + { + try { - try - { - if (this.options.Filter?.Invoke(context) == false) - { - AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - return; - } - } - catch (Exception ex) + if (this.options.Filter?.Invoke(context) == false) { - AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); activity.IsAllDataRequested = false; activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; return; } + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } #if !NET7_0_OR_GREATER - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); - ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); #endif - var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - activity.DisplayName = path; + var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + activity.DisplayName = path; - // 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/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md + if (this.emitOldAttributes) + { + if (request.Host.HasValue) { - if (request.Host.HasValue) - { - 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)); + activity.SetTag(SemanticConventions.AttributeNetHostName, request.Host.Host); - if (request.Headers.TryGetValue("User-Agent", out var values)) + if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); - } + activity.SetTag(SemanticConventions.AttributeNetHostPort, 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.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.AttributeServerPort, 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.QueryString.HasValue) + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) { - // QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571 - activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); + activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); } + } + } - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, request.Method); - activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); - activity.SetTag(SemanticConventions.AttributeUrlPath, path); - activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + if (this.emitNewAttributes) + { + if (request.Host.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); - if (request.Headers.TryGetValue("User-Agent", out var values)) + if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); - } + activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port); } } - try + if (request.QueryString.HasValue) { - this.options.EnrichWithHttpRequest?.Invoke(activity, request); + // QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571 + activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); } - catch (Exception ex) + + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, 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)) { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) + { + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); + } } } + + try + { + this.options.EnrichWithHttpRequest?.Invoke(activity, request); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + } } + } - public void OnStopActivity(Activity activity, object payload) + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + HttpContext context = payload as HttpContext; + if (context == null) { - HttpContext context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); - return; - } + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); + return; + } - var response = context.Response; + var response = context.Response; - if (this.emitOldAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + if (this.emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + } - if (this.emitNewAttributes) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + if (this.emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + } #if !NETSTANDARD2_0 - if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) - { - this.AddGrpcAttributes(activity, grpcMethod, context); - } - else if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); - } + if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) + { + this.AddGrpcAttributes(activity, grpcMethod, context); + } + else if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + } #else - if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); - } + if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + } #endif - try - { - this.options.EnrichWithHttpResponse?.Invoke(activity, response); - } - catch (Exception ex) - { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex); - } + try + { + this.options.EnrichWithHttpResponse?.Invoke(activity, response); } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex); + } + } #if NET7_0_OR_GREATER - var tagValue = activity.GetTagValue("IsCreatedByInstrumentation"); - if (ReferenceEquals(tagValue, bool.TrueString)) + var tagValue = activity.GetTagValue("IsCreatedByInstrumentation"); + if (ReferenceEquals(tagValue, bool.TrueString)) #else - if (activity.TryCheckFirstTag("IsCreatedByInstrumentation", out var tagValue) && ReferenceEquals(tagValue, bool.TrueString)) + if (activity.TryCheckFirstTag("IsCreatedByInstrumentation", out var tagValue) && ReferenceEquals(tagValue, bool.TrueString)) #endif - { - // If instrumentation started a new Activity, it must - // be stopped here. - activity.SetTag("IsCreatedByInstrumentation", null); - activity.Stop(); - - // After the activity.Stop() code, Activity.Current becomes null. - // If Asp.Net Core uses Activity.Current?.Stop() - it'll not stop the activity - // it created. - // Currently Asp.Net core does not use Activity.Current, instead it stores a - // reference to its activity, and calls .Stop on it. - - // TODO: Should we still restore Activity.Current here? - // If yes, then we need to store the asp.net core activity inside - // the one created by the instrumentation. - // And retrieve it here, and set it to Current. - } + { + // If instrumentation started a new Activity, it must + // be stopped here. + activity.SetTag("IsCreatedByInstrumentation", null); + activity.Stop(); + + // After the activity.Stop() code, Activity.Current becomes null. + // If Asp.Net Core uses Activity.Current?.Stop() - it'll not stop the activity + // it created. + // Currently Asp.Net core does not use Activity.Current, instead it stores a + // reference to its activity, and calls .Stop on it. + + // TODO: Should we still restore Activity.Current here? + // If yes, then we need to store the asp.net core activity inside + // the one created by the instrumentation. + // And retrieve it here, and set it to Current. } + } - public void OnMvcBeforeAction(Activity activity, object payload) + public void OnMvcBeforeAction(Activity activity, object payload) + { + // We cannot rely on Activity.Current here + // There could be activities started by middleware + // after activity started by framework resulting in different Activity.Current. + // so, we need to first find the activity started by Asp.Net Core. + // For .net6.0 onwards we could use IHttpActivityFeature to get the activity created by framework + // var httpActivityFeature = context.Features.Get(); + // activity = httpActivityFeature.Activity; + // However, this will not work as in case of custom propagator + // we start a new activity during onStart event which is a sibling to the activity created by framework + // So, in that case we need to get the activity created by us here. + // we can do so only by looping through activity.Parent chain. + while (activity != null) { - // We cannot rely on Activity.Current here - // There could be activities started by middleware - // after activity started by framework resulting in different Activity.Current. - // so, we need to first find the activity started by Asp.Net Core. - // For .net6.0 onwards we could use IHttpActivityFeature to get the activity created by framework - // var httpActivityFeature = context.Features.Get(); - // activity = httpActivityFeature.Activity; - // However, this will not work as in case of custom propagator - // we start a new activity during onStart event which is a sibling to the activity created by framework - // So, in that case we need to get the activity created by us here. - // we can do so only by looping through activity.Parent chain. - while (activity != null) + if (string.Equals(activity.OperationName, ActivityOperationName, StringComparison.Ordinal)) { - if (string.Equals(activity.OperationName, ActivityOperationName, StringComparison.Ordinal)) - { - break; - } - - activity = activity.Parent; + break; } - if (activity == null) - { - return; - } + activity = activity.Parent; + } - if (activity.IsAllDataRequested) - { + if (activity == null) + { + return; + } + + if (activity.IsAllDataRequested) + { #if !NET6_0_OR_GREATER - _ = this.beforeActionActionDescriptorFetcher.TryFetch(payload, out var actionDescriptor); - _ = this.beforeActionAttributeRouteInfoFetcher.TryFetch(actionDescriptor, out var attributeRouteInfo); - _ = this.beforeActionTemplateFetcher.TryFetch(attributeRouteInfo, out var template); + _ = this.beforeActionActionDescriptorFetcher.TryFetch(payload, out var actionDescriptor); + _ = this.beforeActionAttributeRouteInfoFetcher.TryFetch(actionDescriptor, out var attributeRouteInfo); + _ = this.beforeActionTemplateFetcher.TryFetch(attributeRouteInfo, out var template); #else - var beforeActionEventData = payload as BeforeActionEventData; - var template = beforeActionEventData.ActionDescriptor?.AttributeRouteInfo?.Template; + var beforeActionEventData = payload as BeforeActionEventData; + var template = beforeActionEventData.ActionDescriptor?.AttributeRouteInfo?.Template; #endif - if (!string.IsNullOrEmpty(template)) - { - // override the span name that was previously set to the path part of URL. - activity.DisplayName = template; - activity.SetTag(SemanticConventions.AttributeHttpRoute, template); - } - - // TODO: Should we get values from RouteData? - // private readonly PropertyFetcher beforeActionRouteDataFetcher = new PropertyFetcher("routeData"); - // var routeData = this.beforeActionRouteDataFetcher.Fetch(payload) as RouteData; + if (!string.IsNullOrEmpty(template)) + { + // override the span name that was previously set to the path part of URL. + activity.DisplayName = template; + activity.SetTag(SemanticConventions.AttributeHttpRoute, template); } + + // TODO: Should we get values from RouteData? + // private readonly PropertyFetcher beforeActionRouteDataFetcher = new PropertyFetcher("routeData"); + // var routeData = this.beforeActionRouteDataFetcher.Fetch(payload) as RouteData; } + } - public void OnException(Activity activity, object payload) + public void OnException(Activity activity, object payload) + { + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + // We need to use reflection here as the payload type is not a defined public type. + if (!this.stopExceptionFetcher.TryFetch(payload, out Exception exc) || exc == null) { - // We need to use reflection here as the payload type is not a defined public type. - if (!this.stopExceptionFetcher.TryFetch(payload, out Exception exc) || exc == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnException), activity.OperationName); - return; - } + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnException), activity.OperationName); + return; + } - if (this.options.RecordException) - { - activity.RecordException(exc); - } + if (this.options.RecordException) + { + activity.RecordException(exc); + } - activity.SetStatus(ActivityStatusCode.Error, exc.Message); + activity.SetStatus(ActivityStatusCode.Error, exc.Message); - try - { - this.options.EnrichWithException?.Invoke(activity, exc); - } - catch (Exception ex) - { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex); - } + try + { + this.options.EnrichWithException?.Invoke(activity, exc); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex); } } + } - private static string GetUri(HttpRequest request) - { - // this follows the suggestions from https://github.com/dotnet/aspnetcore/issues/28906 - var scheme = request.Scheme ?? string.Empty; - - // HTTP 1.0 request with NO host header would result in empty Host. - // Use placeholder to avoid incorrect URL like "http:///" - var host = request.Host.Value ?? UnknownHostName; - var pathBase = request.PathBase.Value ?? string.Empty; - var path = request.Path.Value ?? string.Empty; - var queryString = request.QueryString.Value ?? string.Empty; - var length = scheme.Length + Uri.SchemeDelimiter.Length + host.Length + pathBase.Length - + path.Length + queryString.Length; + private static string GetUri(HttpRequest request) + { + // this follows the suggestions from https://github.com/dotnet/aspnetcore/issues/28906 + var scheme = request.Scheme ?? string.Empty; + + // HTTP 1.0 request with NO host header would result in empty Host. + // Use placeholder to avoid incorrect URL like "http:///" + var host = request.Host.Value ?? UnknownHostName; + var pathBase = request.PathBase.Value ?? string.Empty; + var path = request.Path.Value ?? string.Empty; + var queryString = request.QueryString.Value ?? string.Empty; + var length = scheme.Length + Uri.SchemeDelimiter.Length + host.Length + pathBase.Length + + path.Length + queryString.Length; #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - return string.Create(length, (scheme, host, pathBase, path, queryString), (span, parts) => + return string.Create(length, (scheme, host, pathBase, path, queryString), (span, parts) => + { + CopyTo(ref span, parts.scheme); + CopyTo(ref span, Uri.SchemeDelimiter); + CopyTo(ref span, parts.host); + CopyTo(ref span, parts.pathBase); + CopyTo(ref span, parts.path); + CopyTo(ref span, parts.queryString); + + static void CopyTo(ref Span buffer, ReadOnlySpan text) { - CopyTo(ref span, parts.scheme); - CopyTo(ref span, Uri.SchemeDelimiter); - CopyTo(ref span, parts.host); - CopyTo(ref span, parts.pathBase); - CopyTo(ref span, parts.path); - CopyTo(ref span, parts.queryString); - - static void CopyTo(ref Span buffer, ReadOnlySpan text) + if (!text.IsEmpty) { - if (!text.IsEmpty) - { - text.CopyTo(buffer); - buffer = buffer.Slice(text.Length); - } + text.CopyTo(buffer); + buffer = buffer.Slice(text.Length); } - }); + } + }); #else - return new System.Text.StringBuilder(length) - .Append(scheme) - .Append(Uri.SchemeDelimiter) - .Append(host) - .Append(pathBase) - .Append(path) - .Append(queryString) - .ToString(); + return new System.Text.StringBuilder(length) + .Append(scheme) + .Append(Uri.SchemeDelimiter) + .Append(host) + .Append(pathBase) + .Append(path) + .Append(queryString) + .ToString(); #endif - } + } #if !NETSTANDARD2_0 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) - { - grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - return !string.IsNullOrEmpty(grpcMethod); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) + { + grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + return !string.IsNullOrEmpty(grpcMethod); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) - { - // The RPC semantic conventions indicate the span name - // should not have a leading forward slash. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#span-name - activity.DisplayName = grpcMethod.TrimStart('/'); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) + { + // The RPC semantic conventions indicate the span name + // should not have a leading forward slash. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#span-name + activity.DisplayName = grpcMethod.TrimStart('/'); - activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); - if (this.emitOldAttributes) + if (this.emitOldAttributes) + { + if (context.Connection.RemoteIpAddress != null) { - 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()); - } - - activity.SetTag(SemanticConventions.AttributeNetPeerPort, context.Connection.RemotePort); + // 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.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.AttributeNetPeerPort, context.Connection.RemotePort); + } - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - if (validConversion) + // 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.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); } - if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) - { - activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); - activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); + } - // Remove the grpc.method tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + if (validConversion) + { + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + } - // Remove the grpc.status_code tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + { + activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); - if (validConversion) - { - // setting rpc.grpc.status_code - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); - } + // Remove the grpc.method tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + + // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + + if (validConversion) + { + // setting rpc.grpc.status_code + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); } } -#endif } +#endif } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index e9522c1c06d..ce06402bc34 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -23,129 +23,128 @@ using OpenTelemetry.Trace; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal sealed class HttpInMetricsListener : ListenerHandler { - internal sealed class HttpInMetricsListener : ListenerHandler + private const string HttpServerDurationMetricName = "http.server.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 httpServerDuration; + private readonly bool emitOldAttributes; + private readonly bool emitNewAttributes; + + internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstrumentationOptions options) + : base(name) { - private const string HttpServerDurationMetricName = "http.server.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 httpServerDuration; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; - - internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstrumentationOptions options) - : base(name) - { - this.meter = meter; - this.options = options; - this.httpServerDuration = meter.CreateHistogram(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests."); + this.meter = meter; + this.options = options; + this.httpServerDuration = meter.CreateHistogram(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests."); - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); + this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); - } + this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); + } - public override void OnEventWritten(string name, object payload) + public override void OnEventWritten(string name, object payload) + { + if (name == OnStopEvent) { - if (name == OnStopEvent) + var context = payload as HttpContext; + if (context == null) { - var context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); - return; - } + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); + return; + } - try - { - if (this.options.Filter?.Invoke(HttpServerDurationMetricName, context) == false) - { - AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); - return; - } - } - catch (Exception ex) + 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; - } + // 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; - 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) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, context.Request.Scheme)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, context.Request.Method)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.emitOldAttributes) + if (context.Request.Host.HasValue) { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, context.Request.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, context.Request.Method)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetHostName, context.Request.Host.Host)); - if (context.Request.Host.HasValue) + if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443) { - tags.Add(new KeyValuePair(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(SemanticConventions.AttributeNetHostPort, context.Request.Host.Port)); - } + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetHostPort, context.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) + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + if (this.emitNewAttributes) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); + + if (context.Request.Host.HasValue) { - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, context.Request.Host.Host)); - if (context.Request.Host.HasValue) + if (context.Request.Host.Port is not null && context.Request.Host.Port != 80 && context.Request.Host.Port != 443) { - tags.Add(new KeyValuePair(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(SemanticConventions.AttributeServerPort, context.Request.Host.Port)); - } + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, context.Request.Host.Port)); } } + } #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(SemanticConventions.AttributeHttpRoute, route)); + } +#endif + if (this.options.Enrich != null) + { + try { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRoute, route)); + this.options.Enrich(HttpServerDurationMetricName, 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, HttpServerDurationMetricName, 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.httpServerDuration.Record(Activity.Current.Duration.TotalMilliseconds, tags); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs index 73114efe0b3..239f0623242 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs @@ -14,34 +14,33 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// A collection of helper methods to be used when building Http activities. +/// +internal static class HttpTagHelper { /// - /// A collection of helper methods to be used when building Http activities. + /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. /// - internal static class HttpTagHelper + /// . + /// Span flavor value. + public static string GetFlavorTagValueFromProtocol(string protocol) { - /// - /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. - /// - /// . - /// Span flavor value. - public static string GetFlavorTagValueFromProtocol(string protocol) + switch (protocol) { - switch (protocol) - { - case "HTTP/2": - return "2.0"; + case "HTTP/2": + return "2.0"; - case "HTTP/3": - return "3.0"; + case "HTTP/3": + return "3.0"; - case "HTTP/1.1": - return "1.1"; + case "HTTP/1.1": + return "1.1"; - default: - return protocol; - } + default: + return protocol; } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs index 59d65896635..99330ac220a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs @@ -20,76 +20,75 @@ using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class MeterProviderBuilderExtensions { /// - /// Extension methods to simplify registering of ASP.NET Core request instrumentation. + /// Enables the incoming requests automatic data collection for ASP.NET Core. /// - public static class MeterProviderBuilderExtensions - { - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder, - Action configureAspNetCoreInstrumentationOptions) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder, + Action configureAspNetCoreInstrumentationOptions) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder, - string name, - Action configureAspNetCoreInstrumentationOptions) - { - Guard.ThrowIfNull(builder); + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder, + string name, + Action configureAspNetCoreInstrumentationOptions) + { + Guard.ThrowIfNull(builder); - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; + // Note: Warm-up the status code mapping. + _ = TelemetryHelper.BoxedStatusCodes; - name ??= Options.DefaultName; + name ??= Options.DefaultName; - builder.ConfigureServices(services => + builder.ConfigureServices(services => + { + if (configureAspNetCoreInstrumentationOptions != null) { - if (configureAspNetCoreInstrumentationOptions != null) - { - services.Configure(name, configureAspNetCoreInstrumentationOptions); - } + services.Configure(name, configureAspNetCoreInstrumentationOptions); + } - services.RegisterOptionsFactory(configuration => new AspNetCoreMetricsInstrumentationOptions(configuration)); - }); + services.RegisterOptionsFactory(configuration => new AspNetCoreMetricsInstrumentationOptions(configuration)); + }); - builder.AddMeter(AspNetCoreMetrics.InstrumentationName); + builder.AddMeter(AspNetCoreMetrics.InstrumentationName); - builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); + builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); - // 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 + // 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); - }); + return new AspNetCoreMetrics(options); + }); - return builder; - } + return builder; } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs index b8872d5912d..dae87bf4991 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs @@ -23,113 +23,112 @@ using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class TracerProviderBuilderExtensions { /// - /// Extension methods to simplify registering of ASP.NET Core request instrumentation. + /// Enables the incoming requests automatic data collection for ASP.NET Core. /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation(this TracerProviderBuilder builder) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - Action configureAspNetCoreInstrumentationOptions) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configureAspNetCoreInstrumentationOptions) - { - Guard.ThrowIfNull(builder); + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation(this TracerProviderBuilder builder) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + Action configureAspNetCoreInstrumentationOptions) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); - name ??= Options.DefaultName; + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureAspNetCoreInstrumentationOptions) + { + Guard.ThrowIfNull(builder); - builder.ConfigureServices(services => - { - if (configureAspNetCoreInstrumentationOptions != null) - { - services.Configure(name, configureAspNetCoreInstrumentationOptions); - } + // Note: Warm-up the status code mapping. + _ = TelemetryHelper.BoxedStatusCodes; - services.RegisterOptionsFactory(configuration => new AspNetCoreInstrumentationOptions(configuration)); - }); + name ??= Options.DefaultName; - if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + builder.ConfigureServices(services => + { + if (configureAspNetCoreInstrumentationOptions != null) { - deferredTracerProviderBuilder.Configure((sp, builder) => - { - AddAspNetCoreInstrumentationSources(builder, sp); - }); + services.Configure(name, configureAspNetCoreInstrumentationOptions); } - return builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); + services.RegisterOptionsFactory(configuration => new AspNetCoreInstrumentationOptions(configuration)); + }); - return new AspNetCoreInstrumentation( - new HttpInListener(options)); + if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + { + deferredTracerProviderBuilder.Configure((sp, builder) => + { + AddAspNetCoreInstrumentationSources(builder, sp); }); } - // Note: This is used by unit tests. - internal static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - HttpInListener listener) + return builder.AddInstrumentation(sp => { - builder.AddAspNetCoreInstrumentationSources(); + var options = sp.GetRequiredService>().Get(name); - return builder.AddInstrumentation( - new AspNetCoreInstrumentation(listener)); - } + return new AspNetCoreInstrumentation( + new HttpInListener(options)); + }); + } - private static void AddAspNetCoreInstrumentationSources( - this TracerProviderBuilder builder, - IServiceProvider serviceProvider = null) - { - // For .NET7.0 onwards activity will be created using activitySource. - // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 - // For .NET6.0 and below, we will continue to use legacy way. + // Note: This is used by unit tests. + internal static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + HttpInListener listener) + { + builder.AddAspNetCoreInstrumentationSources(); + + return builder.AddInstrumentation( + new AspNetCoreInstrumentation(listener)); + } + + private static void AddAspNetCoreInstrumentationSources( + this TracerProviderBuilder builder, + IServiceProvider serviceProvider = null) + { + // For .NET7.0 onwards activity will be created using activitySource. + // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 + // For .NET6.0 and below, we will continue to use legacy way. #if NET7_0_OR_GREATER - // TODO: Check with .NET team to see if this can be prevented - // as this allows user to override the ActivitySource. - var activitySourceService = serviceProvider?.GetService(); - if (activitySourceService != null) - { - builder.AddSource(activitySourceService.Name); - } - else - { - // For users not using hosting package? - builder.AddSource(HttpInListener.AspNetCoreActivitySourceName); - } + // TODO: Check with .NET team to see if this can be prevented + // as this allows user to override the ActivitySource. + var activitySourceService = serviceProvider?.GetService(); + if (activitySourceService != null) + { + builder.AddSource(activitySourceService.Name); + } + else + { + // For users not using hosting package? + builder.AddSource(HttpInListener.AspNetCoreActivitySourceName); + } #else - builder.AddSource(HttpInListener.ActivitySourceName); - builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore + builder.AddSource(HttpInListener.ActivitySourceName); + builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore #endif - } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs index 1e970678603..8fad42cf09f 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs @@ -15,29 +15,28 @@ // using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// GrpcClient instrumentation. +/// +internal sealed class GrpcClientInstrumentation : IDisposable { + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + /// - /// GrpcClient instrumentation. + /// Initializes a new instance of the class. /// - internal sealed class GrpcClientInstrumentation : IDisposable + /// Configuration options for Grpc client instrumentation. + public GrpcClientInstrumentation(GrpcClientInstrumentationOptions options = null) { - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for Grpc client instrumentation. - public GrpcClientInstrumentation(GrpcClientInstrumentationOptions options = null) - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new GrpcClientDiagnosticListener(options), null); - this.diagnosticSourceSubscriber.Subscribe(); - } + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new GrpcClientDiagnosticListener(options), null); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs index 4ea44b69e59..5fa720d89b8 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs @@ -18,51 +18,50 @@ using Microsoft.Extensions.Configuration; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Options for GrpcClient instrumentation. +/// +public class GrpcClientInstrumentationOptions { + internal readonly HttpSemanticConvention HttpSemanticConvention; + /// - /// Options for GrpcClient instrumentation. + /// Initializes a new instance of the class. /// - public class GrpcClientInstrumentationOptions + public GrpcClientInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { - internal readonly HttpSemanticConvention HttpSemanticConvention; - - /// - /// Initializes a new instance of the class. - /// - public GrpcClientInstrumentationOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) - { - } + } - internal GrpcClientInstrumentationOptions(IConfiguration configuration) - { - Debug.Assert(configuration != null, "configuration was null"); + internal GrpcClientInstrumentationOptions(IConfiguration configuration) + { + Debug.Assert(configuration != null, "configuration was null"); - this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); - } + this.HttpSemanticConvention = GetSemanticConventionOptIn(configuration); + } - /// - /// Gets or sets a value indicating whether down stream instrumentation is suppressed (disabled). - /// - public bool SuppressDownstreamInstrumentation { get; set; } + /// + /// Gets or sets a value indicating whether down stream instrumentation is suppressed (disabled). + /// + public bool SuppressDownstreamInstrumentation { get; set; } - /// - /// Gets or sets an action to enrich the Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpRequestMessage { get; set; } + /// + /// Gets or sets an action to enrich the Activity with . + /// + /// + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpRequestMessage { get; set; } - /// - /// Gets or sets an action to enrich an Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpResponseMessage { get; set; } - } + /// + /// Gets or sets an action to enrich an Activity with . + /// + /// + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpResponseMessage { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs index 8933480b157..14f5d7b6164 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs @@ -18,73 +18,72 @@ using System.Text.RegularExpressions; using OpenTelemetry.Trace; -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +internal static class GrpcTagHelper { - internal static class GrpcTagHelper - { - public const string RpcSystemGrpc = "grpc"; + public const string RpcSystemGrpc = "grpc"; + + // The Grpc.Net.Client library adds its own tags to the activity. + // These tags are used to source the tags added by the OpenTelemetry instrumentation. + public const string GrpcMethodTagName = "grpc.method"; + public const string GrpcStatusCodeTagName = "grpc.status_code"; - // The Grpc.Net.Client library adds its own tags to the activity. - // These tags are used to source the tags added by the OpenTelemetry instrumentation. - public const string GrpcMethodTagName = "grpc.method"; - public const string GrpcStatusCodeTagName = "grpc.status_code"; + private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); - private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); + public static string GetGrpcMethodFromActivity(Activity activity) + { + return activity.GetTagValue(GrpcMethodTagName) as string; + } - public static string GetGrpcMethodFromActivity(Activity activity) + public static bool TryGetGrpcStatusCodeFromActivity(Activity activity, out int statusCode) + { + statusCode = -1; + var grpcStatusCodeTag = activity.GetTagValue(GrpcStatusCodeTagName); + if (grpcStatusCodeTag == null) { - return activity.GetTagValue(GrpcMethodTagName) as string; + return false; } - public static bool TryGetGrpcStatusCodeFromActivity(Activity activity, out int statusCode) - { - statusCode = -1; - var grpcStatusCodeTag = activity.GetTagValue(GrpcStatusCodeTagName); - if (grpcStatusCodeTag == null) - { - return false; - } + return int.TryParse(grpcStatusCodeTag as string, out statusCode); + } - return int.TryParse(grpcStatusCodeTag as string, out statusCode); + public static bool TryParseRpcServiceAndRpcMethod(string grpcMethod, out string rpcService, out string rpcMethod) + { + var match = GrpcMethodRegex.Match(grpcMethod); + if (match.Success) + { + rpcService = match.Groups["service"].Value; + rpcMethod = match.Groups["method"].Value; + return true; } - - public static bool TryParseRpcServiceAndRpcMethod(string grpcMethod, out string rpcService, out string rpcMethod) + else { - var match = GrpcMethodRegex.Match(grpcMethod); - if (match.Success) - { - rpcService = match.Groups["service"].Value; - rpcMethod = match.Groups["method"].Value; - return true; - } - else - { - rpcService = string.Empty; - rpcMethod = string.Empty; - return false; - } + rpcService = string.Empty; + rpcMethod = string.Empty; + return false; } + } - /// - /// Helper method that populates span properties from RPC status code according - /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#status. - /// - /// RPC status code. - /// Resolved span for the Grpc status code. - public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCode(int statusCode) - { - var status = ActivityStatusCode.Error; + /// + /// Helper method that populates span properties from RPC status code according + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#status. + /// + /// RPC status code. + /// Resolved span for the Grpc status code. + public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCode(int statusCode) + { + var status = ActivityStatusCode.Error; - if (typeof(StatusCanonicalCode).IsEnumDefined(statusCode)) + if (typeof(StatusCanonicalCode).IsEnumDefined(statusCode)) + { + status = ((StatusCanonicalCode)statusCode) switch { - status = ((StatusCanonicalCode)statusCode) switch - { - StatusCanonicalCode.Ok => ActivityStatusCode.Unset, - _ => ActivityStatusCode.Error, - }; - } - - return status; + StatusCanonicalCode.Ok => ActivityStatusCode.Unset, + _ => ActivityStatusCode.Error, + }; } + + return status; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs index e34133dfed8..b5626c29b08 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs @@ -21,196 +21,195 @@ using OpenTelemetry.Trace; using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +internal sealed class GrpcClientDiagnosticListener : ListenerHandler { - internal sealed class GrpcClientDiagnosticListener : ListenerHandler + internal static readonly AssemblyName AssemblyName = typeof(GrpcClientDiagnosticListener).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + + private const string OnStartEvent = "Grpc.Net.Client.GrpcOut.Start"; + private const string OnStopEvent = "Grpc.Net.Client.GrpcOut.Stop"; + + private readonly GrpcClientInstrumentationOptions options; + private readonly bool emitOldAttributes; + private readonly bool emitNewAttributes; + private readonly PropertyFetcher startRequestFetcher = new("Request"); + private readonly PropertyFetcher stopRequestFetcher = new("Response"); + + public GrpcClientDiagnosticListener(GrpcClientInstrumentationOptions options) + : base("Grpc.Net.Client") + { + 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) { - internal static readonly AssemblyName AssemblyName = typeof(GrpcClientDiagnosticListener).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); - - private const string OnStartEvent = "Grpc.Net.Client.GrpcOut.Start"; - private const string OnStopEvent = "Grpc.Net.Client.GrpcOut.Stop"; - - private readonly GrpcClientInstrumentationOptions options; - private readonly bool emitOldAttributes; - private readonly bool emitNewAttributes; - private readonly PropertyFetcher startRequestFetcher = new("Request"); - private readonly PropertyFetcher stopRequestFetcher = new("Response"); - - public GrpcClientDiagnosticListener(GrpcClientInstrumentationOptions options) - : base("Grpc.Net.Client") + switch (name) { - this.options = options; + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } - this.emitOldAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.Old); + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } - this.emitNewAttributes = this.options.HttpSemanticConvention.HasFlag(HttpSemanticConvention.New); + break; } + } - public override void OnEventWritten(string name, object payload) + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what GrpcClient library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() + + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. + + if (Sdk.SuppressInstrumentation) { - switch (name) - { - case OnStartEvent: - { - this.OnStartActivity(Activity.Current, payload); - } - - break; - case OnStopEvent: - { - this.OnStopActivity(Activity.Current, payload); - } - - break; - } + return; } - public void OnStartActivity(Activity activity, object payload) + // Ensure context propagation irrespective of sampling decision + if (!this.startRequestFetcher.TryFetch(payload, out HttpRequestMessage request) || request == null) { - // The overall flow of what GrpcClient library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() - - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + GrpcInstrumentationEventSource.Log.NullPayload(nameof(GrpcClientDiagnosticListener), nameof(this.OnStartActivity)); + return; + } - if (Sdk.SuppressInstrumentation) - { - return; - } + if (this.options.SuppressDownstreamInstrumentation) + { + SuppressInstrumentationScope.Enter(); + + // If we are suppressing downstream instrumentation then inject + // context here. Grpc.Net.Client uses HttpClient, so + // SuppressDownstreamInstrumentation means that the + // OpenTelemetry instrumentation for HttpClient will not be + // invoked. + + // Note that HttpClient natively generates its own activity and + // propagates W3C trace context headers regardless of whether + // OpenTelemetry HttpClient instrumentation is invoked. + // Therefore, injecting here preserves more intuitive span + // parenting - i.e., the entry point span of a downstream + // service would be parented to the span generated by + // Grpc.Net.Client rather than the span generated natively by + // HttpClient. Injecting here also ensures that baggage is + // propagated to downstream services. + // Injecting context here also ensures that the configured + // propagator is used, as HttpClient by itself will only + // do TraceContext propagation. + var textMapPropagator = Propagators.DefaultTextMapPropagator; + textMapPropagator.Inject( + new PropagationContext(activity.Context, Baggage.Current), + request, + HttpRequestMessageContextPropagation.HeaderValueSetter); + } - // Ensure context propagation irrespective of sampling decision - if (!this.startRequestFetcher.TryFetch(payload, out HttpRequestMessage request) || request == null) - { - GrpcInstrumentationEventSource.Log.NullPayload(nameof(GrpcClientDiagnosticListener), nameof(this.OnStartActivity)); - return; - } + if (activity.IsAllDataRequested) + { + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); - if (this.options.SuppressDownstreamInstrumentation) - { - SuppressInstrumentationScope.Enter(); - - // If we are suppressing downstream instrumentation then inject - // context here. Grpc.Net.Client uses HttpClient, so - // SuppressDownstreamInstrumentation means that the - // OpenTelemetry instrumentation for HttpClient will not be - // invoked. - - // Note that HttpClient natively generates its own activity and - // propagates W3C trace context headers regardless of whether - // OpenTelemetry HttpClient instrumentation is invoked. - // Therefore, injecting here preserves more intuitive span - // parenting - i.e., the entry point span of a downstream - // service would be parented to the span generated by - // Grpc.Net.Client rather than the span generated natively by - // HttpClient. Injecting here also ensures that baggage is - // propagated to downstream services. - // Injecting context here also ensures that the configured - // propagator is used, as HttpClient by itself will only - // do TraceContext propagation. - var textMapPropagator = Propagators.DefaultTextMapPropagator; - textMapPropagator.Inject( - new PropagationContext(activity.Context, Baggage.Current), - request, - HttpRequestMessageContextPropagation.HeaderValueSetter); - } + var grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - if (activity.IsAllDataRequested) - { - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); - ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); + activity.DisplayName = grpcMethod?.Trim('/'); - var grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); - activity.DisplayName = grpcMethod?.Trim('/'); + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + { + activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); - activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + // Remove the grpc.method tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + } - if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + var uriHostNameType = Uri.CheckHostName(request.RequestUri.Host); + if (this.emitOldAttributes) + { + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) { - activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); - activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); - - // Remove the grpc.method tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + activity.SetTag(SemanticConventions.AttributeNetPeerIp, request.RequestUri.Host); } - - var uriHostNameType = Uri.CheckHostName(request.RequestUri.Host); - if (this.emitOldAttributes) + else { - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - activity.SetTag(SemanticConventions.AttributeNetPeerIp, request.RequestUri.Host); - } - else - { - activity.SetTag(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); - } - - activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); + activity.SetTag(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); } - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - if (this.emitNewAttributes) - { - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - activity.SetTag(SemanticConventions.AttributeServerSocketAddress, request.RequestUri.Host); - } - else - { - activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); - } - - activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); - } + activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); + } - try + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + if (this.emitNewAttributes) + { + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) { - this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + activity.SetTag(SemanticConventions.AttributeServerSocketAddress, request.RequestUri.Host); } - catch (Exception ex) + else { - GrpcInstrumentationEventSource.Log.EnrichmentException(ex); + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); } + + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); + } + + try + { + this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + } + catch (Exception ex) + { + GrpcInstrumentationEventSource.Log.EnrichmentException(ex); } } + } - public void OnStopActivity(Activity activity, object payload) + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + if (validConversion) { - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - if (validConversion) + if (activity.Status == ActivityStatusCode.Unset) { - if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); - } - - // setting rpc.grpc.status_code - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); } - // Remove the grpc.status_code tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + // setting rpc.grpc.status_code + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + } + + // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); - if (this.stopRequestFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) + if (this.stopRequestFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) + { + try { - try - { - this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); - } - catch (Exception ex) - { - GrpcInstrumentationEventSource.Log.EnrichmentException(ex); - } + this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); + } + catch (Exception ex) + { + GrpcInstrumentationEventSource.Log.EnrichmentException(ex); } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs index e16c76b8f3c..41bb94d718b 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs @@ -17,35 +17,34 @@ using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-Grpc")] +internal sealed class GrpcInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-Grpc")] - internal sealed class GrpcInstrumentationEventSource : EventSource - { - public static GrpcInstrumentationEventSource Log = new(); + public static GrpcInstrumentationEventSource Log = new(); - [Event(1, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName) - { - this.WriteEvent(1, handlerName, eventName); - } + [Event(1, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName) + { + this.WriteEvent(1, handlerName, eventName); + } - [NonEvent] - public void EnrichmentException(Exception ex) + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(ex.ToInvariantString()); - } + this.EnrichmentException(ex.ToInvariantString()); } + } - [Event(2, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] - public void EnrichmentException(string exception) - { - this.WriteEvent(2, exception); - } + [Event(2, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(2, exception); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs index b4f3662ca36..bd8be72a74a 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs @@ -14,136 +14,135 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Canonical result code of span execution. +/// +/// +/// This follows the standard GRPC codes. +/// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md. +/// +internal enum StatusCanonicalCode { /// - /// Canonical result code of span execution. - /// - /// - /// This follows the standard GRPC codes. - /// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md. - /// - internal enum StatusCanonicalCode - { - /// - /// The operation completed successfully. - /// - Ok = 0, - - /// - /// The operation was cancelled (typically by the caller). - /// - Cancelled = 1, - - /// - /// Unknown error. An example of where this error may be returned is if a Status value received - /// from another address space belongs to an error-space that is not known in this address space. - /// Also errors raised by APIs that do not return enough error information may be converted to - /// this error. - /// - Unknown = 2, - - /// - /// Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. - /// INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the - /// system (e.g., a malformed file name). - /// - InvalidArgument = 3, - - /// - /// Deadline expired before operation could complete. For operations that change the state of the - /// system, this error may be returned even if the operation has completed successfully. For - /// example, a successful response from a server could have been delayed long enough for the - /// deadline to expire. - /// - DeadlineExceeded = 4, - - /// - /// Some requested entity (e.g., file or directory) was not found. - /// - NotFound = 5, - - /// - /// Some entity that we attempted to create (e.g., file or directory) already exists. - /// - AlreadyExists = 6, - - /// - /// The caller does not have permission to execute the specified operation. PERMISSION_DENIED - /// must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED - /// instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be - /// identified (use UNAUTHENTICATED instead for those errors). - /// - PermissionDenied = 7, - - /// - /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system - /// is out of space. - /// - ResourceExhausted = 8, - - /// - /// Operation was rejected because the system is not in a state required for the operation's - /// execution. For example, directory to be deleted may be non-empty, an rmdir operation is - /// applied to a non-directory, etc. - /// A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION, - /// ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. - /// (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a - /// read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until - /// the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory - /// is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless - /// they have first fixed up the directory by deleting files from it. - /// - FailedPrecondition = 9, - - /// - /// The operation was aborted, typically due to a concurrency issue like sequencer check - /// failures, transaction aborts, etc. - /// - Aborted = 10, - - /// - /// Operation was attempted past the valid range. E.g., seeking or reading past end of file. - /// - /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system - /// state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to - /// read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if - /// asked to read from an offset past the current file size. - /// - /// There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend - /// using OUT_OF_RANGE (the more specific error) when it applies so that callers who are - /// iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are - /// done. - /// - OutOfRange = 11, - - /// - /// Operation is not implemented or not supported/enabled in this service. - /// - Unimplemented = 12, - - /// - /// Internal errors. Means some invariants expected by underlying system has been broken. If you - /// see one of these errors, something is very broken. - /// - Internal = 13, - - /// - /// The service is currently unavailable. This is a most likely a transient condition and may be - /// corrected by retrying with a backoff. - /// - /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE. - /// - Unavailable = 14, - - /// - /// Unrecoverable data loss or corruption. - /// - DataLoss = 15, - - /// - /// The request does not have valid authentication credentials for the operation. - /// - Unauthenticated = 16, - } + /// The operation completed successfully. + /// + Ok = 0, + + /// + /// The operation was cancelled (typically by the caller). + /// + Cancelled = 1, + + /// + /// Unknown error. An example of where this error may be returned is if a Status value received + /// from another address space belongs to an error-space that is not known in this address space. + /// Also errors raised by APIs that do not return enough error information may be converted to + /// this error. + /// + Unknown = 2, + + /// + /// Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. + /// INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the + /// system (e.g., a malformed file name). + /// + InvalidArgument = 3, + + /// + /// Deadline expired before operation could complete. For operations that change the state of the + /// system, this error may be returned even if the operation has completed successfully. For + /// example, a successful response from a server could have been delayed long enough for the + /// deadline to expire. + /// + DeadlineExceeded = 4, + + /// + /// Some requested entity (e.g., file or directory) was not found. + /// + NotFound = 5, + + /// + /// Some entity that we attempted to create (e.g., file or directory) already exists. + /// + AlreadyExists = 6, + + /// + /// The caller does not have permission to execute the specified operation. PERMISSION_DENIED + /// must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED + /// instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be + /// identified (use UNAUTHENTICATED instead for those errors). + /// + PermissionDenied = 7, + + /// + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + /// is out of space. + /// + ResourceExhausted = 8, + + /// + /// Operation was rejected because the system is not in a state required for the operation's + /// execution. For example, directory to be deleted may be non-empty, an rmdir operation is + /// applied to a non-directory, etc. + /// A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION, + /// ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. + /// (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a + /// read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory + /// is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// + FailedPrecondition = 9, + + /// + /// The operation was aborted, typically due to a concurrency issue like sequencer check + /// failures, transaction aborts, etc. + /// + Aborted = 10, + + /// + /// Operation was attempted past the valid range. E.g., seeking or reading past end of file. + /// + /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system + /// state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to + /// read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if + /// asked to read from an offset past the current file size. + /// + /// There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend + /// using OUT_OF_RANGE (the more specific error) when it applies so that callers who are + /// iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are + /// done. + /// + OutOfRange = 11, + + /// + /// Operation is not implemented or not supported/enabled in this service. + /// + Unimplemented = 12, + + /// + /// Internal errors. Means some invariants expected by underlying system has been broken. If you + /// see one of these errors, something is very broken. + /// + Internal = 13, + + /// + /// The service is currently unavailable. This is a most likely a transient condition and may be + /// corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE. + /// + Unavailable = 14, + + /// + /// Unrecoverable data loss or corruption. + /// + DataLoss = 15, + + /// + /// The request does not have valid authentication credentials for the operation. + /// + Unauthenticated = 16, } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs index 504a242ac26..69f241afca5 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs @@ -20,68 +20,67 @@ using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of gRPC client +/// instrumentation. +/// +public static class TracerProviderBuilderExtensions { /// - /// Extension methods to simplify registering of gRPC client - /// instrumentation. + /// Enables gRPC client instrumentation. /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation(this TracerProviderBuilder builder) - => AddGrpcClientInstrumentation(builder, name: null, configure: null); + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation(this TracerProviderBuilder builder) + => AddGrpcClientInstrumentation(builder, name: null, configure: null); - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation( - this TracerProviderBuilder builder, - Action configure) - => AddGrpcClientInstrumentation(builder, name: null, configure); + /// + /// Enables gRPC client instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation( + this TracerProviderBuilder builder, + Action configure) + => AddGrpcClientInstrumentation(builder, name: null, configure); - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); + /// + /// Enables gRPC client instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configure) + { + Guard.ThrowIfNull(builder); - name ??= Options.DefaultName; + name ??= Options.DefaultName; - builder.ConfigureServices(services => + builder.ConfigureServices(services => + { + if (configure != null) { - if (configure != null) - { - services.Configure(name, configure); - } + services.Configure(name, configure); + } - services.RegisterOptionsFactory(configuration => new GrpcClientInstrumentationOptions(configuration)); - }); + services.RegisterOptionsFactory(configuration => new GrpcClientInstrumentationOptions(configuration)); + }); - builder.AddSource(GrpcClientDiagnosticListener.ActivitySourceName); - builder.AddLegacySource("Grpc.Net.Client.GrpcOut"); + builder.AddSource(GrpcClientDiagnosticListener.ActivitySourceName); + builder.AddLegacySource("Grpc.Net.Client.GrpcOut"); - return builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); + return builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); - return new GrpcClientInstrumentation(options); - }); - } + return new GrpcClientInstrumentation(options); + }); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs b/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs index 3f8d3ece0e3..5d33ad70f6a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs @@ -18,24 +18,23 @@ using System.Net.Http; #endif -namespace OpenTelemetry.Instrumentation.Http +namespace OpenTelemetry.Instrumentation.Http; + +internal static class HttpRequestMessageContextPropagation { - internal static class HttpRequestMessageContextPropagation + internal static Func> HeaderValuesGetter => (request, name) => { - internal static Func> HeaderValuesGetter => (request, name) => + if (request.Headers.TryGetValues(name, out var values)) { - if (request.Headers.TryGetValues(name, out var values)) - { - return values; - } + return values; + } - return null; - }; + return null; + }; - internal static Action HeaderValueSetter => (request, name, value) => - { - request.Headers.Remove(name); - request.Headers.Add(name, value); - }; - } + internal static Action HeaderValueSetter => (request, name, value) => + { + request.Headers.Remove(name); + request.Headers.Add(name, value); + }; } diff --git a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs index 6993b06805a..5207e98ec2d 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs @@ -18,42 +18,41 @@ using OpenTelemetry.Internal; #pragma warning restore IDE0005 -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +internal sealed class DiagnosticSourceListener : IObserver> { - internal sealed class DiagnosticSourceListener : IObserver> + private readonly ListenerHandler handler; + + public DiagnosticSourceListener(ListenerHandler handler) { - private readonly ListenerHandler handler; + Guard.ThrowIfNull(handler); - public DiagnosticSourceListener(ListenerHandler handler) - { - Guard.ThrowIfNull(handler); + this.handler = handler; + } - this.handler = handler; - } + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } - public void OnCompleted() + public void OnNext(KeyValuePair value) + { + if (!this.handler.SupportsNullActivity && Activity.Current == null) { + return; } - public void OnError(Exception error) + try { + this.handler.OnEventWritten(value.Key, value.Value); } - - public void OnNext(KeyValuePair value) + catch (Exception ex) { - if (!this.handler.SupportsNullActivity && Activity.Current == null) - { - return; - } - - try - { - this.handler.OnEventWritten(value.Key, value.Value); - } - catch (Exception ex) - { - InstrumentationEventSource.Log.UnknownErrorProcessingEvent(this.handler?.SourceName, value.Key, ex); - } + InstrumentationEventSource.Log.UnknownErrorProcessingEvent(this.handler?.SourceName, value.Key, ex); } } } diff --git a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs index 775fc8ae5a4..23f9fa8f067 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs @@ -17,97 +17,96 @@ using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +internal sealed class DiagnosticSourceSubscriber : IDisposable, IObserver { - internal sealed class DiagnosticSourceSubscriber : IDisposable, IObserver + private readonly List listenerSubscriptions; + private readonly Func handlerFactory; + private readonly Func diagnosticSourceFilter; + private readonly Func isEnabledFilter; + private long disposed; + private IDisposable allSourcesSubscription; + + public DiagnosticSourceSubscriber( + ListenerHandler handler, + Func isEnabledFilter) + : this(_ => handler, value => handler.SourceName == value.Name, isEnabledFilter) { - private readonly List listenerSubscriptions; - private readonly Func handlerFactory; - private readonly Func diagnosticSourceFilter; - private readonly Func isEnabledFilter; - private long disposed; - private IDisposable allSourcesSubscription; - - public DiagnosticSourceSubscriber( - ListenerHandler handler, - Func isEnabledFilter) - : this(_ => handler, value => handler.SourceName == value.Name, isEnabledFilter) - { - } + } - public DiagnosticSourceSubscriber( - Func handlerFactory, - Func diagnosticSourceFilter, - Func isEnabledFilter) - { - Guard.ThrowIfNull(handlerFactory); + public DiagnosticSourceSubscriber( + Func handlerFactory, + Func diagnosticSourceFilter, + Func isEnabledFilter) + { + Guard.ThrowIfNull(handlerFactory); - this.listenerSubscriptions = new List(); - this.handlerFactory = handlerFactory; - this.diagnosticSourceFilter = diagnosticSourceFilter; - this.isEnabledFilter = isEnabledFilter; - } + this.listenerSubscriptions = new List(); + this.handlerFactory = handlerFactory; + this.diagnosticSourceFilter = diagnosticSourceFilter; + this.isEnabledFilter = isEnabledFilter; + } - public void Subscribe() + public void Subscribe() + { + if (this.allSourcesSubscription == null) { - if (this.allSourcesSubscription == null) - { - this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); - } + this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); } + } - public void OnNext(DiagnosticListener value) + public void OnNext(DiagnosticListener value) + { + if ((Interlocked.Read(ref this.disposed) == 0) && + this.diagnosticSourceFilter(value)) { - if ((Interlocked.Read(ref this.disposed) == 0) && - this.diagnosticSourceFilter(value)) + var handler = this.handlerFactory(value.Name); + var listener = new DiagnosticSourceListener(handler); + var subscription = this.isEnabledFilter == null ? + value.Subscribe(listener) : + value.Subscribe(listener, this.isEnabledFilter); + + lock (this.listenerSubscriptions) { - var handler = this.handlerFactory(value.Name); - var listener = new DiagnosticSourceListener(handler); - var subscription = this.isEnabledFilter == null ? - value.Subscribe(listener) : - value.Subscribe(listener, this.isEnabledFilter); - - lock (this.listenerSubscriptions) - { - this.listenerSubscriptions.Add(subscription); - } + this.listenerSubscriptions.Add(subscription); } } + } - public void OnCompleted() - { - } + public void OnCompleted() + { + } - public void OnError(Exception error) - { - } + public void OnError(Exception error) + { + } - /// - public void Dispose() + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) { - this.Dispose(true); - GC.SuppressFinalize(this); + return; } - private void Dispose(bool disposing) + lock (this.listenerSubscriptions) { - if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) - { - return; - } - - lock (this.listenerSubscriptions) + foreach (var listenerSubscription in this.listenerSubscriptions) { - foreach (var listenerSubscription in this.listenerSubscriptions) - { - listenerSubscription?.Dispose(); - } - - this.listenerSubscriptions.Clear(); + listenerSubscription?.Dispose(); } - this.allSourcesSubscription?.Dispose(); - this.allSourcesSubscription = null; + this.listenerSubscriptions.Clear(); } + + this.allSourcesSubscription?.Dispose(); + this.allSourcesSubscription = null; } } diff --git a/src/Shared/SpanHelper.cs b/src/Shared/SpanHelper.cs index 6f909db58c0..0a7a68ae323 100644 --- a/src/Shared/SpanHelper.cs +++ b/src/Shared/SpanHelper.cs @@ -16,29 +16,28 @@ using System.Diagnostics; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// A collection of helper methods to be used when building spans. +/// +internal static class SpanHelper { /// - /// A collection of helper methods to be used when building spans. + /// Helper method that populates span properties from http status code according + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status. /// - internal static class SpanHelper + /// The span kind. + /// Http status code. + /// Resolved span for the Http status code. + public static ActivityStatusCode ResolveSpanStatusForHttpStatusCode(ActivityKind kind, int httpStatusCode) { - /// - /// Helper method that populates span properties from http status code according - /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status. - /// - /// The span kind. - /// Http status code. - /// Resolved span for the Http status code. - public static ActivityStatusCode ResolveSpanStatusForHttpStatusCode(ActivityKind kind, int httpStatusCode) + var upperBound = kind == ActivityKind.Client ? 399 : 499; + if (httpStatusCode >= 100 && httpStatusCode <= upperBound) { - var upperBound = kind == ActivityKind.Client ? 399 : 499; - if (httpStatusCode >= 100 && httpStatusCode <= upperBound) - { - return ActivityStatusCode.Unset; - } - - return ActivityStatusCode.Error; + return ActivityStatusCode.Unset; } + + return ActivityStatusCode.Error; } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs index 2bfbd067e98..3bca59f8498 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs @@ -14,13 +14,12 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +internal static class AttributesExtensions { - internal static class AttributesExtensions + public static object GetValue(this IEnumerable> attributes, string key) { - public static object GetValue(this IEnumerable> attributes, string key) - { - return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; - } + return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 326cf4d5705..184422e953e 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -32,151 +32,213 @@ using TestApp.AspNetCore.Filters; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample +public sealed class BasicTests + : IClassFixture>, IDisposable { - // See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample - public sealed class BasicTests - : IClassFixture>, IDisposable + private readonly WebApplicationFactory factory; + private TracerProvider tracerProvider = null; + + public BasicTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() { - private readonly WebApplicationFactory factory; - private TracerProvider tracerProvider = null; + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + } - public BasicTests(WebApplicationFactory factory) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StatusIsUnsetOn200Response(bool disableLogging) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) { - this.factory = factory; + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); } - [Fact] - public void AddAspNetCoreInstrumentation_BadArgs() + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + if (disableLogging) + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + } + }) + .CreateClient()) { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + // Act + using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task StatusIsUnsetOn200Response(bool disableLogging) - { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => + Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => { - builder.ConfigureTestServices(ConfigureTestServices); - if (disableLogging) + if (shouldEnrich) { - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); }; + options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); }; } }) - .CreateClient()) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values").ConfigureAwait(false); - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 - WaitForActivityExport(exportedItems, 1); - } + WaitForActivityExport(exportedItems, 1); + } - Assert.Single(exportedItems); - var activity = exportedItems[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - ValidateAspNetCoreActivity(activity, "/api/values"); + if (shouldEnrich) + { + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes")); + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes")); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich) - { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Fact] + public async Task SuccessfulTemplateControllerCallUsesParentContext() + { + var exportedItems = new List(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedSpanId = ActivitySpanId.CreateRandom(); + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(options => - { - if (shouldEnrich) - { - options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); }; - options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); }; - } - }) + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() .AddInMemoryExporter(exportedItems) .Build(); - } + }); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2"); + request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Act + var response = await client.SendAsync(request).ConfigureAwait(false); - WaitForActivityExport(exportedItems, 1); - } + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 - Assert.Single(exportedItems); - var activity = exportedItems[0]; + WaitForActivityExport(exportedItems, 1); + } - if (shouldEnrich) - { - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes")); - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes")); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; - ValidateAspNetCoreActivity(activity, "/api/values"); - } + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + Assert.Equal("api/Values/{id}", activity.DisplayName); + + Assert.Equal(expectedTraceId, activity.Context.TraceId); + Assert.Equal(expectedSpanId, activity.ParentSpanId); + + ValidateAspNetCoreActivity(activity, "/api/values/2"); + } - [Fact] - public async Task SuccessfulTemplateControllerCallUsesParentContext() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomPropagator(bool addSampler) + { + try { var exportedItems = new List(); var expectedTraceId = ActivityTraceId.CreateRandom(); var expectedSpanId = ActivitySpanId.CreateRandom(); + var propagator = new Mock(); + propagator.Setup(m => m.Extract(It.IsAny(), It.IsAny(), It.IsAny>>())).Returns( + new PropagationContext( + new ActivityContext( + expectedTraceId, + expectedSpanId, + ActivityTraceFlags.Recorded), + default)); + // Arrange using (var testFactory = this.factory .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - }); + builder.ConfigureTestServices(services => + { + Sdk.SetDefaultTextMapPropagator(propagator.Object); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) + if (addSampler) + { + tracerProviderBuilder + .SetSampler(new TestSampler(SamplingDecision.RecordAndSample, new Dictionary { { "SomeTag", "SomeKey" }, })); + } + + this.tracerProvider = tracerProviderBuilder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) { using var client = testFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2"); - request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); - - // Act - var response = await client.SendAsync(request).ConfigureAwait(false); - - // Assert + using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); response.EnsureSuccessStatusCode(); // Status Code 200-299 WaitForActivityExport(exportedItems, 1); @@ -185,7 +247,7 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() Assert.Single(exportedItems); var activity = exportedItems[0]; - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + Assert.True(activity.Duration != TimeSpan.Zero); Assert.Equal("api/Values/{id}", activity.DisplayName); Assert.Equal(expectedTraceId, activity.Context.TraceId); @@ -193,734 +255,588 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() ValidateAspNetCoreActivity(activity, "/api/values/2"); } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CustomPropagator(bool addSampler) + [Fact] + public async Task RequestNotCollectedWhenFilterIsApplied() + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) { - try + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - var exportedItems = new List(); - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedSpanId = ActivitySpanId.CreateRandom(); - - var propagator = new Mock(); - propagator.Setup(m => m.Extract(It.IsAny(), It.IsAny(), It.IsAny>>())).Returns( - new PropagationContext( - new ActivityContext( - expectedTraceId, - expectedSpanId, - ActivityTraceFlags.Recorded), - default)); - - // Arrange - using (var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - Sdk.SetDefaultTextMapPropagator(propagator.Object); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); - if (addSampler) - { - tracerProviderBuilder - .SetSampler(new TestSampler(SamplingDecision.RecordAndSample, new Dictionary { { "SomeTag", "SomeKey" }, })); - } + // Act + using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - this.tracerProvider = tracerProviderBuilder - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) - { - using var client = testFactory.CreateClient(); - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Assert + response1.EnsureSuccessStatusCode(); // Status Code 200-299 + response2.EnsureSuccessStatusCode(); // Status Code 200-299 - WaitForActivityExport(exportedItems, 1); - } + WaitForActivityExport(exportedItems, 1); + } - Assert.Single(exportedItems); - var activity = exportedItems[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.True(activity.Duration != TimeSpan.Zero); - Assert.Equal("api/Values/{id}", activity.DisplayName); + ValidateAspNetCoreActivity(activity, "/api/values"); + } - Assert.Equal(expectedTraceId, activity.Context.TraceId); - Assert.Equal(expectedSpanId, activity.ParentSpanId); + [Fact] + public async Task RequestNotCollectedWhenFilterThrowException() + { + var exportedItems = new List(); - ValidateAspNetCoreActivity(activity, "/api/values/2"); - } - finally - { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + if (ctx.Request.Path == "/api/values/2") + { + throw new Exception("from InstrumentationFilter"); + } + else + { + return true; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); } - [Fact] - public async Task RequestNotCollectedWhenFilterIsApplied() - { - var exportedItems = new List(); - - void ConfigureTestServices(IServiceCollection services) + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") - .AddInMemoryExporter(exportedItems) - .Build(); - } + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); - // Arrange - using (var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) + // Act + using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) { - using var client = testFactory.CreateClient(); - - // Act using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - // Assert response1.EnsureSuccessStatusCode(); // Status Code 200-299 response2.EnsureSuccessStatusCode(); // Status Code 200-299 - - WaitForActivityExport(exportedItems, 1); + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); } - Assert.Single(exportedItems); - var activity = exportedItems[0]; - - ValidateAspNetCoreActivity(activity, "/api/values"); + WaitForActivityExport(exportedItems, 1); } - [Fact] - public async Task RequestNotCollectedWhenFilterThrowException() - { - var exportedItems = new List(); + // As InstrumentationFilter threw, we continue as if the + // InstrumentationFilter did not exist. - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => - { - if (ctx.Request.Path == "/api/values/2") - { - throw new Exception("from InstrumentationFilter"); - } - else - { - return true; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) + { + try + { + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); // Arrange - using (var testFactory = this.factory + using var testFactory = this.factory .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) - { - using var client = testFactory.CreateClient(); - - // Act - using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) - { - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - - response1.EnsureSuccessStatusCode(); // Status Code 200-299 - response2.EnsureSuccessStatusCode(); // Status Code 200-299 - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); - } - - WaitForActivityExport(exportedItems, 1); - } - - // As InstrumentationFilter threw, we continue as if the - // InstrumentationFilter did not exist. - - Assert.Single(exportedItems); - var activity = exportedItems[0]; - ValidateAspNetCoreActivity(activity, "/api/values"); - } - - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) - { - try - { - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedParentSpanId = ActivitySpanId.CreateRandom(); - var expectedTraceState = "rojo=1,congo=2"; - var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true); - var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); - Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); - - // Arrange - using var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }); - using var client = testFactory.CreateClient(); + { + builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); - // Test TraceContext Propagation - var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); - var response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request).ConfigureAwait(false); + var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); - Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); - Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers - // Test Baggage Context Propagation - request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + // Test Baggage Context Propagation + request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); - response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + response = await client.SendAsync(request).ConfigureAwait(false); + var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); - Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); - } - finally + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public async Task ExtractContextIrrespectiveOfTheFilterApplied() + [Fact] + public async Task ExtractContextIrrespectiveOfTheFilterApplied() + { + try { - try - { - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedParentSpanId = ActivitySpanId.CreateRandom(); - var expectedTraceState = "rojo=1,congo=2"; - var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState); - var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); - Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); - - // Arrange - bool isFilterCalled = false; - using var testFactory = this.factory - .WithWebHostBuilder(builder => + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); + + // Arrange + bool isFilterCalled = false; + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { - builder.ConfigureTestServices(services => - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(options => + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => + { + options.Filter = context => { - options.Filter = context => - { - isFilterCalled = true; - return false; - }; - }) - .Build(); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + isFilterCalled = true; + return false; + }; + }) + .Build(); }); - using var client = testFactory.CreateClient(); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); - // Test TraceContext Propagation - var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); - var response = await client.SendAsync(request).ConfigureAwait(false); + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request).ConfigureAwait(false); - // Ensure that filter was called - Assert.True(isFilterCalled); + // Ensure that filter was called + Assert.True(isFilterCalled); - var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + var childActivityTraceContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); - Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); - Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers - // Test Baggage Context Propagation - request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + // Test Baggage Context Propagation + request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); - response = await client.SendAsync(request).ConfigureAwait(false); - var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); + response = await client.SendAsync(request).ConfigureAwait(false); + var childActivityBaggageContext = JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); - Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); - } - finally + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public async Task BaggageIsNotClearedWhenActivityStopped() - { - int? baggageCountAfterStart = null; - int? baggageCountAfterStop = null; - using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); + [Fact] + public async Task BaggageIsNotClearedWhenActivityStopped() + { + int? baggageCountAfterStart = null; + int? baggageCountAfterStop = null; + using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - baggageCountAfterStart = Baggage.Current.Count; - } - - break; - case HttpInListener.OnStopEvent: - { - baggageCountAfterStop = Baggage.Current.Count; - stopSignal.Set(); - } - - break; - } - }, - }) - .Build(); - } + case HttpInListener.OnStartEvent: + { + baggageCountAfterStart = Baggage.Current.Count; + } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + break; + case HttpInListener.OnStopEvent: + { + baggageCountAfterStop = Baggage.Current.Count; + stopSignal.Set(); + } - request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); + break; + } + }, + }) + .Build(); + } - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); - stopSignal.WaitOne(5000); + request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); - // Assert - Assert.NotNull(baggageCountAfterStart); - Assert.Equal(2, baggageCountAfterStart); - Assert.NotNull(baggageCountAfterStop); - Assert.Equal(2, baggageCountAfterStop); + // Act + using var response = await client.SendAsync(request).ConfigureAwait(false); } - [Theory] - [InlineData(SamplingDecision.Drop, false, false)] - [InlineData(SamplingDecision.RecordOnly, true, true)] - [InlineData(SamplingDecision.RecordAndSample, true, true)] - public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled) - { - bool filterCalled = false; - bool enrichWithHttpRequestCalled = false; - bool enrichWithHttpResponseCalled = false; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new TestSampler(samplingDecision)) - .AddAspNetCoreInstrumentation(options => - { - options.Filter = (context) => - { - filterCalled = true; - return true; - }; - options.EnrichWithHttpRequest = (activity, request) => - { - enrichWithHttpRequestCalled = true; - }; - options.EnrichWithHttpResponse = (activity, request) => - { - enrichWithHttpResponseCalled = true; - }; - }) - .Build(); - } + stopSignal.WaitOne(5000); - // Arrange - using var client = this.factory - .WithWebHostBuilder(builder => + // Assert + Assert.NotNull(baggageCountAfterStart); + Assert.Equal(2, baggageCountAfterStart); + Assert.NotNull(baggageCountAfterStop); + Assert.Equal(2, baggageCountAfterStop); + } + + [Theory] + [InlineData(SamplingDecision.Drop, false, false)] + [InlineData(SamplingDecision.RecordOnly, true, true)] + [InlineData(SamplingDecision.RecordAndSample, true, true)] + public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled) + { + bool filterCalled = false; + bool enrichWithHttpRequestCalled = false; + bool enrichWithHttpResponseCalled = false; + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new TestSampler(samplingDecision)) + .AddAspNetCoreInstrumentation(options => { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + options.Filter = (context) => + { + filterCalled = true; + return true; + }; + options.EnrichWithHttpRequest = (activity, request) => + { + enrichWithHttpRequestCalled = true; + }; + options.EnrichWithHttpResponse = (activity, request) => + { + enrichWithHttpResponseCalled = true; + }; }) - .CreateClient(); - - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); - - // Assert - Assert.Equal(shouldFilterBeCalled, filterCalled); - Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled); - Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled); + .Build(); } - [Fact] - public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() - { - var exportedItems = new List(); + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + // Act + using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + + // Assert + Assert.Equal(shouldFilterBeCalled, filterCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled); + } - var activitySourceName = "TestMiddlewareActivitySource"; - var activityName = "TestMiddlewareActivity"; + [Fact] + public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() + { + var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddSource(activitySourceName) - .AddInMemoryExporter(exportedItems) - .Build(); - } + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + void ConfigureTestServices(IServiceCollection services) + { + services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - WaitForActivityExport(exportedItems, 2); - } + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } - Assert.Equal(2, exportedItems.Count); + Assert.Equal(2, exportedItems.Count); - var middlewareActivity = exportedItems[0]; + var middlewareActivity = exportedItems[0]; - var aspnetcoreframeworkactivity = exportedItems[1]; + var aspnetcoreframeworkactivity = exportedItems[1]; - // Middleware activity name should not be changed - Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); - Assert.Equal(activityName, middlewareActivity.OperationName); - Assert.Equal(activityName, middlewareActivity.DisplayName); + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + Assert.Equal(activityName, middlewareActivity.OperationName); + Assert.Equal(activityName, middlewareActivity.DisplayName); - // tag http.route should be added on activity started by asp.net core - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRoute) as string); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.DisplayName); - } + // tag http.route should be added on activity started by asp.net core + Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRoute) as string); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); + Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.DisplayName); + } - [Fact] - public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated() - { - var exportedItems = new List(); + [Fact] + public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated() + { + var exportedItems = new List(); - var activitySourceName = "TestMiddlewareActivitySource"; - var activityName = "TestMiddlewareActivity"; + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddSource(activitySourceName) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - WaitForActivityExport(exportedItems, 2); - } + builder.ConfigureTestServices((IServiceCollection services) => + { + services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems)); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } - Assert.Equal(2, exportedItems.Count); + Assert.Equal(2, exportedItems.Count); - var middlewareActivity = exportedItems[0]; + var middlewareActivity = exportedItems[0]; - var aspnetcoreframeworkactivity = exportedItems[1]; + var aspnetcoreframeworkactivity = exportedItems[1]; - // Middleware activity name should not be changed - Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); - Assert.Equal(activityName, middlewareActivity.OperationName); - Assert.Equal(activityName, middlewareActivity.DisplayName); + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + Assert.Equal(activityName, middlewareActivity.OperationName); + Assert.Equal(activityName, middlewareActivity.DisplayName); - // tag http.route should not be added on activity started by asp.net core as it will not be found during OnEventWritten event - Assert.DoesNotContain(aspnetcoreframeworkactivity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("/api/values/2", aspnetcoreframeworkactivity.DisplayName); - } + // tag http.route should not be added on activity started by asp.net core as it will not be found during OnEventWritten event + Assert.DoesNotContain(aspnetcoreframeworkactivity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); + Assert.Equal("/api/values/2", aspnetcoreframeworkactivity.DisplayName); + } #if NET7_0_OR_GREATER - [Fact] - public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() + [Fact] + public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems)); - - // Register ActivitySource here so that it will be used - // by ASP.NET Core to create activities - // https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152 - services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource")); - } - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); - - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems)); - WaitForActivityExport(exportedItems, 1); - } + // Register ActivitySource here so that it will be used + // by ASP.NET Core to create activities + // https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152 + services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource")); + } - Assert.Single(exportedItems); - var activity = exportedItems[0]; + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values").ConfigureAwait(false); - Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); + } #endif - [Theory] - [InlineData(1)] - [InlineData(2)] - public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) - { - var exportedItems = new List(); + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) + { + var exportedItems = new List(); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices( - (s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems)); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - // Act - using var response = await client.GetAsync("/api/error").ConfigureAwait(false); - - WaitForActivityExport(exportedItems, 1); - } + builder.ConfigureTestServices( + (s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems)); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/error").ConfigureAwait(false); - // Assert - AssertException(exportedItems); + WaitForActivityExport(exportedItems, 1); } - [Fact] - public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() + // Assert + AssertException(exportedItems); + } + + [Fact] + public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + void ConfigureTestServices(IServiceCollection services) { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } - - break; - default: - { - numberOfUnSubscribedEvents++; - } - - break; - } - }, - }) - .Build(); - } + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } + break; + case HttpInListener.OnMvcBeforeActionEvent: + { + numberofSubscribedEvents++; + } - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(3, numberofSubscribedEvents); - } + break; + default: + { + numberOfUnSubscribedEvents++; + } - [Fact] - public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException() - { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - int numberOfExceptionCallbacks = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) - { - OnEventWrittenCallback = (name, payload) => - { - switch (name) - { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } - - break; - - // TODO: Add test case for validating name for both the types - // of exception event. - case HttpInListener.OnUnhandledHostingExceptionEvent: - case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: - { - numberofSubscribedEvents++; - numberOfExceptionCallbacks++; - } - - break; - default: - { - numberOfUnSubscribedEvents++; - } - - break; - } - }, - }) - .Build(); - } + break; + } + }, + }) + .Build(); + } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); - - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } - catch - { - // ignore exception - } - } + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); - Assert.Equal(1, numberOfExceptionCallbacks); - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(4, numberofSubscribedEvents); + // Act + using var response = await client.SendAsync(request).ConfigureAwait(false); } - [Fact] - public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() - { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - int numberOfExceptionCallbacks = 0; + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(3, numberofSubscribedEvents); + } - // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() + [Fact] + public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation( new TestHttpInListener(new AspNetCoreInstrumentationOptions()) { @@ -939,6 +855,12 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan numberofSubscribedEvents++; } + break; + case HttpInListener.OnMvcBeforeActionEvent: + { + numberofSubscribedEvents++; + } + break; // TODO: Add test case for validating name for both the types @@ -960,273 +882,350 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan } }, }) - .Build(); - - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - - app.UseExceptionHandler(handler => - { - handler.Run(async (ctx) => - { - await ctx.Response.WriteAsync("handled").ConfigureAwait(false); - }); - }); - - app.Map("/error", ThrowException); + .Build(); + } - static void ThrowException(IApplicationBuilder app) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - app.Run(context => - { - throw new Exception("CustomException"); - }); - } - - _ = app.RunAsync(); - - using var client = new HttpClient(); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { try { - await client.GetStringAsync("http://localhost:5000/error").ConfigureAwait(false); + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + + // Act + using var response = await client.SendAsync(request).ConfigureAwait(false); } catch { - // ignore 500 error. + // ignore exception } - - Assert.Equal(0, numberOfExceptionCallbacks); - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(2, numberofSubscribedEvents); - - await app.DisposeAsync().ConfigureAwait(false); } - [Fact] - public async Task RouteInformationIsNotAddedToRequestsOutsideOfMVC() - { - var exportedItems = new List(); + Assert.Equal(1, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(4, numberofSubscribedEvents); + } - // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); + [Fact] + public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + + // configure SDK + using var tracerprovider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) + { + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } - app.MapGet("/custom/{name:alpha}", () => "Hello"); + break; - _ = app.RunAsync(); + // TODO: Add test case for validating name for both the types + // of exception event. + case HttpInListener.OnUnhandledHostingExceptionEvent: + case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: + { + numberofSubscribedEvents++; + numberOfExceptionCallbacks++; + } - using var client = new HttpClient(); - var res = await client.GetStringAsync("http://localhost:5000/custom/abc").ConfigureAwait(false); - Assert.NotNull(res); + break; + default: + { + numberOfUnSubscribedEvents++; + } - tracerprovider.ForceFlush(); - for (var i = 0; i < 10; i++) - { - if (exportedItems.Count > 0) - { - break; - } + break; + } + }, + }) + .Build(); - // 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); - } + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); - var activity = exportedItems[0]; + app.UseExceptionHandler(handler => + { + handler.Run(async (ctx) => + { + await ctx.Response.WriteAsync("handled").ConfigureAwait(false); + }); + }); - Assert.NotNull(activity); + app.Map("/error", ThrowException); - // After fix update to Contains http.route - Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + static void ThrowException(IApplicationBuilder app) + { + app.Run(context => + { + throw new Exception("CustomException"); + }); + } - // After fix this should be /custom/{name:alpha} - Assert.Equal("/custom/abc", activity.DisplayName); + _ = app.RunAsync(); - await app.DisposeAsync().ConfigureAwait(false); + using var client = new HttpClient(); + try + { + await client.GetStringAsync("http://localhost:5000/error").ConfigureAwait(false); } - - public void Dispose() + catch { - this.tracerProvider?.Dispose(); + // ignore 500 error. } - private static void WaitForActivityExport(List exportedItems, int count) + Assert.Equal(0, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberofSubscribedEvents); + + await app.DisposeAsync().ConfigureAwait(false); + } + + [Fact] + public async Task RouteInformationIsNotAddedToRequestsOutsideOfMVC() + { + var exportedItems = new List(); + + // configure SDK + using var tracerprovider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + + app.MapGet("/custom/{name:alpha}", () => "Hello"); + + _ = app.RunAsync(); + + using var client = new HttpClient(); + var res = await client.GetStringAsync("http://localhost:5000/custom/abc").ConfigureAwait(false); + Assert.NotNull(res); + + tracerprovider.ForceFlush(); + for (var i = 0; i < 10; i++) { + if (exportedItems.Count > 0) + { + 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return exportedItems.Count >= count; - }, - TimeSpan.FromSeconds(1))); + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } - private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) - { - Assert.Equal(ActivityKind.Server, activityToValidate.Kind); + var activity = exportedItems[0]; + + Assert.NotNull(activity); + + // After fix update to Contains http.route + Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + + // After fix this should be /custom/{name:alpha} + Assert.Equal("/custom/abc", activity.DisplayName); + + await app.DisposeAsync().ConfigureAwait(false); + } + + public void Dispose() + { + this.tracerProvider?.Dispose(); + } + + private static void WaitForActivityExport(List exportedItems, int count) + { + // 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 + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return exportedItems.Count >= count; + }, + TimeSpan.FromSeconds(1))); + } + + private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) + { + Assert.Equal(ActivityKind.Server, activityToValidate.Kind); #if NET7_0_OR_GREATER - Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); - Assert.Empty(activityToValidate.Source.Version); + Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); + Assert.Empty(activityToValidate.Source.Version); #else - Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); - Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); + 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.AttributeHttpTarget) as string); + } - private static void AssertException(List exportedItems) - { - Assert.Single(exportedItems); - var activity = exportedItems[0]; + private static void AssertException(List exportedItems) + { + Assert.Single(exportedItems); + var activity = exportedItems[0]; - var exMessage = "something's wrong!"; - Assert.Single(activity.Events); - Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + var exMessage = "something's wrong!"; + Assert.Single(activity.Events); + Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - ValidateAspNetCoreActivity(activity, "/api/error"); - } + ValidateAspNetCoreActivity(activity, "/api/error"); + } - private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List exportedItems) + private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List exportedItems) + { + switch (mode) { - switch (mode) - { - case 1: - services.AddMvc(x => x.Filters.Add()); - break; - case 2: - services.AddMvc(x => x.Filters.Add()); - services.AddMvc(x => x.Filters.Add()); - break; - default: - break; - } - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(x => x.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + case 1: + services.AddMvc(x => x.Filters.Add()); + break; + case 2: + services.AddMvc(x => x.Filters.Add()); + services.AddMvc(x => x.Filters.Add()); + break; + default: + break; } - private class ExtractOnlyPropagator : TextMapPropagator - { - private readonly ActivityContext activityContext; - private readonly Baggage baggage; + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(x => x.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); + } - public ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) - { - this.activityContext = activityContext; - this.baggage = baggage; - } + private class ExtractOnlyPropagator : TextMapPropagator + { + private readonly ActivityContext activityContext; + private readonly Baggage baggage; - public override ISet Fields => throw new NotImplementedException(); + public ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) + { + this.activityContext = activityContext; + this.baggage = baggage; + } - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) - { - return new PropagationContext(this.activityContext, this.baggage); - } + public override ISet Fields => throw new NotImplementedException(); - public override void Inject(PropagationContext context, T carrier, Action setter) - { - throw new NotImplementedException(); - } + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + return new PropagationContext(this.activityContext, this.baggage); } - private class TestSampler : Sampler + public override void Inject(PropagationContext context, T carrier, Action setter) { - private readonly SamplingDecision samplingDecision; - private readonly IEnumerable> attributes; + throw new NotImplementedException(); + } + } - public TestSampler(SamplingDecision samplingDecision, IEnumerable> attributes = null) - { - this.samplingDecision = samplingDecision; - this.attributes = attributes; - } + private class TestSampler : Sampler + { + private readonly SamplingDecision samplingDecision; + private readonly IEnumerable> attributes; - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return new SamplingResult(this.samplingDecision, this.attributes); - } + public TestSampler(SamplingDecision samplingDecision, IEnumerable> attributes = null) + { + this.samplingDecision = samplingDecision; + this.attributes = attributes; } - private class TestHttpInListener : HttpInListener + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - public Action OnEventWrittenCallback; - - public TestHttpInListener(AspNetCoreInstrumentationOptions options) - : base(options) - { - } + return new SamplingResult(this.samplingDecision, this.attributes); + } + } - public override void OnEventWritten(string name, object payload) - { - base.OnEventWritten(name, payload); + private class TestHttpInListener : HttpInListener + { + public Action OnEventWrittenCallback; - this.OnEventWrittenCallback?.Invoke(name, payload); - } + public TestHttpInListener(AspNetCoreInstrumentationOptions options) + : base(options) + { } - private class TestNullHostActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + public override void OnEventWritten(string name, object payload) { - private ActivitySource activitySource; - private Activity activity; - private string activityName; + base.OnEventWritten(name, payload); - public TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) - { - this.activitySource = new ActivitySource(activitySourceName); - this.activityName = activityName; - } + this.OnEventWrittenCallback?.Invoke(name, payload); + } + } - public override void PreProcess(HttpContext context) - { - // Setting the host activity i.e. activity started by asp.net core - // to null here will have no impact on middleware activity. - // This also means that asp.net core activity will not be found - // during OnEventWritten event. - Activity.Current = null; - this.activity = this.activitySource.StartActivity(this.activityName); - } + private class TestNullHostActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + { + private ActivitySource activitySource; + private Activity activity; + private string activityName; - public override void PostProcess(HttpContext context) - { - this.activity?.Stop(); - } + public TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) + { + this.activitySource = new ActivitySource(activitySourceName); + this.activityName = activityName; } - private class TestActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + public override void PreProcess(HttpContext context) { - private ActivitySource activitySource; - private Activity activity; - private string activityName; + // Setting the host activity i.e. activity started by asp.net core + // to null here will have no impact on middleware activity. + // This also means that asp.net core activity will not be found + // during OnEventWritten event. + Activity.Current = null; + this.activity = this.activitySource.StartActivity(this.activityName); + } - public TestActivityMiddlewareImpl(string activitySourceName, string activityName) - { - this.activitySource = new ActivitySource(activitySourceName); - this.activityName = activityName; - } + public override void PostProcess(HttpContext context) + { + this.activity?.Stop(); + } + } - public override void PreProcess(HttpContext context) - { - this.activity = this.activitySource.StartActivity(this.activityName); - } + private class TestActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + { + private ActivitySource activitySource; + private Activity activity; + private string activityName; - public override void PostProcess(HttpContext context) - { - this.activity?.Stop(); - } + public TestActivityMiddlewareImpl(string activitySourceName, string activityName) + { + this.activitySource = new ActivitySource(activitySourceName); + this.activityName = activityName; + } + + public override void PreProcess(HttpContext context) + { + this.activity = this.activitySource.StartActivity(this.activityName); + } + + public override void PostProcess(HttpContext context) + { + this.activity?.Stop(); } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index 3631ca9a1ef..86e31d863ed 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -22,78 +22,77 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class DependencyInjectionConfigTests + : IClassFixture> { - public class DependencyInjectionConfigTests - : IClassFixture> + private readonly WebApplicationFactory factory; + + public DependencyInjectionConfigTests(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public DependencyInjectionConfigTests(WebApplicationFactory factory) - { - this.factory = factory; - } + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void TestTracingOptionsDIConfig(string name) + { + name ??= Options.DefaultName; - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void TestTracingOptionsDIConfig(string name) + bool optionsPickedFromDI = false; + void ConfigureTestServices(IServiceCollection services) { - name ??= Options.DefaultName; + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); - bool optionsPickedFromDI = false; - void ConfigureTestServices(IServiceCollection services) + services.Configure(name, options => { - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); - - services.Configure(name, options => - { - optionsPickedFromDI = true; - }); - } - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - builder.ConfigureTestServices(ConfigureTestServices)) - .CreateClient()) - { - } - - Assert.True(optionsPickedFromDI); + optionsPickedFromDI = true; + }); } - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void TestMetricsOptionsDIConfig(string name) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(ConfigureTestServices)) + .CreateClient()) { - name ??= Options.DefaultName; + } - bool optionsPickedFromDI = false; - void ConfigureTestServices(IServiceCollection services) - { - services.AddOpenTelemetry() - .WithMetrics(builder => builder - .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); + Assert.True(optionsPickedFromDI); + } - services.Configure(name, options => - { - optionsPickedFromDI = true; - }); - } + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void TestMetricsOptionsDIConfig(string name) + { + name ??= Options.DefaultName; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - builder.ConfigureTestServices(ConfigureTestServices)) - .CreateClient()) + bool optionsPickedFromDI = false; + void ConfigureTestServices(IServiceCollection services) + { + services.AddOpenTelemetry() + .WithMetrics(builder => builder + .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); + + services.Configure(name, options => { - } + optionsPickedFromDI = true; + }); + } - Assert.True(optionsPickedFromDI); + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(ConfigureTestServices)) + .CreateClient()) + { } + + Assert.True(optionsPickedFromDI); } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs index e347a9b63d0..749b1b897e8 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs @@ -18,14 +18,13 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_AspNetCoreInstrumentationEventSource() { - [Fact] - public void EventSourceTest_AspNetCoreInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(AspNetCoreInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(AspNetCoreInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs index 74c0cced5d2..c3b16cf24af 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs @@ -21,69 +21,68 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public sealed class InProcServerTests : IDisposable { - public sealed class InProcServerTests : IDisposable + private TracerProvider tracerProvider; + private WebApplication app; + private HttpClient client; + private List exportedItems; + + public InProcServerTests() { - private TracerProvider tracerProvider; - private WebApplication app; - private HttpClient client; - private List exportedItems; + this.exportedItems = new List(); + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); - public InProcServerTests() - { - this.exportedItems = new List(); - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedItems).Build(); + app.MapGet("/", () => "Hello World!"); + app.RunAsync(); - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(this.exportedItems).Build(); - app.MapGet("/", () => "Hello World!"); - app.RunAsync(); + this.app = app; + this.client = new HttpClient(); + } - this.app = app; - this.client = new HttpClient(); - } + [Fact] + public async void ExampleTest() + { + var res = await this.client.GetStringAsync("http://localhost:5000").ConfigureAwait(false); + Assert.NotNull(res); - [Fact] - public async void ExampleTest() + this.tracerProvider.ForceFlush(); + for (var i = 0; i < 10; i++) { - var res = await this.client.GetStringAsync("http://localhost:5000").ConfigureAwait(false); - Assert.NotNull(res); - - this.tracerProvider.ForceFlush(); - for (var i = 0; i < 10; i++) + if (this.exportedItems.Count > 0) { - if (this.exportedItems.Count > 0) - { - 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)).ConfigureAwait(false); + break; } - var activity = this.exportedItems[0]; - Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal(5000, activity.GetTagValue(SemanticConventions.AttributeNetHostPort)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor)); - Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - Assert.True(activity.Status == ActivityStatusCode.Unset); - Assert.True(activity.StatusDescription is null); + // 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); } - public async void Dispose() - { - this.tracerProvider.Dispose(); - this.client.Dispose(); - await this.app.DisposeAsync().ConfigureAwait(false); - } + var activity = this.exportedItems[0]; + Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); + Assert.Equal(5000, activity.GetTagValue(SemanticConventions.AttributeNetHostPort)); + Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); + Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor)); + Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + Assert.True(activity.Status == ActivityStatusCode.Unset); + Assert.True(activity.StatusDescription is null); + } + + public async void Dispose() + { + this.tracerProvider.Dispose(); + this.client.Dispose(); + await this.app.DisposeAsync().ConfigureAwait(false); } } #endif diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index af9e63e2677..58688301760 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -26,172 +26,171 @@ using TestApp.AspNetCore; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class IncomingRequestsCollectionsIsAccordingToTheSpecTests + : IClassFixture> { - public class IncomingRequestsCollectionsIsAccordingToTheSpecTests - : IClassFixture> + private readonly WebApplicationFactory factory; + + public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactory 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_Old( + string urlPath, + string query, + string userAgent, + int statusCode, + string reasonPhrase, + bool recordException = false) + { + try { - this.factory = factory; - } + Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "none"); - [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_Old( - string urlPath, - string query, - string userAgent, - int statusCode, - string reasonPhrase, - bool recordException = false) - { - try - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "none"); + var exportedItems = new List(); - var exportedItems = new List(); - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try + builder.ConfigureTestServices((IServiceCollection services) => { - 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).ConfigureAwait(false); - } - catch (Exception) + services.AddSingleton(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)) { - // 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)).ConfigureAwait(false); + path += query; } - } - 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) - { - Assert.Equal(ActivityStatusCode.Error, activity.Status); + using var response = await client.GetAsync(path).ConfigureAwait(false); } - else + catch (Exception) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + // ignore errors } - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - if (!urlPath.EndsWith("exception")) + for (var i = 0; i < 10; i++) { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else - { - Assert.Equal("exception description", activity.StatusDescription); - } + if (exportedItems.Count == 1) + { + break; + } - if (recordException) - { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); + // 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); } + } - ValidateTagValue(activity, SemanticConventions.AttributeHttpUserAgent, userAgent); + Assert.Single(exportedItems); + var activity = exportedItems[0]; - activity.Dispose(); + 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) + { + Assert.Equal(ActivityStatusCode.Error, activity.Status); } - finally + else { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - } - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) - { - if (string.IsNullOrEmpty(expectedValue)) + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + if (!urlPath.EndsWith("exception")) { - Assert.Null(activity.GetTagValue(attribute)); + Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); } else { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + Assert.Equal("exception description", activity.StatusDescription); } + + if (recordException) + { + Assert.Single(activity.Events); + Assert.Equal("exception", activity.Events.First().Name); + } + + ValidateTagValue(activity, SemanticConventions.AttributeHttpUserAgent, userAgent); + + activity.Dispose(); } + finally + { + Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + } + } - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) + { + if (string.IsNullOrEmpty(expectedValue)) + { + Assert.Null(activity.GetTagValue(attribute)); + } + else { - private readonly int statusCode; - private readonly string reasonPhrase; + Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + } + } - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } + public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + { + private readonly int statusCode; + private readonly string reasonPhrase; - public override async Task ProcessAsync(HttpContext context) - { - context.Response.StatusCode = this.statusCode; - context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); + public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) + { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } + public override async Task ProcessAsync(HttpContext context) + { + context.Response.StatusCode = this.statusCode; + context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; + await context.Response.WriteAsync("empty").ConfigureAwait(false); - return false; + if (context.Request.Path.Value.EndsWith("exception")) + { + throw new Exception("exception description"); } + + return false; } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs index 01e3cb8ce93..622498876cd 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs @@ -26,179 +26,178 @@ using TestApp.AspNetCore; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe + : IClassFixture> { - public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe - : IClassFixture> + private readonly WebApplicationFactory factory; + + public IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe(WebApplicationFactory 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 { - this.factory = factory; - } + Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup"); - [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(); - var exportedItems = new List(); - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try + builder.ConfigureTestServices((IServiceCollection services) => { - 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).ConfigureAwait(false); - } - catch (Exception) + services.AddSingleton(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)) { - // 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)).ConfigureAwait(false); + path += query; } - } - 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); + using var response = await client.GetAsync(path).ConfigureAwait(false); } - else + catch (Exception) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + // ignore errors } - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - if (!urlPath.EndsWith("exception")) - { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else + for (var i = 0; i < 10; i++) { - Assert.Equal("exception description", activity.StatusDescription); - } + if (exportedItems.Count == 1) + { + break; + } - if (recordException) - { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); + // 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); } + } - ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); - - activity.Dispose(); + 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); } - finally + else { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - } - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) - { - if (string.IsNullOrEmpty(expectedValue)) + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + if (!urlPath.EndsWith("exception")) { - Assert.Null(activity.GetTagValue(attribute)); + Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); } else { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + Assert.Equal("exception description", 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); + } + } - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) + { + if (string.IsNullOrEmpty(expectedValue)) { - private readonly int statusCode; - private readonly string reasonPhrase; + Assert.Null(activity.GetTagValue(attribute)); + } + else + { + Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + } + } - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } + public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + { + private readonly int statusCode; + private readonly string reasonPhrase; - public override async Task ProcessAsync(HttpContext context) - { - context.Response.StatusCode = this.statusCode; - context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); + public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) + { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } + public override async Task ProcessAsync(HttpContext context) + { + context.Response.StatusCode = this.statusCode; + context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; + await context.Response.WriteAsync("empty").ConfigureAwait(false); - return false; + if (context.Request.Path.Value.EndsWith("exception")) + { + throw new Exception("exception description"); } + + return false; } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs index 47895a14754..ee51eba3c47 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs @@ -26,172 +26,171 @@ using TestApp.AspNetCore; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_New + : IClassFixture> { - public class IncomingRequestsCollectionsIsAccordingToTheSpecTests_New - : IClassFixture> + private readonly WebApplicationFactory factory; + + public IncomingRequestsCollectionsIsAccordingToTheSpecTests_New(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public IncomingRequestsCollectionsIsAccordingToTheSpecTests_New(WebApplicationFactory 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_New( + string urlPath, + string query, + string userAgent, + int statusCode, + string reasonPhrase, + bool recordException = false) + { + try { - this.factory = factory; - } + Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "http"); - [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_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(); - var exportedItems = new List(); - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(options => options.RecordException = recordException) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try + builder.ConfigureTestServices((IServiceCollection services) => { - 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).ConfigureAwait(false); - } - catch (Exception) + services.AddSingleton(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)) { - // 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)).ConfigureAwait(false); + path += query; } - } - 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); + using var response = await client.GetAsync(path).ConfigureAwait(false); } - else + catch (Exception) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + // ignore errors } - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - if (!urlPath.EndsWith("exception")) + for (var i = 0; i < 10; i++) { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else - { - Assert.Equal("exception description", activity.StatusDescription); - } + if (exportedItems.Count == 1) + { + break; + } - if (recordException) - { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); + // 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); } + } - ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); + Assert.Single(exportedItems); + var activity = exportedItems[0]; - activity.Dispose(); + 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); } - finally + else { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - } - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) - { - if (string.IsNullOrEmpty(expectedValue)) + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + if (!urlPath.EndsWith("exception")) { - Assert.Null(activity.GetTagValue(attribute)); + Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); } else { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + Assert.Equal("exception description", 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); + } + } - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) + { + if (string.IsNullOrEmpty(expectedValue)) + { + Assert.Null(activity.GetTagValue(attribute)); + } + else { - private readonly int statusCode; - private readonly string reasonPhrase; + Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + } + } - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } + public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + { + private readonly int statusCode; + private readonly string reasonPhrase; - public override async Task ProcessAsync(HttpContext context) - { - context.Response.StatusCode = this.statusCode; - context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; - await context.Response.WriteAsync("empty").ConfigureAwait(false); + public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) + { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } + public override async Task ProcessAsync(HttpContext context) + { + context.Response.StatusCode = this.statusCode; + context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; + await context.Response.WriteAsync("empty").ConfigureAwait(false); - return false; + if (context.Request.Path.Value.EndsWith("exception")) + { + throw new Exception("exception description"); } + + return false; } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index ac7f6610e24..607472c83a4 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -25,226 +25,225 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class MetricTests + : IClassFixture>, IDisposable { - public class MetricTests - : IClassFixture>, IDisposable + private const int StandardTagsCount = 6; + + private readonly WebApplicationFactory factory; + private MeterProvider meterProvider; + + public MetricTests(WebApplicationFactory factory) { - private const int StandardTagsCount = 6; + this.factory = factory; + } - private readonly WebApplicationFactory factory; - private MeterProvider meterProvider; + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() + { + MeterProviderBuilder builder = null; + Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + } - public MetricTests(WebApplicationFactory factory) - { - this.factory = factory; - } + [Fact] + public async Task RequestMetricIsCaptured() + { + var metricItems = new List(); - [Fact] - public void AddAspNetCoreInstrumentation_BadArgs() - { - MeterProviderBuilder builder = null; - Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); - } + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); - [Fact] - public async Task RequestMetricIsCaptured() + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) { - var metricItems = new List(); + using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) - .Build(); + response1.EnsureSuccessStatusCode(); + response2.EnsureSuccessStatusCode(); + } - using (var client = this.factory - .WithWebHostBuilder(builder => - { - 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); + // 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); - response1.EnsureSuccessStatusCode(); - response2.EnsureSuccessStatusCode(); - } + this.meterProvider.Dispose(); - // 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); + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.duration") + .ToArray(); - this.meterProvider.Dispose(); + var metric = Assert.Single(requestMetrics); + var metricPoints = GetMetricPoints(metric); + Assert.Equal(2, metricPoints.Count); - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); + AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values"); + AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}"); + } - var metric = Assert.Single(requestMetrics); - var metricPoints = GetMetricPoints(metric); - Assert.Equal(2, metricPoints.Count); + [Fact] + public async Task MetricNotCollectedWhenFilterIsApplied() + { + var metricItems = new List(); - AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values"); - AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}"); + void ConfigureTestServices(IServiceCollection services) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation(opt => opt.Filter = (name, ctx) => ctx.Request.Path != "/api/values/2") + .AddInMemoryExporter(metricItems) + .Build(); } - [Fact] - public async Task MetricNotCollectedWhenFilterIsApplied() + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) { - var metricItems = new List(); - - void ConfigureTestServices(IServiceCollection services) - { - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(opt => opt.Filter = (name, ctx) => ctx.Request.Path != "/api/values/2") - .AddInMemoryExporter(metricItems) - .Build(); - } - - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - 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); - 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(); + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.duration") + .ToArray(); - var metric = Assert.Single(requestMetrics); + var metric = Assert.Single(requestMetrics); - // Assert single because we filtered out one route - var metricPoint = Assert.Single(GetMetricPoints(metric)); - AssertMetricPoint(metricPoint); - } + // Assert single because we filtered out one route + var metricPoint = Assert.Single(GetMetricPoints(metric)); + AssertMetricPoint(metricPoint); + } - [Fact] - public async Task MetricEnrichedWithCustomTags() + [Fact] + public async Task MetricEnrichedWithCustomTags() + { + var tagsToAdd = new KeyValuePair[] { - var tagsToAdd = new KeyValuePair[] - { - new("custom_tag_1", 1), - new("custom_tag_2", "one"), - }; + new("custom_tag_1", 1), + new("custom_tag_2", "one"), + }; - var metricItems = new List(); + var metricItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(opt => opt.Enrich = (string _, HttpContext _, ref TagList tags) => + void ConfigureTestServices(IServiceCollection services) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation(opt => opt.Enrich = (string _, HttpContext _, ref TagList tags) => + { + foreach (var keyValuePair in tagsToAdd) { - foreach (var keyValuePair in tagsToAdd) - { - tags.Add(keyValuePair); - } - }) - .AddInMemoryExporter(metricItems) - .Build(); - } - - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - } + tags.Add(keyValuePair); + } + }) + .AddInMemoryExporter(metricItems) + .Build(); + } - // 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); + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } - this.meterProvider.Dispose(); + // 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); - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); + this.meterProvider.Dispose(); - var metric = Assert.Single(requestMetrics); - var metricPoint = Assert.Single(GetMetricPoints(metric)); + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.duration") + .ToArray(); - var tags = AssertMetricPoint(metricPoint, expectedTagsCount: StandardTagsCount + 2); + var metric = Assert.Single(requestMetrics); + var metricPoint = Assert.Single(GetMetricPoints(metric)); - Assert.Contains(tagsToAdd[0], tags); - Assert.Contains(tagsToAdd[1], tags); - } + var tags = AssertMetricPoint(metricPoint, expectedTagsCount: StandardTagsCount + 2); - public void Dispose() - { - this.meterProvider?.Dispose(); - GC.SuppressFinalize(this); - } + Assert.Contains(tagsToAdd[0], tags); + Assert.Contains(tagsToAdd[1], tags); + } - private static List GetMetricPoints(Metric metric) - { - Assert.NotNull(metric); - Assert.True(metric.MetricType == MetricType.Histogram); - var metricPoints = new List(); - foreach (var p in metric.GetMetricPoints()) - { - metricPoints.Add(p); - } + public void Dispose() + { + this.meterProvider?.Dispose(); + GC.SuppressFinalize(this); + } - return metricPoints; + private static List GetMetricPoints(Metric metric) + { + Assert.NotNull(metric); + Assert.True(metric.MetricType == MetricType.Histogram); + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) + { + metricPoints.Add(p); } - private static KeyValuePair[] AssertMetricPoint( - MetricPoint metricPoint, - string expectedRoute = "api/Values", - int expectedTagsCount = StandardTagsCount) - { - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); + return metricPoints; + } - Assert.Equal(1L, count); - Assert.True(sum > 0); + private static KeyValuePair[] AssertMetricPoint( + MetricPoint metricPoint, + string expectedRoute = "api/Values", + int expectedTagsCount = StandardTagsCount) + { + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); - var attributes = new KeyValuePair[metricPoint.Tags.Count]; - int i = 0; - foreach (var tag in metricPoint.Tags) - { - attributes[i++] = tag; - } - - var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, "GET"); - var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); - var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, 200); - var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "1.1"); - var host = new KeyValuePair(SemanticConventions.AttributeNetHostName, "localhost"); - var route = new KeyValuePair(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); - Assert.Equal(expectedTagsCount, attributes.Length); - - return attributes; + Assert.Equal(1L, count); + Assert.True(sum > 0); + + var attributes = new KeyValuePair[metricPoint.Tags.Count]; + int i = 0; + foreach (var tag in metricPoint.Tags) + { + attributes[i++] = tag; } + + var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, "GET"); + var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); + var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, 200); + var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "1.1"); + var host = new KeyValuePair(SemanticConventions.AttributeNetHostName, "localhost"); + var route = new KeyValuePair(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); + Assert.Equal(expectedTagsCount, attributes.Length); + + return attributes; } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs index 3139e55e105..411a53e8742 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs @@ -18,14 +18,13 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_GrpcInstrumentationEventSource() { - [Fact] - public void EventSourceTest_GrpcInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(GrpcInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(GrpcInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs index 85e7e11967a..bd3f65ff6e6 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs @@ -15,90 +15,88 @@ // #if !NETFRAMEWORK -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class GrpcServer : IDisposable + where TService : class { - public class GrpcServer : IDisposable - where TService : class - { - private static readonly Random GlobalRandom = new(); + private static readonly Random GlobalRandom = new(); - private readonly IHost host; + private readonly IHost host; - public GrpcServer() - { - // Allows gRPC client to call insecure gRPC services - // https://docs.microsoft.com/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + public GrpcServer() + { + // Allows gRPC client to call insecure gRPC services + // https://docs.microsoft.com/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - this.Port = 0; + this.Port = 0; - var retryCount = 5; - while (retryCount > 0) + var retryCount = 5; + while (retryCount > 0) + { + try + { + this.Port = GlobalRandom.Next(2000, 5000); + this.host = this.CreateServer(); + this.host.StartAsync().GetAwaiter().GetResult(); + break; + } + catch (IOException) { - try - { - this.Port = GlobalRandom.Next(2000, 5000); - this.host = this.CreateServer(); - this.host.StartAsync().GetAwaiter().GetResult(); - break; - } - catch (System.IO.IOException) - { - retryCount--; - this.host.Dispose(); - } + retryCount--; + this.host.Dispose(); } } + } - public int Port { get; } + public int Port { get; } - public void Dispose() - { - this.host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); - this.host.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + this.host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); + this.host.Dispose(); + GC.SuppressFinalize(this); + } - private IHost CreateServer() - { - var hostBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder - .ConfigureKestrel(options => - { - // Setup a HTTP/2 endpoint without TLS. - options.ListenLocalhost(this.Port, o => o.Protocols = HttpProtocols.Http2); - }) - .UseStartup(); - }); + private IHost CreateServer() + { + var hostBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .ConfigureKestrel(options => + { + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(this.Port, o => o.Protocols = HttpProtocols.Http2); + }) + .UseStartup(); + }); - return hostBuilder.Build(); + return hostBuilder.Build(); + } + + private class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); } - private class Startup + public void Configure(IApplicationBuilder app) { - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); - } + app.UseRouting(); - public void Configure(IApplicationBuilder app) + app.UseEndpoints(endpoints => { - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - }); - } + endpoints.MapGrpcService(); + }); } } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs index f4d753044d0..eb80d0c804f 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs @@ -18,62 +18,61 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class GrpcTagHelperTests { - public class GrpcTagHelperTests + [Fact] + public void GrpcTagHelper_GetGrpcMethodFromActivity() { - [Fact] - public void GrpcTagHelper_GetGrpcMethodFromActivity() - { - var grpcMethod = "/some.service/somemethod"; - using var activity = new Activity("operationName"); - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, grpcMethod); + var grpcMethod = "/some.service/somemethod"; + using var activity = new Activity("operationName"); + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, grpcMethod); - var result = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + var result = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - Assert.Equal(grpcMethod, result); - } + Assert.Equal(grpcMethod, result); + } - [Theory] - [InlineData("Package.Service/Method", true, "Package.Service", "Method")] - [InlineData("/Package.Service/Method", true, "Package.Service", "Method")] - [InlineData("/ServiceWithNoPackage/Method", true, "ServiceWithNoPackage", "Method")] - [InlineData("/Some.Package.Service/Method", true, "Some.Package.Service", "Method")] - [InlineData("Invalid", false, "", "")] - public void GrpcTagHelper_TryParseRpcServiceAndRpcMethod(string grpcMethod, bool isSuccess, string expectedRpcService, string expectedRpcMethod) - { - var success = GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod); + [Theory] + [InlineData("Package.Service/Method", true, "Package.Service", "Method")] + [InlineData("/Package.Service/Method", true, "Package.Service", "Method")] + [InlineData("/ServiceWithNoPackage/Method", true, "ServiceWithNoPackage", "Method")] + [InlineData("/Some.Package.Service/Method", true, "Some.Package.Service", "Method")] + [InlineData("Invalid", false, "", "")] + public void GrpcTagHelper_TryParseRpcServiceAndRpcMethod(string grpcMethod, bool isSuccess, string expectedRpcService, string expectedRpcMethod) + { + var success = GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod); - Assert.Equal(isSuccess, success); - Assert.Equal(expectedRpcService, rpcService); - Assert.Equal(expectedRpcMethod, rpcMethod); - } + Assert.Equal(isSuccess, success); + Assert.Equal(expectedRpcService, rpcService); + Assert.Equal(expectedRpcMethod, rpcMethod); + } - [Fact] - public void GrpcTagHelper_GetGrpcStatusCodeFromActivity() - { - using var activity = new Activity("operationName"); - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, "0"); + [Fact] + public void GrpcTagHelper_GetGrpcStatusCodeFromActivity() + { + using var activity = new Activity("operationName"); + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, "0"); - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - Assert.True(validConversion); + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + Assert.True(validConversion); - var statusCode = GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status); - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + var statusCode = GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); - Assert.Equal(ActivityStatusCode.Unset, statusCode); - Assert.Equal(status, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + Assert.Equal(ActivityStatusCode.Unset, statusCode); + Assert.Equal(status, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } - [Fact] - public void GrpcTagHelper_GetGrpcStatusCodeFromEmptyActivity() - { - using var activity = new Activity("operationName"); + [Fact] + public void GrpcTagHelper_GetGrpcStatusCodeFromEmptyActivity() + { + using var activity = new Activity("operationName"); - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - Assert.False(validConversion); - Assert.Equal(-1, status); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + Assert.False(validConversion); + Assert.Equal(-1, status); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs index e0f3f76196b..cdd6eb37eaf 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs @@ -19,70 +19,69 @@ using Google.Protobuf; using Grpc.Net.Compression; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class ClientTestHelpers { - internal static class ClientTestHelpers + public static HttpClient CreateTestClient(Func> sendAsync, Uri baseAddress = null) { - public static HttpClient CreateTestClient(Func> sendAsync, Uri baseAddress = null) - { - var handler = TestHttpMessageHandler.Create(sendAsync); - var httpClient = new HttpClient(handler); - httpClient.BaseAddress = baseAddress ?? new Uri("https://localhost"); + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new HttpClient(handler); + httpClient.BaseAddress = baseAddress ?? new Uri("https://localhost"); - return httpClient; - } + return httpClient; + } - public static Task CreateResponseContent(TResponse response, ICompressionProvider compressionProvider = null) - where TResponse : IMessage - { - return CreateResponseContentCore(new[] { response }, compressionProvider); - } + public static Task CreateResponseContent(TResponse response, ICompressionProvider compressionProvider = null) + where TResponse : IMessage + { + return CreateResponseContentCore(new[] { response }, compressionProvider); + } - public static async Task WriteResponseAsync(Stream ms, TResponse response, ICompressionProvider compressionProvider) - where TResponse : IMessage - { - var compress = false; + public static async Task WriteResponseAsync(Stream ms, TResponse response, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var compress = false; - byte[] data; - if (compressionProvider != null) - { - compress = true; + byte[] data; + if (compressionProvider != null) + { + compress = true; - var output = new MemoryStream(); - var compressionStream = compressionProvider.CreateCompressionStream(output, System.IO.Compression.CompressionLevel.Fastest); - var compressedData = response.ToByteArray(); + var output = new MemoryStream(); + var compressionStream = compressionProvider.CreateCompressionStream(output, System.IO.Compression.CompressionLevel.Fastest); + var compressedData = response.ToByteArray(); - compressionStream.Write(compressedData, 0, compressedData.Length); - compressionStream.Flush(); - compressionStream.Dispose(); - data = output.ToArray(); - } - else - { - data = response.ToByteArray(); - } + compressionStream.Write(compressedData, 0, compressedData.Length); + compressionStream.Flush(); + compressionStream.Dispose(); + data = output.ToArray(); + } + else + { + data = response.ToByteArray(); + } - await ResponseUtils.WriteHeaderAsync(ms, data.Length, compress, CancellationToken.None).ConfigureAwait(false); + await ResponseUtils.WriteHeaderAsync(ms, data.Length, compress, CancellationToken.None).ConfigureAwait(false); #if NET5_0_OR_GREATER - await ms.WriteAsync(data).ConfigureAwait(false); + await ms.WriteAsync(data).ConfigureAwait(false); #else - await ms.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + await ms.WriteAsync(data, 0, data.Length).ConfigureAwait(false); #endif - } + } - private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) - where TResponse : IMessage + private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) { - var ms = new MemoryStream(); - foreach (var response in responses) - { - await WriteResponseAsync(ms, response, compressionProvider).ConfigureAwait(false); - } - - ms.Seek(0, SeekOrigin.Begin); - var streamContent = new StreamContent(ms); - streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); - return streamContent; + await WriteResponseAsync(ms, response, compressionProvider).ConfigureAwait(false); } + + ms.Seek(0, SeekOrigin.Begin); + var streamContent = new StreamContent(ms); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); + return streamContent; } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs index 244f5df0281..93d91ab35c9 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs @@ -20,69 +20,68 @@ using System.Net.Http; using System.Net.Http.Headers; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class ResponseUtils { - internal static class ResponseUtils + internal const string MessageEncodingHeader = "grpc-encoding"; + internal const string IdentityGrpcEncoding = "identity"; + internal const string StatusTrailer = "grpc-status"; + internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + internal static readonly Version ProtocolVersion = new Version(2, 0); + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static HttpResponseMessage CreateResponse( + HttpStatusCode statusCode, + HttpContent payload, + global::Grpc.Core.StatusCode? grpcStatusCode = global::Grpc.Core.StatusCode.OK) { - internal const string MessageEncodingHeader = "grpc-encoding"; - internal const string IdentityGrpcEncoding = "identity"; - internal const string StatusTrailer = "grpc-status"; - internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); - internal static readonly Version ProtocolVersion = new Version(2, 0); - private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" - private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + payload.Headers.ContentType = GrpcContentTypeHeaderValue; - public static HttpResponseMessage CreateResponse( - HttpStatusCode statusCode, - HttpContent payload, - global::Grpc.Core.StatusCode? grpcStatusCode = global::Grpc.Core.StatusCode.OK) + var message = new HttpResponseMessage(statusCode) { - payload.Headers.ContentType = GrpcContentTypeHeaderValue; - - var message = new HttpResponseMessage(statusCode) - { - Content = payload, - Version = ProtocolVersion, - }; + Content = payload, + Version = ProtocolVersion, + }; - message.RequestMessage = new HttpRequestMessage(); + message.RequestMessage = new HttpRequestMessage(); #if NETFRAMEWORK - message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); + message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); #endif - message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); + message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); - if (grpcStatusCode != null) - { - message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); - } - - return message; + if (grpcStatusCode != null) + { + message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); } - public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) - { - var headerData = new byte[HeaderSize]; + return message; + } - // Compression flag - headerData[0] = compress ? (byte)1 : (byte)0; + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; - // Message length - EncodeMessageLength(length, headerData.AsSpan(1)); + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; - return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); - } + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); - private static void EncodeMessageLength(int messageLength, Span destination) - { - Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } - BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); - } + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } #if NETFRAMEWORK - private class ResponseTrailers : HttpHeaders - { - } -#endif + private class ResponseTrailers : HttpHeaders + { } +#endif } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs index 0681fe7f6c4..6fb8ff727e8 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs @@ -16,38 +16,37 @@ using System.Net.Http; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +public class TestHttpMessageHandler : HttpMessageHandler { - public class TestHttpMessageHandler : HttpMessageHandler + private readonly Func> sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) { - private readonly Func> sendAsync; + this.sendAsync = sendAsync; + } - public TestHttpMessageHandler(Func> sendAsync) - { - this.sendAsync = sendAsync; - } + public static TestHttpMessageHandler Create(Func> sendAsync) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public static TestHttpMessageHandler Create(Func> sendAsync) + return new TestHttpMessageHandler(async (request, cancellationToken) => { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - return new TestHttpMessageHandler(async (request, cancellationToken) => - { - using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); - var result = await Task.WhenAny(sendAsync(request), tcs.Task).ConfigureAwait(false); - return await result.ConfigureAwait(false); - }); - } + var result = await Task.WhenAny(sendAsync(request), tcs.Task).ConfigureAwait(false); + return await result.ConfigureAwait(false); + }); + } - public static TestHttpMessageHandler Create(Func> sendAsync) - { - return new TestHttpMessageHandler(sendAsync); - } + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return this.sendAsync(request, cancellationToken); - } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.sendAsync(request, cancellationToken); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs index 429f1d1b581..85e016566fc 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs @@ -17,43 +17,42 @@ using System.Net.Http; using System.Net.Http.Headers; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class TrailingHeadersHelpers { - internal static class TrailingHeadersHelpers - { - public static readonly string ResponseTrailersKey = "__ResponseTrailers"; + public static readonly string ResponseTrailersKey = "__ResponseTrailers"; - public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) - { + public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) + { #if !NETFRAMEWORK - return responseMessage.TrailingHeaders; + return responseMessage.TrailingHeaders; #else - if (responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers) && - headers is HttpHeaders httpHeaders) - { - return httpHeaders; - } + if (responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers) && + headers is HttpHeaders httpHeaders) + { + return httpHeaders; + } - // App targets .NET Standard 2.0 and the handler hasn't set trailers - // in RequestMessage.Properties with known key. Return empty collection. - // Client call will likely fail because it is unable to get a grpc-status. - return ResponseTrailers.Empty; + // App targets .NET Standard 2.0 and the handler hasn't set trailers + // in RequestMessage.Properties with known key. Return empty collection. + // Client call will likely fail because it is unable to get a grpc-status. + return ResponseTrailers.Empty; #endif - } + } #if NETFRAMEWORK - public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + { + if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) { - if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) - { - responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); - } + responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); } + } - private class ResponseTrailers : HttpHeaders - { - public static readonly ResponseTrailers Empty = new ResponseTrailers(); - } -#endif + private class ResponseTrailers : HttpHeaders + { + public static readonly ResponseTrailers Empty = new ResponseTrailers(); } +#endif } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs index ca19fdc6646..2598106a702 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs @@ -30,656 +30,655 @@ using Xunit; using Status = OpenTelemetry.Trace.Status; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public partial class GrpcTests { - public partial class GrpcTests + [Theory] + [InlineData("http://localhost")] + [InlineData("http://localhost", false)] + [InlineData("http://127.0.0.1")] + [InlineData("http://127.0.0.1", false)] + [InlineData("http://[::1]")] + [InlineData("http://[::1]", false)] + public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool shouldEnrich = true) { - [Theory] - [InlineData("http://localhost")] - [InlineData("http://localhost", false)] - [InlineData("http://127.0.0.1")] - [InlineData("http://127.0.0.1", false)] - [InlineData("http://[::1]")] - [InlineData("http://[::1]", false)] - public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool shouldEnrich = true) - { - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; - var uri = new Uri($"{baseAddress}:1234"); - var uriHostNameType = Uri.CheckHostName(uri.Host); + var uri = new Uri($"{baseAddress}:1234"); + var uriHostNameType = Uri.CheckHostName(uri.Host); - using var httpClient = ClientTestHelpers.CreateTestClient(async request => - { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); - var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); - response.TrailingHeaders().Add("grpc-message", "value"); - return response; - }); + using var httpClient = ClientTestHelpers.CreateTestClient(async request => + { + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); + response.TrailingHeaders().Add("grpc-message", "value"); + return response; + }); - var processor = new Mock>(); + var processor = new Mock>(); - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(options => + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) - { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - } - }) - .AddProcessor(processor.Object) - .Build()) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + } + }) + .AddProcessor(processor.Object) + .Build()) + { + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { - var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions - { - HttpClient = httpClient, - }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } + HttpClient = httpClient, + }); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; + Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. + var activity = (Activity)processor.Invocations[2].Arguments[0]; - ValidateGrpcActivity(activity); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); + ValidateGrpcActivity(activity); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); - Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); + Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); + Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); + Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); + Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } - - Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - Assert.Equal(Status.Unset, activity.GetStatus()); - - // Tags added by the library then removed from the instrumentation - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - - if (shouldEnrich) - { - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); } - - [Theory] - [InlineData("http://localhost")] - [InlineData("http://localhost", false)] - [InlineData("http://127.0.0.1")] - [InlineData("http://127.0.0.1", false)] - [InlineData("http://[::1]")] - [InlineData("http://[::1]", false)] - public void GrpcClientCallsAreCollectedSuccessfully_New(string baseAddress, bool shouldEnrich = true) + else { - KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http") }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(config) - .Build(); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + } - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; + Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + Assert.Equal(Status.Unset, activity.GetStatus()); - var uri = new Uri($"{baseAddress}:1234"); - var uriHostNameType = Uri.CheckHostName(uri.Host); + // Tags added by the library then removed from the instrumentation + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - using var httpClient = ClientTestHelpers.CreateTestClient(async request => - { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); - var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); - response.TrailingHeaders().Add("grpc-message", "value"); - return response; - }); + if (shouldEnrich) + { + Assert.True(enrichWithHttpRequestMessageCalled); + Assert.True(enrichWithHttpResponseMessageCalled); + } + } - var processor = new Mock>(); + [Theory] + [InlineData("http://localhost")] + [InlineData("http://localhost", false)] + [InlineData("http://127.0.0.1")] + [InlineData("http://127.0.0.1", false)] + [InlineData("http://[::1]")] + [InlineData("http://[::1]", false)] + public void GrpcClientCallsAreCollectedSuccessfully_New(string baseAddress, bool shouldEnrich = true) + { + KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http") }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddGrpcClientInstrumentation(options => + var uri = new Uri($"{baseAddress}:1234"); + var uriHostNameType = Uri.CheckHostName(uri.Host); + + using var httpClient = ClientTestHelpers.CreateTestClient(async request => + { + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); + response.TrailingHeaders().Add("grpc-message", "value"); + return response; + }); + + var processor = new Mock>(); + + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) - { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - } - }) - .AddProcessor(processor.Object) - .Build()) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + } + }) + .AddProcessor(processor.Object) + .Build()) + { + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { - var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions - { - HttpClient = httpClient, - }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } - - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; - - ValidateGrpcActivity(activity); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); - - Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); + HttpClient = httpClient, + }); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - } + Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. + var activity = (Activity)processor.Invocations[2].Arguments[0]; - Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); - Assert.Equal(Status.Unset, activity.GetStatus()); + ValidateGrpcActivity(activity); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); - // Tags added by the library then removed from the instrumentation - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); + Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); + Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); + Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - if (shouldEnrich) - { - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerAddress)); } - - [Theory] - [InlineData("http://localhost")] - [InlineData("http://localhost", false)] - [InlineData("http://127.0.0.1")] - [InlineData("http://127.0.0.1", false)] - [InlineData("http://[::1]")] - [InlineData("http://[::1]", false)] - public void GrpcClientCallsAreCollectedSuccessfully_Dupe(string baseAddress, bool shouldEnrich = true) + else { - KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(config) - .Build(); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; + Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal(Status.Unset, activity.GetStatus()); - var uri = new Uri($"{baseAddress}:1234"); - var uriHostNameType = Uri.CheckHostName(uri.Host); + // Tags added by the library then removed from the instrumentation + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - using var httpClient = ClientTestHelpers.CreateTestClient(async request => - { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); - var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); - response.TrailingHeaders().Add("grpc-message", "value"); - return response; - }); + if (shouldEnrich) + { + Assert.True(enrichWithHttpRequestMessageCalled); + Assert.True(enrichWithHttpResponseMessageCalled); + } + } - var processor = new Mock>(); + [Theory] + [InlineData("http://localhost")] + [InlineData("http://localhost", false)] + [InlineData("http://127.0.0.1")] + [InlineData("http://127.0.0.1", false)] + [InlineData("http://[::1]")] + [InlineData("http://[::1]", false)] + public void GrpcClientCallsAreCollectedSuccessfully_Dupe(string baseAddress, bool shouldEnrich = true) + { + KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddGrpcClientInstrumentation(options => + var uri = new Uri($"{baseAddress}:1234"); + var uriHostNameType = Uri.CheckHostName(uri.Host); + + using var httpClient = ClientTestHelpers.CreateTestClient(async request => + { + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); + response.TrailingHeaders().Add("grpc-message", "value"); + return response; + }); + + var processor = new Mock>(); + + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) - { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - } - }) - .AddProcessor(processor.Object) - .Build()) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + } + }) + .AddProcessor(processor.Object) + .Build()) + { + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { - var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions - { - HttpClient = httpClient, - }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } + HttpClient = httpClient, + }); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; + Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. + var activity = (Activity)processor.Invocations[2].Arguments[0]; - ValidateGrpcActivity(activity); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); + ValidateGrpcActivity(activity); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); - Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); + Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); + Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); + Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); + Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); - } + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } - Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); - Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - Assert.Equal(Status.Unset, activity.GetStatus()); + Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + Assert.Equal(Status.Unset, activity.GetStatus()); - // Tags added by the library then removed from the instrumentation - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + // Tags added by the library then removed from the instrumentation + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - if (shouldEnrich) - { - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } + if (shouldEnrich) + { + Assert.True(enrichWithHttpRequestMessageCalled); + Assert.True(enrichWithHttpResponseMessageCalled); } + } #if NET6_0_OR_GREATER - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GrpcAndHttpClientInstrumentationIsInvoked(bool shouldEnrich) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GrpcAndHttpClientInstrumentationIsInvoked(bool shouldEnrich) + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var processor = new Mock>(); + processor.Setup(x => x.OnStart(It.IsAny())).Callback(c => { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); - processor.Setup(x => x.OnStart(It.IsAny())).Callback(c => - { - c.SetTag("enrichedWithHttpRequestMessage", "no"); - c.SetTag("enrichedWithHttpResponseMessage", "no"); - }); + c.SetTag("enrichedWithHttpRequestMessage", "no"); + c.SetTag("enrichedWithHttpResponseMessage", "no"); + }); - using var parent = new Activity("parent") - .Start(); + using var parent = new Activity("parent") + .Start(); - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(options => + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("enrichedWithHttpRequestMessage", "yes"); + }; + + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => - { - activity.SetTag("enrichedWithHttpRequestMessage", "yes"); - }; - - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => - { - activity.SetTag("enrichedWithHttpResponseMessage", "yes"); - }; - } - }) - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + activity.SetTag("enrichedWithHttpResponseMessage", "yes"); + }; + } + }) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) + { + // With net5, based on the grpc changes, the quantity of default activities changed. + // TODO: This is a workaround. https://github.com/open-telemetry/opentelemetry-dotnet/issues/1490 + using var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions() { - // With net5, based on the grpc changes, the quantity of default activities changed. - // TODO: This is a workaround. https://github.com/open-telemetry/opentelemetry-dotnet/issues/1490 - using var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions() - { - HttpClient = new HttpClient(), - }); + HttpClient = new HttpClient(), + }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - Assert.Equal(7, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) + OnStart/OnEnd (HTTP) + OnShutdown/Dispose called. - var httpSpan = (Activity)processor.Invocations[3].Arguments[0]; - var grpcSpan = (Activity)processor.Invocations[4].Arguments[0]; + Assert.Equal(7, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) + OnStart/OnEnd (HTTP) + OnShutdown/Dispose called. + var httpSpan = (Activity)processor.Invocations[3].Arguments[0]; + var grpcSpan = (Activity)processor.Invocations[4].Arguments[0]; - ValidateGrpcActivity(grpcSpan); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan.DisplayName); - Assert.Equal(0, grpcSpan.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - Assert.Equal($"HTTP POST", httpSpan.DisplayName); - Assert.Equal(grpcSpan.SpanId, httpSpan.ParentSpanId); + ValidateGrpcActivity(grpcSpan); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan.DisplayName); + Assert.Equal(0, grpcSpan.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal($"HTTP POST", httpSpan.DisplayName); + Assert.Equal(grpcSpan.SpanId, httpSpan.ParentSpanId); - Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); - Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); - Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); - Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); - } + Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); + Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); + Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); + Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); + } - [Fact] - public void GrpcAndHttpClientInstrumentationWithSuppressInstrumentation() + [Fact] + public void GrpcAndHttpClientInstrumentationWithSuppressInstrumentation() + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var processor = new Mock>(); + + using var parent = new Activity("parent") + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(o => o.SuppressDownstreamInstrumentation = true) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); - - using var parent = new Activity("parent") - .Start(); - - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(o => o.SuppressDownstreamInstrumentation = true) - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + Parallel.ForEach( + new int[4], + new ParallelOptions { - Parallel.ForEach( - new int[4], - new ParallelOptions - { - MaxDegreeOfParallelism = 4, - }, - (value) => - { - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - }); - } + MaxDegreeOfParallelism = 4, + }, + (value) => + { + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + }); + } - Assert.Equal(11, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) * 4 + OnShutdown/Dispose called. - var grpcSpan1 = (Activity)processor.Invocations[2].Arguments[0]; - var grpcSpan2 = (Activity)processor.Invocations[4].Arguments[0]; - var grpcSpan3 = (Activity)processor.Invocations[6].Arguments[0]; - var grpcSpan4 = (Activity)processor.Invocations[8].Arguments[0]; + Assert.Equal(11, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) * 4 + OnShutdown/Dispose called. + var grpcSpan1 = (Activity)processor.Invocations[2].Arguments[0]; + var grpcSpan2 = (Activity)processor.Invocations[4].Arguments[0]; + var grpcSpan3 = (Activity)processor.Invocations[6].Arguments[0]; + var grpcSpan4 = (Activity)processor.Invocations[8].Arguments[0]; - ValidateGrpcActivity(grpcSpan1); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan1.DisplayName); - Assert.Equal(0, grpcSpan1.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan1); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan1.DisplayName); + Assert.Equal(0, grpcSpan1.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan2); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan2.DisplayName); - Assert.Equal(0, grpcSpan2.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan2); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan2.DisplayName); + Assert.Equal(0, grpcSpan2.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan3); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan3.DisplayName); - Assert.Equal(0, grpcSpan3.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan3); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan3.DisplayName); + Assert.Equal(0, grpcSpan3.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan4); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan4.DisplayName); - Assert.Equal(0, grpcSpan4.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + ValidateGrpcActivity(grpcSpan4); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan4.DisplayName); + Assert.Equal(0, grpcSpan4.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } - [Fact] - public void GrpcPropagatesContextWithSuppressInstrumentationOptionSetToTrue() + [Fact] + public void GrpcPropagatesContextWithSuppressInstrumentationOptionSetToTrue() + { + try { - try - { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); - - using var source = new ActivitySource("test-source"); + var uri = new Uri($"http://localhost:{this.server.Port}"); + var processor = new Mock>(); - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - action(message, "customField", "customValue"); - }); + using var source = new ActivitySource("test-source"); - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + var propagator = new Mock(); + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, message, action) => { - new TraceContextPropagator(), - propagator.Object, - })); + action(message, "customField", "customValue"); + }); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = true; - }) - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation(options => - { - options.EnrichWithHttpRequest = (activity, request) => - { - activity.SetCustomProperty("customField", request.Headers["customField"].ToString()); - }; - }) // Instrumenting the server side as well - .AddProcessor(processor.Object) - .Build()) + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator.Object, + })); + + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => { - using (var activity = source.StartActivity("parent")) + o.SuppressDownstreamInstrumentation = true; + }) + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation(options => + { + options.EnrichWithHttpRequest = (activity, request) => { - Assert.NotNull(activity); - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } - - WaitForProcessorInvocations(processor, 7); + activity.SetCustomProperty("customField", request.Headers["customField"].ToString()); + }; + }) // Instrumenting the server side as well + .AddProcessor(processor.Object) + .Build()) + { + using (var activity = source.StartActivity("parent")) + { + Assert.NotNull(activity); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); } - Assert.Equal(9, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 (parent, gRPC client, and server) + Shutdown + Dispose called. - - Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameHttpRequestIn)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameHttpRequestIn)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); - Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); - - var serverActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameHttpRequestIn); - var clientActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameGrpcOut); - - Assert.Equal($"greet.Greeter/SayHello", clientActivity.DisplayName); - Assert.Equal($"greet.Greeter/SayHello", serverActivity.DisplayName); - Assert.Equal(clientActivity.TraceId, serverActivity.TraceId); - Assert.Equal(clientActivity.SpanId, serverActivity.ParentSpanId); - Assert.Equal(0, clientActivity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - Assert.Equal("customValue", serverActivity.GetCustomProperty("customField") as string); + WaitForProcessorInvocations(processor, 7); } - finally + + Assert.Equal(9, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 (parent, gRPC client, and server) + Shutdown + Dispose called. + + Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameHttpRequestIn)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameHttpRequestIn)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); + Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); + Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); + + var serverActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameHttpRequestIn); + var clientActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameGrpcOut); + + Assert.Equal($"greet.Greeter/SayHello", clientActivity.DisplayName); + Assert.Equal($"greet.Greeter/SayHello", serverActivity.DisplayName); + Assert.Equal(clientActivity.TraceId, serverActivity.TraceId); + Assert.Equal(clientActivity.SpanId, serverActivity.ParentSpanId); + Assert.Equal(0, clientActivity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal("customValue", serverActivity.GetCustomProperty("customField") as string); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public void GrpcDoesNotPropagateContextWithSuppressInstrumentationOptionSetToFalse() + [Fact] + public void GrpcDoesNotPropagateContextWithSuppressInstrumentationOptionSetToFalse() + { + try { - try - { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); + var uri = new Uri($"http://localhost:{this.server.Port}"); + var processor = new Mock>(); - using var source = new ActivitySource("test-source"); + using var source = new ActivitySource("test-source"); - bool isPropagatorCalled = false; - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - isPropagatorCalled = true; - }); + bool isPropagatorCalled = false; + var propagator = new Mock(); + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, message, action) => + { + isPropagatorCalled = true; + }); - Sdk.SetDefaultTextMapPropagator(propagator.Object); + Sdk.SetDefaultTextMapPropagator(propagator.Object); - var headers = new Metadata(); + var headers = new Metadata(); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = false; - }) - .AddProcessor(processor.Object) - .Build()) + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => { - using var activity = source.StartActivity("parent"); - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest(), headers); - } + o.SuppressDownstreamInstrumentation = false; + }) + .AddProcessor(processor.Object) + .Build()) + { + using var activity = source.StartActivity("parent"); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest(), headers); + } - Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose called. + Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose called. - Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); - Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); + Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); + Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); + Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); - // Propagator is not called - Assert.False(isPropagatorCalled); - } - finally - { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + // Propagator is not called + Assert.False(isPropagatorCalled); } - - [Fact] - public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() + finally { - try + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } - using var source = new ActivitySource("test-source"); + [Fact] + public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() + { + try + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var processor = new Mock>(); - bool isPropagatorCalled = false; - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - isPropagatorCalled = true; - }); + using var source = new ActivitySource("test-source"); - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + bool isPropagatorCalled = false; + var propagator = new Mock(); + propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((context, message, action) => { - new TraceContextPropagator(), - propagator.Object, - })); + isPropagatorCalled = true; + }); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = true; - }) - .AddProcessor(processor.Object) - .Build()) - { - using var activity = source.StartActivity("parent"); - using (SuppressInstrumentationScope.Begin()) - { - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } - } + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator.Object, + })); - // If suppressed, activity is not emitted and - // propagation is also not performed. - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 for parent + OnShutdown + Dispose called. - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.False(isPropagatorCalled); - } - finally + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => + { + o.SuppressDownstreamInstrumentation = true; + }) + .AddProcessor(processor.Object) + .Build()) { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + using var activity = source.StartActivity("parent"); + using (SuppressInstrumentationScope.Begin()) { - new TraceContextPropagator(), - new BaggagePropagator(), - })); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } } + + // If suppressed, activity is not emitted and + // propagation is also not performed. + Assert.Equal(5, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 for parent + OnShutdown + Dispose called. + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); + Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); + Assert.False(isPropagatorCalled); } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } #endif - [Fact] - public void AddGrpcClientInstrumentationNamedOptionsSupported() - { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + [Fact] + public void AddGrpcClientInstrumentationNamedOptionsSupported() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddGrpcClientInstrumentation() - .AddGrpcClientInstrumentation("Instrumentation2", configure: null) - .Build(); + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddGrpcClientInstrumentation() + .AddGrpcClientInstrumentation("Instrumentation2", configure: null) + .Build(); - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); - } + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - [Fact] - public void Grpc_BadArgs() - { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddGrpcClientInstrumentation()); - } + [Fact] + public void Grpc_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddGrpcClientInstrumentation()); + } - private static void ValidateGrpcActivity(Activity activityToValidate) - { - Assert.Equal(GrpcClientDiagnosticListener.ActivitySourceName, activityToValidate.Source.Name); - Assert.Equal(GrpcClientDiagnosticListener.Version.ToString(), activityToValidate.Source.Version); - Assert.Equal(ActivityKind.Client, activityToValidate.Kind); - } + private static void ValidateGrpcActivity(Activity activityToValidate) + { + Assert.Equal(GrpcClientDiagnosticListener.ActivitySourceName, activityToValidate.Source.Name); + Assert.Equal(GrpcClientDiagnosticListener.Version.ToString(), activityToValidate.Source.Version); + Assert.Equal(ActivityKind.Client, activityToValidate.Kind); + } - private static Predicate GeneratePredicateForMoqProcessorActivity(string methodName, string activityOperationName) - { - return invo => invo.Method.Name == methodName && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; - } + private static Predicate GeneratePredicateForMoqProcessorActivity(string methodName, string activityOperationName) + { + return invo => invo.Method.Name == methodName && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs index 357cc9052d9..de328a91701 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs @@ -15,12 +15,8 @@ // #if NET6_0_OR_GREATER -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; -using System.Threading; using Greet; using Grpc.Core; using Grpc.Net.Client; @@ -35,182 +31,285 @@ using static OpenTelemetry.Internal.HttpSemanticConventionHelper; using Status = OpenTelemetry.Trace.Status; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public partial class GrpcTests : IDisposable { - public partial class GrpcTests : IDisposable + private const string OperationNameHttpRequestIn = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; + private const string OperationNameGrpcOut = "Grpc.Net.Client.GrpcOut"; + private const string OperationNameHttpOut = "System.Net.Http.HttpRequestOut"; + + private readonly GrpcServer server; + + public GrpcTests() { - private const string OperationNameHttpRequestIn = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; - private const string OperationNameGrpcOut = "Grpc.Net.Client.GrpcOut"; - private const string OperationNameHttpOut = "System.Net.Http.HttpRequestOut"; + this.server = new GrpcServer(); + } - private readonly GrpcServer server; + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcAspNetCoreSupport) + { + var exportedItems = new List(); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); - public GrpcTests() + if (enableGrpcAspNetCoreSupport.HasValue) { - this.server = new GrpcServer(); + tracerProviderBuilder.AddAspNetCoreInstrumentation(options => + { + options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; + }); } - - [Theory] - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcAspNetCoreSupport) + else { - var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); + tracerProviderBuilder.AddAspNetCoreInstrumentation(); + } - if (enableGrpcAspNetCoreSupport.HasValue) - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(options => - { - options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; - }); - } - else - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(); - } + using var tracerProvider = tracerProviderBuilder + .AddInMemoryExporter(exportedItems) + .Build(); - 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}"); - 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)); - 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]; - WaitForExporterToReceiveItems(exportedItems, 1); - Assert.Single(exportedItems); - var activity = exportedItems[0]; + Assert.Equal(ActivityKind.Server, activity.Kind); - 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)); + } - 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()); - 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); + } - // 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 { [SemanticConventionOptInKeyName] = "http" }) + .Build(); + + var exportedItems = new List(); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)); - // 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) + if (enableGrpcAspNetCoreSupport.HasValue) { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) - .Build(); + tracerProviderBuilder.AddAspNetCoreInstrumentation(options => + { + options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; + }); + } + else + { + tracerProviderBuilder.AddAspNetCoreInstrumentation(); + } - var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)); + using var tracerProvider = tracerProviderBuilder + .AddInMemoryExporter(exportedItems) + .Build(); - if (enableGrpcAspNetCoreSupport.HasValue) - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(options => - { - options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; - }); - } - else - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(); - } + var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; + var uri = new Uri($"http://localhost:{this.server.Port}"); - using var tracerProvider = tracerProviderBuilder - .AddInMemoryExporter(exportedItems) - .Build(); + using var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var returnMsg = client.SayHello(new HelloRequest()).Message; + Assert.False(string.IsNullOrEmpty(returnMsg)); - var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; - var uri = new Uri($"http://localhost:{this.server.Port}"); + WaitForExporterToReceiveItems(exportedItems, 1); + Assert.Single(exportedItems); + var activity = exportedItems[0]; - using var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var returnMsg = client.SayHello(new HelloRequest()).Message; - Assert.False(string.IsNullOrEmpty(returnMsg)); + Assert.Equal(ActivityKind.Server, activity.Kind); - WaitForExporterToReceiveItems(exportedItems, 1); - Assert.Single(exportedItems); - var activity = exportedItems[0]; + 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(ActivityKind.Server, activity.Kind); + Assert.Equal(Status.Unset, activity.GetStatus()); - 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 + // 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.0", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); + 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 { [SemanticConventionOptInKeyName] = "http/dup" }) + .Build(); + + var exportedItems = new List(); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)); + + if (enableGrpcAspNetCoreSupport.HasValue) + { + tracerProviderBuilder.AddAspNetCoreInstrumentation(options => { - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - } + options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; + }); + } + else + { + tracerProviderBuilder.AddAspNetCoreInstrumentation(); + } - Assert.Equal(Status.Unset, activity.GetStatus()); + using var tracerProvider = tracerProviderBuilder + .AddInMemoryExporter(exportedItems) + .Build(); - // 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.0", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); - Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string); + 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)); } - // 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) + 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) { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http/dup" }) - .Build(); + 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.0", 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 + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewActivity(bool? enableGrpcAspNetCoreSupport) + { + try + { + // B3Propagator along with the headers passed to the client.SayHello ensure that the instrumentation creates a sibling activity + Sdk.SetDefaultTextMapPropagator(new Extensions.Propagators.B3Propagator()); var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); if (enableGrpcAspNetCoreSupport.HasValue) { @@ -233,8 +332,14 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_Dupe(bool? enable using var channel = GrpcChannel.ForAddress(uri); var client = new Greeter.GreeterClient(channel); - var returnMsg = client.SayHello(new HelloRequest()).Message; - Assert.False(string.IsNullOrEmpty(returnMsg)); + var headers = new Metadata + { + { "traceparent", "00-120dc44db5b736468afb112197b0dbd3-5dfbdf27ec544544-01" }, + { "x-b3-traceid", "120dc44db5b736468afb112197b0dbd3" }, + { "x-b3-spanid", "b0966f651b9e0126" }, + { "x-b3-sampled", "1" }, + }; + client.SayHello(new HelloRequest(), headers); WaitForExporterToReceiveItems(exportedItems, 1); Assert.Single(exportedItems); @@ -242,7 +347,6 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_Dupe(bool? enable Assert.Equal(ActivityKind.Server, activity.Kind); - // OLD if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value) { Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); @@ -269,169 +373,60 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes_Dupe(bool? enable 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.0", 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 - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewActivity(bool? enableGrpcAspNetCoreSupport) + finally { - try - { - // B3Propagator along with the headers passed to the client.SayHello ensure that the instrumentation creates a sibling activity - Sdk.SetDefaultTextMapPropagator(new Extensions.Propagators.B3Propagator()); - var exportedItems = new List(); - 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 headers = new Metadata - { - { "traceparent", "00-120dc44db5b736468afb112197b0dbd3-5dfbdf27ec544544-01" }, - { "x-b3-traceid", "120dc44db5b736468afb112197b0dbd3" }, - { "x-b3-spanid", "b0966f651b9e0126" }, - { "x-b3-sampled", "1" }, - }; - client.SayHello(new HelloRequest(), headers); - - 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); - } - finally + // Set the SDK to use the default propagator for other unit tests + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - // Set the SDK to use the default propagator for other unit tests - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - public void Dispose() - { - this.server.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + this.server.Dispose(); + GC.SuppressFinalize(this); + } - private static void WaitForExporterToReceiveItems(List itemsReceived, int itemCount) - { - // 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return itemsReceived.Count >= itemCount; - }, - TimeSpan.FromSeconds(1))); - } + private static void WaitForExporterToReceiveItems(List itemsReceived, int itemCount) + { + // 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 + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return itemsReceived.Count >= itemCount; + }, + TimeSpan.FromSeconds(1))); + } - private static void WaitForProcessorInvocations(Mock> spanProcessor, int invocationCount) - { - // 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return spanProcessor.Invocations.Count >= invocationCount; - }, - TimeSpan.FromSeconds(1))); - } + private static void WaitForProcessorInvocations(Mock> spanProcessor, int invocationCount) + { + // 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 + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return spanProcessor.Invocations.Count >= invocationCount; + }, + TimeSpan.FromSeconds(1))); + } - private static Activity GetActivityFromProcessorInvocation(Mock> processor, string methodName, string activityOperationName) - { - return processor.Invocations - .FirstOrDefault(invo => - { - return invo.Method.Name == methodName - && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; - })?.Arguments[0] as Activity; - } + private static Activity GetActivityFromProcessorInvocation(Mock> processor, string methodName, string activityOperationName) + { + return processor.Invocations + .FirstOrDefault(invo => + { + return invo.Method.Name == methodName + && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; + })?.Arguments[0] as Activity; } } #endif diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs index 16bef79b11d..cf444b2a2c5 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs @@ -17,36 +17,35 @@ using Grpc.Core; using Microsoft.Extensions.Logging; -namespace OpenTelemetry.Instrumentation.Grpc.Services.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Services.Tests; + +public class GreeterService : Greeter.GreeterBase { - public class GreeterService : Greeter.GreeterBase - { - private readonly ILogger logger; + private readonly ILogger logger; - public GreeterService(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } + public GreeterService(ILoggerFactory loggerFactory) + { + this.logger = loggerFactory.CreateLogger(); + } - public override Task SayHello(HelloRequest request, ServerCallContext context) - { - this.logger.LogInformation("Sending hello to {Name}", request.Name); - return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); - } + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + this.logger.LogInformation("Sending hello to {Name}", request.Name); + return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); + } - public override async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) + public override async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + var i = 0; + while (!context.CancellationToken.IsCancellationRequested) { - var i = 0; - while (!context.CancellationToken.IsCancellationRequested) - { - var message = $"How are you {request.Name}? {++i}"; - this.logger.LogInformation("Sending greeting {Message}.", message); + var message = $"How are you {request.Name}? {++i}"; + this.logger.LogInformation("Sending greeting {Message}.", message); - await responseStream.WriteAsync(new HelloReply { Message = message }).ConfigureAwait(false); + await responseStream.WriteAsync(new HelloReply { Message = message }).ConfigureAwait(false); - // Gotta look busy - await Task.Delay(1000).ConfigureAwait(false); - } + // Gotta look busy + await Task.Delay(1000).ConfigureAwait(false); } } }