diff --git a/examples/wcf/client-core/Program.cs b/examples/wcf/client-core/Program.cs index 71a68aae83..71cc37ec5d 100644 --- a/examples/wcf/client-core/Program.cs +++ b/examples/wcf/client-core/Program.cs @@ -42,12 +42,22 @@ public static async Task Main() .AddZipkinExporter() .Build(); - await CallService( - new BasicHttpBinding(BasicHttpSecurityMode.None), - new EndpointAddress(config.GetSection("Service").GetValue("HttpAddress"))).ConfigureAwait(false); - await CallService( - new NetTcpBinding(SecurityMode.None), - new EndpointAddress(config.GetSection("Service").GetValue("TcpAddress"))).ConfigureAwait(false); + switch (config.GetValue("Server")!.ToUpperInvariant()) + { + case "ASPNET": + await CallService( + new BasicHttpBinding(BasicHttpSecurityMode.None), + new EndpointAddress(config.GetSection("Service").GetValue("AspNetAddress"))).ConfigureAwait(false); + break; + default: + await CallService( + new BasicHttpBinding(BasicHttpSecurityMode.None), + new EndpointAddress(config.GetSection("Service").GetValue("HttpAddress"))).ConfigureAwait(false); + await CallService( + new NetTcpBinding(SecurityMode.None), + new EndpointAddress(config.GetSection("Service").GetValue("TcpAddress"))).ConfigureAwait(false); + break; + } Console.WriteLine("Press enter to exit."); Console.ReadLine(); diff --git a/examples/wcf/client-core/appsettings.json b/examples/wcf/client-core/appsettings.json index a468e204fb..20ad770bad 100644 --- a/examples/wcf/client-core/appsettings.json +++ b/examples/wcf/client-core/appsettings.json @@ -1,6 +1,8 @@ -{ +{ + "Server": "self-hosted", "Service": { "HttpAddress": "http://localhost:9009/Telemetry", - "TcpAddress": "net.tcp://localhost:9090/Telemetry" + "TcpAddress": "net.tcp://localhost:9090/Telemetry", + "AspNetAddress": "http://localhost:61494/StatusService.svc" } -} \ No newline at end of file +} diff --git a/examples/wcf/client-netframework/App.config b/examples/wcf/client-netframework/App.config index 49b2186221..e6e12aaa03 100644 --- a/examples/wcf/client-netframework/App.config +++ b/examples/wcf/client-netframework/App.config @@ -1,5 +1,8 @@ - + + + + @@ -33,6 +36,7 @@ + diff --git a/examples/wcf/client-netframework/Examples.Wcf.Client.NetFramework.csproj b/examples/wcf/client-netframework/Examples.Wcf.Client.NetFramework.csproj index 2e846ea19e..cdf07abc4b 100644 --- a/examples/wcf/client-netframework/Examples.Wcf.Client.NetFramework.csproj +++ b/examples/wcf/client-netframework/Examples.Wcf.Client.NetFramework.csproj @@ -18,6 +18,7 @@ + diff --git a/examples/wcf/client-netframework/Program.cs b/examples/wcf/client-netframework/Program.cs index 38334d7b0a..ee699a507a 100644 --- a/examples/wcf/client-netframework/Program.cs +++ b/examples/wcf/client-netframework/Program.cs @@ -15,6 +15,7 @@ // using System; +using System.Configuration; using System.ServiceModel; using System.Threading.Tasks; using OpenTelemetry; @@ -33,9 +34,17 @@ public static async Task Main() .AddZipkinExporter() .Build(); - await CallService("StatusService_Http").ConfigureAwait(false); - await CallService("StatusService_Tcp").ConfigureAwait(false); - await CallService("StatusService_Rest").ConfigureAwait(false); + switch (ConfigurationManager.AppSettings["Server"].ToUpperInvariant()) + { + case "ASPNET": + await CallService("StatusService_AspNet").ConfigureAwait(false); + break; + default: + await CallService("StatusService_Http").ConfigureAwait(false); + await CallService("StatusService_Tcp").ConfigureAwait(false); + await CallService("StatusService_Rest").ConfigureAwait(false); + break; + } Console.WriteLine("Press enter to exit."); Console.ReadLine(); diff --git a/examples/wcf/server-aspnetframework/Examples.Wcf.Server.AspNetFramework.csproj b/examples/wcf/server-aspnetframework/Examples.Wcf.Server.AspNetFramework.csproj new file mode 100644 index 0000000000..c4efc4eceb --- /dev/null +++ b/examples/wcf/server-aspnetframework/Examples.Wcf.Server.AspNetFramework.csproj @@ -0,0 +1,74 @@ + + + + net462 + Library + bin\ + false + false + web.config + false + + + + + + + + + + + + + + + + + + + + + Global.asax + + + StatusService.svc + + + + + Web.config + + + Web.config + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + + + + $(BuildDependsOn) + SkipBuildWithoutVisualStudio + + diff --git a/examples/wcf/server-aspnetframework/Global.asax b/examples/wcf/server-aspnetframework/Global.asax new file mode 100644 index 0000000000..7041acc60d --- /dev/null +++ b/examples/wcf/server-aspnetframework/Global.asax @@ -0,0 +1 @@ +<%@ Application Codebehind="Global.asax.cs" Inherits="Examples.Wcf.Server.AspNetFramework.WebApiApplication" Language="C#" %> \ No newline at end of file diff --git a/examples/wcf/server-aspnetframework/Global.asax.cs b/examples/wcf/server-aspnetframework/Global.asax.cs new file mode 100644 index 0000000000..692259cc99 --- /dev/null +++ b/examples/wcf/server-aspnetframework/Global.asax.cs @@ -0,0 +1,67 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Configuration; +using System.Web; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace Examples.Wcf.Server.AspNetFramework; + +#pragma warning disable SA1649 // File name should match first type name +public class WebApiApplication : HttpApplication +#pragma warning restore SA1649 // File name should match first type name +{ + private IDisposable? tracerProvider; + + protected void Application_Start() + { + var builder = Sdk.CreateTracerProviderBuilder() + .ConfigureResource(resource => resource.AddService("Wcf-AspNetServer")) + .AddAspNetInstrumentation() + .AddWcfInstrumentation(); + + switch (ConfigurationManager.AppSettings["UseExporter"].ToUpperInvariant()) + { + case "ZIPKIN": + builder.AddZipkinExporter(zipkinOptions => + { + zipkinOptions.Endpoint = new Uri(ConfigurationManager.AppSettings["ZipkinEndpoint"]); + }); + break; + case "OTLP": + builder.AddOtlpExporter(otlpOptions => + { + otlpOptions.Endpoint = new Uri(ConfigurationManager.AppSettings["OtlpEndpoint"]); + }); + break; + default: + builder.AddConsoleExporter(options => options.Targets = ConsoleExporterOutputTargets.Debug); + break; + } + + this.tracerProvider = builder.Build(); + } + + protected void Application_End() + { + this.tracerProvider?.Dispose(); + } +} diff --git a/examples/wcf/server-aspnetframework/Properties/launchSettings.json b/examples/wcf/server-aspnetframework/Properties/launchSettings.json new file mode 100644 index 0000000000..de6793e474 --- /dev/null +++ b/examples/wcf/server-aspnetframework/Properties/launchSettings.json @@ -0,0 +1,18 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61494" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/wcf/server-aspnetframework/StatusService.svc b/examples/wcf/server-aspnetframework/StatusService.svc new file mode 100644 index 0000000000..6197043ec9 --- /dev/null +++ b/examples/wcf/server-aspnetframework/StatusService.svc @@ -0,0 +1 @@ +<%@ ServiceHost Language="C#" Debug="true" Service="Examples.Wcf.Server.AspNetFramework.StatusService" CodeBehind="StatusService.svc.cs" %> diff --git a/examples/wcf/server-aspnetframework/StatusService.svc.cs b/examples/wcf/server-aspnetframework/StatusService.svc.cs new file mode 100644 index 0000000000..e09a817b82 --- /dev/null +++ b/examples/wcf/server-aspnetframework/StatusService.svc.cs @@ -0,0 +1,32 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading.Tasks; + +namespace Examples.Wcf.Server.AspNetFramework; + +public class StatusService : IStatusServiceContract +{ + public Task PingAsync(StatusRequest request) + { + return Task.FromResult( + new StatusResponse + { + ServerTime = DateTimeOffset.UtcNow, + }); + } +} diff --git a/examples/wcf/server-aspnetframework/Web.Debug.config b/examples/wcf/server-aspnetframework/Web.Debug.config new file mode 100644 index 0000000000..104f153f93 --- /dev/null +++ b/examples/wcf/server-aspnetframework/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/examples/wcf/server-aspnetframework/Web.Release.config b/examples/wcf/server-aspnetframework/Web.Release.config new file mode 100644 index 0000000000..63c58428af --- /dev/null +++ b/examples/wcf/server-aspnetframework/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/examples/wcf/server-aspnetframework/Web.config b/examples/wcf/server-aspnetframework/Web.config new file mode 100644 index 0000000000..97259f8cc7 --- /dev/null +++ b/examples/wcf/server-aspnetframework/Web.config @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 9784de114d..c250a011cc 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -301,6 +301,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.InfluxDB", "exampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.AotCompatibility.TestApp", "test\OpenTelemetry.AotCompatibility.TestApp\OpenTelemetry.AotCompatibility.TestApp.csproj", "{31937862-0C88-41C0-AFD6-F97A7BF803A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Wcf.Server.AspNetFramework", "examples\wcf\server-aspnetframework\Examples.Wcf.Server.AspNetFramework.csproj", "{23AA75F6-403F-4867-BF0E-BAF0A44F9A7A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{9B30F5FD-3309-49CB-9CAD-D3372FAFD796}" ProjectSection(SolutionItems) = preProject examples\Directory.Build.props = examples\Directory.Build.props @@ -630,6 +632,10 @@ Global {31937862-0C88-41C0-AFD6-F97A7BF803A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {31937862-0C88-41C0-AFD6-F97A7BF803A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {31937862-0C88-41C0-AFD6-F97A7BF803A9}.Release|Any CPU.Build.0 = Release|Any CPU + {23AA75F6-403F-4867-BF0E-BAF0A44F9A7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23AA75F6-403F-4867-BF0E-BAF0A44F9A7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23AA75F6-403F-4867-BF0E-BAF0A44F9A7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23AA75F6-403F-4867-BF0E-BAF0A44F9A7A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -724,6 +730,7 @@ Global {B4951583-D432-4E87-85CF-498FDD6A35E6} = {2D354354-BAFB-490B-A26F-6A16A52A1A45} {31937862-0C88-41C0-AFD6-F97A7BF803A9} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {9B30F5FD-3309-49CB-9CAD-D3372FAFD796} = {824BD1DE-3FA8-4FE0-823A-FD365EAC78AF} + {23AA75F6-403F-4867-BF0E-BAF0A44F9A7A} = {73474960-8F91-4EE5-8E3E-F7E7ADA99238} {048509D6-FB49-4B84-832A-90E55520B97B} = {824BD1DE-3FA8-4FE0-823A-FD365EAC78AF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md index a73132691d..ed16c6bdd0 100644 --- a/src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md @@ -4,6 +4,8 @@ * Update OpenTelemetry SDK version to `1.6.0`. ([#1344](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1344)) +* Fixed span hierarchy when hosted in ASP.NET + ([#1342](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1342)) ## 1.0.0-rc.12 diff --git a/src/OpenTelemetry.Instrumentation.Wcf/Implementation/AspNetParentSpanCorrector.cs b/src/OpenTelemetry.Instrumentation.Wcf/Implementation/AspNetParentSpanCorrector.cs new file mode 100644 index 0000000000..99c9a8e9e3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Wcf/Implementation/AspNetParentSpanCorrector.cs @@ -0,0 +1,151 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETFRAMEWORK +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Instrumentation.Wcf.Implementation; + +/// +/// When WCF is hosted in ASP.NET and the ASP.NET telemetry instrumentation is installed, the execution context does not flow from ASP.NET into WCF +/// so the span which is created by ASP.NET is not visible to WCF. When OpenTelemetry.Instrumentation.Wcf then creates its own span it uses the parent +/// which is passed from the caller. This results in the ASP.NET span and the WCF span being siblings off the same parent when, really, the WCF span +/// should actually be a child of the ASP.NET (transport) span. This class corrects that behavior so the generated spans are correctly parented. +/// +/// The way it does that is it hooks into the ASP.NET telemetry and rewrites the incoming request headers to reflect the ASP.NET parent (so, as far +/// as WCF is concerned, it appears like the ASP.NET parent span was sent from the caller). It does this all via reflection to avoid having explicit +/// dependencies on System.Web and OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule. This way this behavior is only enabled if the consumer +/// also has the ASP.NET instrumentation installed. +/// +internal static class AspNetParentSpanCorrector +{ + private const string TelemetryHttpModuleTypeName = "OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule"; + private const string TelemetryHttpModuleOptionsTypeName = "OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule"; + + private static readonly ReflectedInfo ReflectedValues = Initialize(); + private static readonly PropertyFetcher RequestFetcher = new PropertyFetcher("Request"); + private static readonly PropertyFetcher HeadersFetcher = new PropertyFetcher("Headers"); + private static bool isRegistered; + + public static void Register() + { + if (!isRegistered && ReflectedValues != null) + { + ReflectedValues.SubscribeToOnRequestStartedCallback(); + isRegistered = true; + } + } + + private static void OnRequestStarted(Activity activity, object context) + { + var request = RequestFetcher.Fetch(context); + var headers = HeadersFetcher.Fetch(request); + + ReflectedValues.SetHeadersReadOnly(headers, false); + try + { + ReflectedValues.GetTelemetryHttpModulePropagator().Inject( + new PropagationContext(activity.Context, Baggage.Current), + headers, + (headers, name, value) => headers[name] = value); + } + finally + { + ReflectedValues.SetHeadersReadOnly(headers, true); + } + } + + private static ReflectedInfo Initialize() + { + try + { + var isReadOnlyProp = typeof(NameValueCollection).GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); + if (isReadOnlyProp == null) + { + throw new NotSupportedException("NameValueCollection.IsReadOnly property not found"); + } + + var setHeadersReadOnly = (Action)isReadOnlyProp.SetMethod.CreateDelegate(typeof(Action)); + + return new ReflectedInfo + { + SetHeadersReadOnly = setHeadersReadOnly, + GetTelemetryHttpModulePropagator = GenerateGetPropagatorLambda(), + SubscribeToOnRequestStartedCallback = GenerateSubscribeLambda(), + }; + } + catch (Exception ex) + { + WcfInstrumentationEventSource.Log.AspNetReflectionFailedToBind(ex); + } + + return null; + } + + private static Func GenerateGetPropagatorLambda() + { + // this method generates this lambda: + // () => TelemetryHttpModule.Options.TextMapPropagator + + var telemetryHttpModuleType = Type.GetType(TelemetryHttpModuleTypeName, true); + var options = Expression.Property(null, telemetryHttpModuleType, "Options"); + var propertyReferenceExpression = Expression.Property(options, "TextMapPropagator"); + return (Func)Expression.Lambda(propertyReferenceExpression).Compile(); + } + + private static Func GenerateSubscribeLambda() + { + // this method effectively generates this lambda: + // () => TelemetryHttpModule.Options.OnRequestStartedCallback += OnRequestStarted + // technically it generates this: + // () => TelemetryHttpModule.Options.OnRequestStartedCallback = + // (Action)addOurCallbackToDelegate(TelemetryHttpModule.Options.OnRequestStartedCallback); + + var telemetryHttpModuleType = Type.GetType(TelemetryHttpModuleTypeName, true); + var telemetryHttpModuleOptionsType = Type.GetType(TelemetryHttpModuleOptionsTypeName, true); + var onRequestStartedProp = telemetryHttpModuleOptionsType.GetProperty("OnRequestStartedCallback"); + if (onRequestStartedProp == null) + { + throw new NotSupportedException("TelemetryHttpModuleOptions.OnRequestStartedCallback property not found"); + } + + Func addOurCallbackToDelegate = (existingCallback) => + { + var myCallback = OnRequestStarted; + var myCallbackProperlyTyped = Delegate.CreateDelegate(onRequestStartedProp.PropertyType, myCallback.Target, myCallback.Method); + return Delegate.Combine(existingCallback, myCallbackProperlyTyped); + }; + + var options = Expression.Property(null, telemetryHttpModuleType, "Options"); + var callbackProperty = Expression.Property(options, onRequestStartedProp); + var combinedDelegate = Expression.Call(Expression.Constant(addOurCallbackToDelegate.Target), addOurCallbackToDelegate.Method, callbackProperty); + var subscribeExpression = Expression.Assign(callbackProperty, Expression.Convert(combinedDelegate, onRequestStartedProp.PropertyType)); + return (Func)Expression.Lambda(subscribeExpression).Compile(); + } + + private sealed class ReflectedInfo + { + public Action SetHeadersReadOnly; + public Func GetTelemetryHttpModulePropagator; + public Func SubscribeToOnRequestStartedCallback; + } +} +#endif diff --git a/src/OpenTelemetry.Instrumentation.Wcf/Implementation/WcfInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.Wcf/Implementation/WcfInstrumentationEventSource.cs index d6546b0ecf..24cace7703 100644 --- a/src/OpenTelemetry.Instrumentation.Wcf/Implementation/WcfInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.Wcf/Implementation/WcfInstrumentationEventSource.cs @@ -76,11 +76,27 @@ public void HttpServiceModelReflectionFailedToBind(string exception, string? ass this.WriteEvent(EventIds.HttpServiceModelReflectionFailedToBind, exception, assembly); } + [NonEvent] + public void AspNetReflectionFailedToBind(Exception exception) + { + if (this.IsEnabled(EventLevel.Verbose, (EventKeywords)(-1))) + { + this.AspNetReflectionFailedToBind(exception.ToInvariantString()); + } + } + + [Event(EventIds.AspNetReflectionFailedToBind, Message = "Failed to bind to ASP.NET instrumentation. Exception {0}.", Level = EventLevel.Verbose)] + public void AspNetReflectionFailedToBind(string exception) + { + this.WriteEvent(EventIds.AspNetReflectionFailedToBind, exception); + } + private class EventIds { public const int RequestIsFilteredOut = 1; public const int RequestFilterException = 2; public const int EnrichmentException = 3; public const int HttpServiceModelReflectionFailedToBind = 4; + public const int AspNetReflectionFailedToBind = 5; } } diff --git a/src/OpenTelemetry.Instrumentation.Wcf/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Wcf/TracerProviderBuilderExtensions.cs index 41c3ff8778..4cac3b1bb7 100644 --- a/src/OpenTelemetry.Instrumentation.Wcf/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Wcf/TracerProviderBuilderExtensions.cs @@ -53,6 +53,10 @@ public static TracerProviderBuilder AddWcfInstrumentation(this TracerProviderBui WcfInstrumentationActivitySource.Options = options; +#if NETFRAMEWORK + Instrumentation.Wcf.Implementation.AspNetParentSpanCorrector.Register(); +#endif + return builder.AddSource(WcfInstrumentationActivitySource.ActivitySourceName); } } diff --git a/test/OpenTelemetry.Instrumentation.Wcf.Tests/AspNetParentSpanCorrectorTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Wcf.Tests/AspNetParentSpanCorrectorTests.netfx.cs new file mode 100644 index 0000000000..22f671e14d --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Wcf.Tests/AspNetParentSpanCorrectorTests.netfx.cs @@ -0,0 +1,65 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETFRAMEWORK +using System.Collections.Specialized; +using System.Diagnostics; +using System.Reflection; +using OpenTelemetry.Instrumentation.Wcf.Implementation; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Wcf.Tests; + +[Collection("WCF")] +public class AspNetParentSpanCorrectorTests +{ + [Fact] + public void IncomingRequestHeadersAreOverwrittenWithAspNetParent() + { + var testSource = new ActivitySource("TestSource"); + using var provider = Sdk.CreateTracerProviderBuilder() + .AddSource("TestSource") + .AddWcfInstrumentation() + .Build(); + + var reflectedValues = typeof(AspNetParentSpanCorrector).GetField("ReflectedValues", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + Assert.False(reflectedValues == null, "The reflection-based bind to OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule failed. The AspNet telemetry has likely changed, and this assembly needs to be updated to match"); + + using (var aspNetActivity = testSource.StartActivity("AspNetActivity")) + { + var context = new FakeHttpContext(); + + var method = typeof(AspNetParentSpanCorrector).GetMethod("OnRequestStarted", BindingFlags.Static | BindingFlags.NonPublic); + method.Invoke(null, new object[] { aspNetActivity, context }); + + var headerVal = context.Request.Headers["traceparent"]; + Assert.Contains(aspNetActivity.TraceId.ToString(), headerVal); + Assert.Contains(aspNetActivity.SpanId.ToString(), headerVal); + } + } + + private class FakeHttpContext + { + public FakeRequest Request { get; } = new FakeRequest(); + } + + private class FakeRequest + { + public NameValueCollection Headers { get; } = new NameValueCollection(); + } +} +#endif diff --git a/test/OpenTelemetry.Instrumentation.Wcf.Tests/OpenTelemetry.Instrumentation.Wcf.Tests.csproj b/test/OpenTelemetry.Instrumentation.Wcf.Tests/OpenTelemetry.Instrumentation.Wcf.Tests.csproj index 93e0100f67..d7934fba41 100644 --- a/test/OpenTelemetry.Instrumentation.Wcf.Tests/OpenTelemetry.Instrumentation.Wcf.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Wcf.Tests/OpenTelemetry.Instrumentation.Wcf.Tests.csproj @@ -15,6 +15,7 @@ +