diff --git a/.github/workflows/package-Instrumentation.Owin.yml b/.github/workflows/package-Instrumentation.Owin.yml new file mode 100644 index 0000000000..5ae2534edb --- /dev/null +++ b/.github/workflows/package-Instrumentation.Owin.yml @@ -0,0 +1,49 @@ +name: Pack OpenTelemetry.Contrib.Instrumentation.Owin + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Instrumentation.Owin-*' + +jobs: + build-test-pack: + runs-on: ${{ matrix.os }} + env: + PROJECT: OpenTelemetry.Contrib.Instrumentation.Owin + + strategy: + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetching all + + - name: Install dependencies + run: dotnet restore + + - name: dotnet build ${{env.PROJECT}} + run: dotnet build src/${{env.PROJECT}} --configuration Release --no-restore -p:Deterministic=true + + - name: dotnet test ${{env.PROJECT}} + run: dotnet test test/${{env.PROJECT}}.Tests + + - name: dotnet pack ${{env.PROJECT}} + run: dotnet pack src/${{env.PROJECT}} --configuration Release --no-build + + - name: Publish Artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{env.PROJECT}}-packages + path: '**/${{env.PROJECT}}/bin/**/*.*nupkg' + + - name: Publish Nuget + run: | + nuget push **/${{env.PROJECT}}/bin/**/*.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey ${{ secrets.NUGET_TOKEN }} -SymbolApiKey ${{ secrets.NUGET_TOKEN }} diff --git a/CODEOWNERS b/CODEOWNERS index 12d3abdfd4..d22258b37e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,8 +8,9 @@ src/OpenTelemetry.Contrib.Exporter.Stackdriver/ @ope src/OpenTelemetry.Contrib.Extensions.AWSXRay/ @open-telemetry/dotnet-contrib-approvers @srprash @lupengamzn src/OpenTelemetry.Contrib.Instrumentation.ElasticsearchClient/ @open-telemetry/dotnet-contrib-approvers @ejsmith src/OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore/ @open-telemetry/dotnet-contrib-approvers -src/OpenTelemetry.Contrib.Instrumentation.MassTransit/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy src/OpenTelemetry.Contrib.Instrumentation.GrpcCore/ @open-telemetry/dotnet-contrib-approvers @pcwiese +src/OpenTelemetry.Contrib.Instrumentation.MassTransit/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy +src/OpenTelemetry.Contrib.Instrumentation.Owin/ @open-telemetry/dotnet-contrib-approvers @codeblanch src/OpenTelemetry.Contrib.Instrumentation.Wcf/ @open-telemetry/dotnet-contrib-approvers @codeblanch src/OpenTelemetry.Contrib.Instrumentation.MySqlData/ @open-telemetry/dotnet-contrib-approvers @moonheart src/OpenTelemetry.Contrib.Preview/ @open-telemetry/dotnet-contrib-approvers @codeblanch @@ -18,8 +19,9 @@ test/OpenTelemetry.Contrib.Exporter.Stackdriver.Tests/ @ope test/OpenTelemetry.Contrib.Extensions.AWSXRay.Tests/ @open-telemetry/dotnet-contrib-approvers @srprash @lupengamzn test/OpenTelemetry.Contrib.Instrumentation.ElasticsearchClient.Tests/ @open-telemetry/dotnet-contrib-approvers @ejsmith test/OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCoreTests/ @open-telemetry/dotnet-contrib-approvers -test/OpenTelemetry.Contrib.Instrumentation.MassTransit.Tests/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy test/OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests/ @open-telemetry/dotnet-contrib-approvers @pcwiese +test/OpenTelemetry.Contrib.Instrumentation.MassTransit.Tests/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy +test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch test/OpenTelemetry.Contrib.Instrumentation.Wcf.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/ @open-telemetry/dotnet-contrib-approvers @moonheart test/OpenTelemetry.Contrib.Preview.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index b43fa19ccf..3dd5e62aa5 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -4,11 +4,6 @@ false $(MSBuildThisFileDirectory)/OpenTelemetryContrib.test.ruleset - - true - $(MSBuildThisFileDirectory)/debug.snk - $(DefineConstants);SIGNED - true $(NoWarn),1574,1591 diff --git a/build/Common.prod.props b/build/Common.prod.props index 41dc196e13..fa47e5e8d7 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -1,13 +1,6 @@ - - true - $(MSBuildThisFileDirectory)debug.snk - $(DefineConstants);SIGNED - true - - git https://github.com/open-telemetry/opentelemetry-dotnet-contrib diff --git a/build/Common.props b/build/Common.props index 00cea01585..275cfee36c 100644 --- a/build/Common.props +++ b/build/Common.props @@ -26,9 +26,10 @@ [16.7.1] [2.1.0,5.0) [1.0.0,2.0) + [4.1.1] [3.3.2] [1.0.0,2.0) - [1.1.118,2.0) + [1.2.0-beta.354,2.0) diff --git a/examples/owin/Controllers/TestController.cs b/examples/owin/Controllers/TestController.cs new file mode 100644 index 0000000000..4b8e29eddc --- /dev/null +++ b/examples/owin/Controllers/TestController.cs @@ -0,0 +1,30 @@ +// +// 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.Web.Http; + +namespace Examples.Owin.Controllers +{ + public class TestController : ApiController + { + // GET api/test/{id} + public string Get(string id = null) + { + return $"id:{id}"; + } + } +} diff --git a/examples/owin/Examples.Owin.csproj b/examples/owin/Examples.Owin.csproj new file mode 100644 index 0000000000..0fbabb0964 --- /dev/null +++ b/examples/owin/Examples.Owin.csproj @@ -0,0 +1,18 @@ + + + + Exe + net461 + false + + + + + + + + + + + + diff --git a/examples/owin/Program.cs b/examples/owin/Program.cs new file mode 100644 index 0000000000..9ee8b2ff03 --- /dev/null +++ b/examples/owin/Program.cs @@ -0,0 +1,88 @@ +// +// 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.Diagnostics; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using Microsoft.Owin.Hosting; +using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Owin; + +namespace Examples.Owin +{ + internal static class Program + { + public static void Main() + { + using var host = WebApp.Start( + "http://localhost:9000", + appBuilder => + { + // Add OpenTelemetry early in the pipeline to start timing + // the request as soon as possible. + appBuilder.UseOpenTelemetry(); + + HttpConfiguration config = new HttpConfiguration(); + + config.MessageHandlers.Add(new ActivityDisplayNameRouteEnrichingHandler()); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional }); + + appBuilder.UseWebApi(config); + }); + + using var openTelemetry = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Owin-Example")) + .AddOwinInstrumentation() + .AddConsoleExporter() + .Build(); + + Console.WriteLine("Service listening. Press enter to exit."); + Console.ReadLine(); + } + + private class ActivityDisplayNameRouteEnrichingHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + finally + { + var activity = Activity.Current; + if (activity != null) + { + var routeData = request.GetRouteData(); + if (routeData != null) + { + activity.DisplayName = routeData.Route.RouteTemplate; + } + } + } + } + } + } +} diff --git a/examples/owin/README.md b/examples/owin/README.md new file mode 100644 index 0000000000..db8d94a0c8 --- /dev/null +++ b/examples/owin/README.md @@ -0,0 +1,7 @@ +# OWIN Instrumentation for OpenTelemetry .NET - Example + +An example application that shows how to use +`OpenTelemetry.Contrib.Instrumentation.Owin` to capture telemetry from a +self-hosted WebAPI .NET Framework service. + +Span names are set to the route template resolved by WebAPI. diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index c78fe4d4ff..8cf3585a72 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -36,6 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Instrumentation.GrpcCore.yml = .github\workflows\package-Instrumentation.GrpcCore.yml .github\workflows\package-Instrumentation.MassTransit.yml = .github\workflows\package-Instrumentation.MassTransit.yml .github\workflows\package-Instrumentation.MySqlData.yml = .github\workflows\package-Instrumentation.MySqlData.yml + .github\workflows\package-Instrumentation.Owin.yml = .github\workflows\package-Instrumentation.Owin.yml .github\workflows\package-Instrumentation.Wcf.yml = .github\workflows\package-Instrumentation.Wcf.yml .github\workflows\package-Preview.yml = .github\workflows\package-Preview.yml .github\workflows\pr_build.yml = .github\workflows\pr_build.yml @@ -143,6 +144,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Exten EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests", "test\OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests\OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj", "{771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.Owin", "src\OpenTelemetry.Contrib.Instrumentation.Owin\OpenTelemetry.Contrib.Instrumentation.Owin.csproj", "{530255C1-D130-4B43-981D-911E54F7C787}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "owin", "owin", "{8D11A34C-D0EF-4DE1-8230-32168E67044D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Owin", "examples\owin\Examples.Owin.csproj", "{6B3AA3F2-89A7-433F-918A-1E5E6AAF8423}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.Owin.Tests", "test\OpenTelemetry.Contrib.Instrumentation.Owin.Tests\OpenTelemetry.Contrib.Instrumentation.Owin.Tests.csproj", "{D52558C8-B7BF-4F59-A0FA-9AA629E68012}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -273,6 +282,18 @@ Global {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Debug|Any CPU.Build.0 = Debug|Any CPU {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Release|Any CPU.Build.0 = Release|Any CPU + {530255C1-D130-4B43-981D-911E54F7C787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {530255C1-D130-4B43-981D-911E54F7C787}.Debug|Any CPU.Build.0 = Debug|Any CPU + {530255C1-D130-4B43-981D-911E54F7C787}.Release|Any CPU.ActiveCfg = Release|Any CPU + {530255C1-D130-4B43-981D-911E54F7C787}.Release|Any CPU.Build.0 = Release|Any CPU + {6B3AA3F2-89A7-433F-918A-1E5E6AAF8423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B3AA3F2-89A7-433F-918A-1E5E6AAF8423}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B3AA3F2-89A7-433F-918A-1E5E6AAF8423}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B3AA3F2-89A7-433F-918A-1E5E6AAF8423}.Release|Any CPU.Build.0 = Release|Any CPU + {D52558C8-B7BF-4F59-A0FA-9AA629E68012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D52558C8-B7BF-4F59-A0FA-9AA629E68012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D52558C8-B7BF-4F59-A0FA-9AA629E68012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D52558C8-B7BF-4F59-A0FA-9AA629E68012}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -314,6 +335,10 @@ Global {4172D671-3AAC-443F-9EB0-A6608B154AF0} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {9C2D6D1A-8580-4527-B718-E206D0690635} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {530255C1-D130-4B43-981D-911E54F7C787} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {8D11A34C-D0EF-4DE1-8230-32168E67044D} = {B75EE478-97F7-4E9F-9A5A-DB3D0988EDEA} + {6B3AA3F2-89A7-433F-918A-1E5E6AAF8423} = {8D11A34C-D0EF-4DE1-8230-32168E67044D} + {D52558C8-B7BF-4F59-A0FA-9AA629E68012} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/src/OpenTelemetry.Contrib.Extensions.AWSXRay/AWSXRayIdGenerator.cs b/src/OpenTelemetry.Contrib.Extensions.AWSXRay/AWSXRayIdGenerator.cs index 770fe9d18a..5e05d14436 100644 --- a/src/OpenTelemetry.Contrib.Extensions.AWSXRay/AWSXRayIdGenerator.cs +++ b/src/OpenTelemetry.Contrib.Extensions.AWSXRay/AWSXRayIdGenerator.cs @@ -179,7 +179,7 @@ private static ActivitySamplingResult ComputeRootActivitySamplingResult( { SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded, SamplingDecision.RecordOnly => ActivitySamplingResult.AllData, - _ => ActivitySamplingResult.PropagationData + _ => ActivitySamplingResult.PropagationData, }; if (activitySamplingResult != ActivitySamplingResult.PropagationData) diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/AppBuilderExtensions.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/AppBuilderExtensions.cs new file mode 100644 index 0000000000..902cfe3286 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/AppBuilderExtensions.cs @@ -0,0 +1,35 @@ +// +// 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.Diagnostics; +using OpenTelemetry.Contrib.Instrumentation.Owin; + +namespace Owin +{ + /// + /// Provides extension methods for the class. + /// + public static class AppBuilderExtensions + { + /// Adds a component to the OWIN pipeline for instrumenting + /// incoming request with and notifying listeners + /// with . + /// . + /// Supplied for chaining. + public static IAppBuilder UseOpenTelemetry(this IAppBuilder appBuilder) + => appBuilder.Use(); + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/AssemblyInfo.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/AssemblyInfo.cs new file mode 100644 index 0000000000..f4836c41f8 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/AssemblyInfo.cs @@ -0,0 +1,26 @@ +// +// 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.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Contrib.Instrumentation.Owin.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.Owin.Tests")] +#endif + +[assembly: CLSCompliant(false)] diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/CHANGELOG.md b/src/OpenTelemetry.Contrib.Instrumentation.Owin/CHANGELOG.md new file mode 100644 index 0000000000..134621e04d --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +* Initial release diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/DiagnosticsMiddleware.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/DiagnosticsMiddleware.cs new file mode 100644 index 0000000000..058500e658 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/DiagnosticsMiddleware.cs @@ -0,0 +1,224 @@ +// +// 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.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Owin; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin +{ + /// + /// Instruments incoming request with and notifies listeners with . + /// + internal sealed class DiagnosticsMiddleware : OwinMiddleware + { + private const string ContextKey = "__OpenTelemetry.Context__"; + private static readonly Func> OwinRequestHeaderValuesGetter + = (request, name) => request.Headers.GetValues(name); + + /// + /// Initializes a new instance of the class. + /// + /// An optional pointer to the next component. + public DiagnosticsMiddleware(OwinMiddleware next) + : base(next) + { + } + + /// + public override async Task Invoke(IOwinContext owinContext) + { + try + { + BeginRequest(owinContext); + await this.Next.Invoke(owinContext).ConfigureAwait(false); + RequestEnd(owinContext, null); + } + catch (Exception ex) + { + RequestEnd(owinContext, ex); + throw; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BeginRequest(IOwinContext owinContext) + { + try + { + if (OwinInstrumentationActivitySource.Options == null || OwinInstrumentationActivitySource.Options.Filter?.Invoke(owinContext) == false) + { + OwinInstrumentationEventSource.Log.RequestIsFilteredOut(); + return; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + OwinInstrumentationEventSource.Log.RequestFilterException(ex); + return; + } + + var textMapPropagator = Propagators.DefaultTextMapPropagator; + var ctx = textMapPropagator.Extract(default, owinContext.Request, OwinRequestHeaderValuesGetter); + + Activity activity = OwinInstrumentationActivitySource.ActivitySource.StartActivity( + OwinInstrumentationActivitySource.IncomingRequestActivityName, + ActivityKind.Server, + ctx.ActivityContext); + + if (activity != null) + { + var request = owinContext.Request; + + /* + * Note: Display name is intentionally set to a low cardinality + * value because OWIN does not expose any kind of + * route/template. See: + * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name + */ + activity.DisplayName = request.Method switch + { + "GET" => "HTTP GET", + "POST" => "HTTP POST", + "PUT" => "HTTP PUT", + "DELETE" => "HTTP DELETE", + _ => $"HTTP {request.Method}", + }; + + if (activity.IsAllDataRequested) + { + if (request.Uri.Port == 80 || request.Uri.Port == 443) + { + activity.SetTag(SemanticConventions.AttributeHttpHost, request.Uri.Host); + } + else + { + activity.SetTag(SemanticConventions.AttributeHttpHost, request.Uri.Host + ":" + request.Uri.Port); + } + + activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method); + activity.SetTag(SemanticConventions.AttributeHttpTarget, request.Uri.AbsolutePath); + activity.SetTag(SemanticConventions.AttributeHttpUrl, GetUriTagValueFromRequestUri(request.Uri)); + + if (request.Headers.TryGetValue("User-Agent", out string[] userAgent) && userAgent.Length > 0) + { + activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent[0]); + } + + try + { + OwinInstrumentationActivitySource.Options.Enrich?.Invoke( + activity, + OwinEnrichEventType.BeginRequest, + owinContext, + null); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + OwinInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + + if (!(textMapPropagator is TraceContextPropagator)) + { + Baggage.Current = ctx.Baggage; + } + + owinContext.Environment[ContextKey] = activity; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RequestEnd(IOwinContext owinContext, Exception exception) + { + if (owinContext.Environment.TryGetValue(ContextKey, out object context) + && context is Activity activity) + { + if (Activity.Current != activity) + { + Activity.Current = activity; + } + + if (activity.IsAllDataRequested) + { + var response = owinContext.Response; + + if (exception != null) + { + activity.SetStatus(Status.Error); + + if (OwinInstrumentationActivitySource.Options.RecordException) + { + activity.RecordException(exception); + } + } + else if (activity.GetStatus().StatusCode == StatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(response.StatusCode)); + } + + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, response.StatusCode); + + try + { + OwinInstrumentationActivitySource.Options.Enrich?.Invoke( + activity, + OwinEnrichEventType.EndRequest, + owinContext, + exception); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + OwinInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + + activity.Stop(); + + if (!(Propagators.DefaultTextMapPropagator is TraceContextPropagator)) + { + Baggage.Current = default; + } + } + } + + /// + /// Gets the OpenTelemetry standard uri tag value for a span based on its request . + /// + /// . + /// Span uri value. + private static string GetUriTagValueFromRequestUri(Uri uri) + { + if (string.IsNullOrEmpty(uri.UserInfo)) + { + return uri.ToString(); + } + + return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.PathAndQuery, uri.Fragment); + } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationActivitySource.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationActivitySource.cs new file mode 100644 index 0000000000..60bfb6e358 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationActivitySource.cs @@ -0,0 +1,33 @@ +// +// 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.Diagnostics; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin +{ + internal static class OwinInstrumentationActivitySource + { + public const string ActivitySourceName = "OpenTelemetry.OWIN"; + public const string IncomingRequestActivityName = ActivitySourceName + ".IncomingRequest"; + + private static readonly Version Version = typeof(OwinInstrumentationActivitySource).Assembly.GetName().Version; + + public static ActivitySource ActivitySource { get; } = new ActivitySource(ActivitySourceName, Version.ToString()); + + public static OwinInstrumentationOptions Options { get; set; } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationEventSource.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationEventSource.cs new file mode 100644 index 0000000000..021d137d90 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/Implementation/OwinInstrumentationEventSource.cs @@ -0,0 +1,94 @@ +// +// 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.Diagnostics.Tracing; +using System.Globalization; +using System.Threading; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin +{ + /// + /// EventSource events emitted from the project. + /// + [EventSource(Name = "OpenTelemetry-Instrumentation-Owin")] + internal sealed class OwinInstrumentationEventSource : EventSource + { + public static OwinInstrumentationEventSource Log { get; } = new OwinInstrumentationEventSource(); + + [NonEvent] + public void RequestFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1))) + { + this.RequestFilterException(ToInvariantString(ex)); + } + } + + [Event(EventIds.RequestIsFilteredOut, Message = "Request is filtered out.", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut() + { + this.WriteEvent(EventIds.RequestIsFilteredOut); + } + + [Event(EventIds.RequestFilterException, Message = "InstrumentationFilter threw exception. Request will not be collected. Exception {0}.", Level = EventLevel.Error)] + public void RequestFilterException(string exception) + { + this.WriteEvent(EventIds.RequestFilterException, exception); + } + + [NonEvent] + public void EnrichmentException(Exception exception) + { + if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1))) + { + this.EnrichmentException(ToInvariantString(exception)); + } + } + + [Event(EventIds.EnrichmentException, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(EventIds.EnrichmentException, exception); + } + + /// + /// Returns a culture-independent string representation of the given object, + /// appropriate for diagnostics tracing. + /// + private static string ToInvariantString(Exception exception) + { + var originalUICulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + return exception.ToString(); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalUICulture; + } + } + + private class EventIds + { + public const int RequestIsFilteredOut = 1; + public const int RequestFilterException = 2; + public const int EnrichmentException = 3; + } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/OpenTelemetry.Contrib.Instrumentation.Owin.csproj b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OpenTelemetry.Contrib.Instrumentation.Owin.csproj new file mode 100644 index 0000000000..86e437fbde --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OpenTelemetry.Contrib.Instrumentation.Owin.csproj @@ -0,0 +1,21 @@ + + + net461 + OpenTelemetry instrumentation for OWIN + $(PackageTags);distributed-tracing;OWIN + Instrumentation.Owin- + true + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinEnrichEventType.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinEnrichEventType.cs new file mode 100644 index 0000000000..9d5e3260c3 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinEnrichEventType.cs @@ -0,0 +1,36 @@ +// +// 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.Diagnostics; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin +{ + /// + /// Describes the possible events fired when enriching an . + /// + public enum OwinEnrichEventType + { + /// + /// Begin request. + /// + BeginRequest, + + /// + /// End request. + /// + EndRequest, + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinInstrumentationOptions.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinInstrumentationOptions.cs new file mode 100644 index 0000000000..0739e587fd --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/OwinInstrumentationOptions.cs @@ -0,0 +1,50 @@ +// +// 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.Diagnostics; +using Microsoft.Owin; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin +{ + /// + /// Options for requests instrumentation. + /// + public class OwinInstrumentationOptions + { + /// + /// Gets or sets a Filter function that determines whether or not to collect telemetry about requests on a per request basis. + /// The Filter gets the , and should return a boolean. + /// If Filter returns true, the request is collected. + /// If Filter returns false or throw exception, the request is filtered out. + /// + public Func Filter { get; set; } + + /// + /// Gets or sets an action to enrich the created by the instrumentation. + /// + public Action Enrich { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be recorded as or not. + /// + /// + /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. + /// + public bool RecordException { get; set; } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/README.md b/src/OpenTelemetry.Contrib.Instrumentation.Owin/README.md new file mode 100644 index 0000000000..c60f2c6135 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/README.md @@ -0,0 +1,114 @@ +# OWIN Instrumentation for OpenTelemetry .NET + +[![nuget](https://img.shields.io/nuget/v/OpenTelemetry.Contrib.Instrumentation.Own.svg)](https://www.nuget.org/packages/OpenTelemetry.Contrib.Instrumentation.Owin/) + +This is an [Instrumentation +Library](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/glossary.md#instrumentation-library), +which instruments [OWIN/Katana](https://github.com/aspnet/AspNetKatana/) and +collects telemetry about incoming requests. + +## Steps to enable OpenTelemetry.Contrib.Instrumentation.Owin + +An example project is available in the +[examples/owin](../../examples/owin/) folder. + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Contrib.Instrumentation.Owin`](https://www.nuget.org/packages/opentelemetry.contrib.instrumentation.owin) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package OpenTelemetry.Contrib.Instrumentation.Owin +``` + +### Step 2: Configure OWIN middleware + +Call the `UseOpenTelemetry` `IAppBuilder` extension to register OpenTelemetry +middleware which emits diagnostic events from th OWIN pipeline. This should be +done before any other middleware registrations. + +```csharp + using var host = WebApp.Start( + "http://localhost:9000", + appBuilder => + { + appBuilder.UseOpenTelemetry(); + }); +``` + +### Step 3: Configure OpenTelemetry TracerProvider + +Call the `AddOwinInstrumentation` `TracerProviderBuilder` extension to register +OpenTelemetry instrumentation which listens to the OWIN diagnostic events. + +```csharp + using var openTelemetry = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Owin-Example")) + .AddOwinInstrumentation() + .AddConsoleExporter() + .Build(); +``` + +## Customize OWIN span names + +The OpenTelemetry OWIN instrumentation will create spans with very generic names +based on the http method of the request. For example: `HTTP GET` or `HTTP POST`. +The reason for this is the [OpenTelemetry Specification http semantic +conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name) +call specifically for low cardinality values and OWIN does not expose any kind +of route template. + +To change the span name set `Activity.Current.DisplayName` to the value you want +to display once a route has been resolved. Here is how this can be done using WebAPI: + +```csharp + using var host = WebApp.Start( + "http://localhost:9000", + appBuilder => + { + appBuilder.UseOpenTelemetry(); + + HttpConfiguration config = new HttpConfiguration(); + + config.MessageHandlers.Add(new ActivityDisplayNameRouteEnrichingHandler()); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional }); + + appBuilder.UseWebApi(config); + }); + + private class ActivityDisplayNameRouteEnrichingHandler : DelegatingHandler + { + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + finally + { + var activity = Activity.Current; + if (activity != null) + { + var routeData = request.GetRouteData(); + if (routeData != null) + { + activity.DisplayName = routeData.Route.RouteTemplate; + } + } + } + } + } +``` + +## References + +* [Open Web Interface for .NET](http://owin.org/) +* [Katana Project](https://github.com/aspnet/AspNetKatana/) +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Contrib.Instrumentation.Owin/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Contrib.Instrumentation.Owin/TracerProviderBuilderExtensions.cs new file mode 100644 index 0000000000..f34ecfa534 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.Owin/TracerProviderBuilderExtensions.cs @@ -0,0 +1,50 @@ +// +// 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 OpenTelemetry.Contrib.Instrumentation.Owin; + +namespace OpenTelemetry.Trace +{ + /// + /// Extension methods to simplify registering of OWIN request instrumentation. + /// + public static class TracerProviderBuilderExtensions + { + /// + /// Enables the incoming requests automatic data collection for OWIN. + /// + /// being configured. + /// OWIN Request configuration options. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOwinInstrumentation( + this TracerProviderBuilder builder, + Action configureOwinInstrumentationOptions = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var owinOptions = new OwinInstrumentationOptions(); + configureOwinInstrumentationOptions?.Invoke(owinOptions); + + OwinInstrumentationActivitySource.Options = owinOptions; + + return builder.AddSource(OwinInstrumentationActivitySource.ActivitySourceName); + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/Controllers/TestController.cs b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/Controllers/TestController.cs new file mode 100644 index 0000000000..661126c47d --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/Controllers/TestController.cs @@ -0,0 +1,30 @@ +// +// 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.Web.Http; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin.Tests.Controllers +{ + public class TestController : ApiController + { + // GET api/test/{id} + public string Get(string id = null) + { + return $"id:{id}"; + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/DiagnosticsMiddlewareTests.cs b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/DiagnosticsMiddlewareTests.cs new file mode 100644 index 0000000000..858232aa5e --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/DiagnosticsMiddlewareTests.cs @@ -0,0 +1,227 @@ +// +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using Microsoft.Owin; +using Microsoft.Owin.Hosting; +using OpenTelemetry.Trace; +using Owin; +using Xunit; + +namespace OpenTelemetry.Contrib.Instrumentation.Owin.Tests +{ + public class DiagnosticsMiddlewareTests : IDisposable + { + private readonly Uri serviceBaseUri; + private readonly IDisposable listener; + private readonly EventWaitHandle requestCompleteHandle = new(false, EventResetMode.AutoReset); + + public DiagnosticsMiddlewareTests() + { + Random random = new Random(); + var retryCount = 5; + while (retryCount > 0) + { + try + { + this.serviceBaseUri = new Uri($"http://localhost:{random.Next(2000, 5000)}/"); + + this.listener = WebApp.Start( + this.serviceBaseUri.ToString(), + appBuilder => + { + appBuilder.Use((context, next) => + { + try + { + return next(); + } + finally + { + this.requestCompleteHandle?.Set(); + } + }); + + appBuilder.UseOpenTelemetry(); + + appBuilder.Use((context, next) => + { + if (context.Request.Path == new PathString("/exception")) + { + context.Response.StatusCode = 500; + throw new InvalidOperationException("Unhandled exception requested by caller."); + } + + return next(); + }); + + HttpConfiguration config = new HttpConfiguration(); + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional }); + + appBuilder.UseWebApi(config); + }); + break; + } + catch + { + this.listener.Dispose(); + this.listener = null; + retryCount--; + } + } + + if (this.listener == null) + { + throw new InvalidOperationException("HttpListener could not be started."); + } + } + + public void Dispose() + { + this.listener?.Dispose(); + this.requestCompleteHandle?.Dispose(); + } + + [Theory] + [InlineData(true, false)] + [InlineData(true, true)] + [InlineData(false)] + [InlineData(true, false, true)] + [InlineData(true, false, true, true)] + [InlineData(true, false, false, false, true)] + [InlineData(true, false, false, false, true, true)] + public async Task OutgoingRequestInstrumentationTest( + bool instrument, + bool filter = false, + bool enrich = false, + bool enrichmentException = false, + bool generateRemoteException = false, + bool recordException = false) + { + List stoppedActivities = new List(); + + var builder = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(stoppedActivities); + + if (instrument) + { + builder + .AddOwinInstrumentation(options => + { + if (enrich) + { + if (!enrichmentException) + { + options.Enrich = (activity, eventName, context, exception) => + { + switch (eventName) + { + case OwinEnrichEventType.BeginRequest: + activity.SetTag("client.beginrequest", nameof(OwinEnrichEventType.BeginRequest)); + break; + case OwinEnrichEventType.EndRequest: + activity.SetTag("client.endrequest", nameof(OwinEnrichEventType.EndRequest)); + break; + } + }; + } + else + { + options.Enrich = (activity, eventName, context, exception) => throw new Exception("Error while enriching activity"); + } + } + + options.Filter = _ => !filter; + options.RecordException = recordException; + }); + } + + using TracerProvider tracerProvider = builder.Build(); + + using HttpClient client = new HttpClient(); + + Uri requestUri = generateRemoteException + ? new Uri($"{this.serviceBaseUri}exception") + : new Uri($"{this.serviceBaseUri}api/test"); + + this.requestCompleteHandle.Reset(); + + using var response = await client.GetAsync(requestUri).ConfigureAwait(false); + + /* Note: This code will continue executing as soon as the response + is available but Owin could still be working. We need to wait until + Owin has finished to inspect the activity status. */ + + Assert.True(this.requestCompleteHandle.WaitOne(3000)); + + if (instrument) + { + if (!filter) + { + Assert.NotEmpty(stoppedActivities); + Assert.Single(stoppedActivities); + + Activity activity = stoppedActivities[0]; + Assert.Equal(OwinInstrumentationActivitySource.IncomingRequestActivityName, activity.OperationName); + + Assert.Equal(requestUri.Host + ":" + requestUri.Port, activity.TagObjects.FirstOrDefault(t => t.Key == SemanticConventions.AttributeHttpHost).Value); + Assert.Equal("GET", activity.TagObjects.FirstOrDefault(t => t.Key == SemanticConventions.AttributeHttpMethod).Value); + Assert.Equal(requestUri.AbsolutePath, activity.TagObjects.FirstOrDefault(t => t.Key == SemanticConventions.AttributeHttpTarget).Value); + Assert.Equal(requestUri.ToString(), activity.TagObjects.FirstOrDefault(t => t.Key == SemanticConventions.AttributeHttpUrl).Value); + + Assert.Equal(generateRemoteException ? 500 : 200, activity.TagObjects.FirstOrDefault(t => t.Key == SemanticConventions.AttributeHttpStatusCode).Value); + if (generateRemoteException) + { + Assert.Equal(Status.Error, activity.GetStatus()); + + if (recordException) + { + Assert.Contains(activity.Events, ae => ae.Name == SemanticConventions.AttributeExceptionEventName); + } + } + else + { + Assert.Equal(Status.Unset, activity.GetStatus()); + } + + if (enrich && !enrichmentException) + { + Assert.Equal(nameof(OwinEnrichEventType.BeginRequest), activity.TagObjects.Single(t => t.Key == "client.beginrequest").Value); + Assert.Equal(nameof(OwinEnrichEventType.EndRequest), activity.TagObjects.Single(t => t.Key == "client.endrequest").Value); + } + } + else + { + Assert.Empty(stoppedActivities); + } + } + else + { + Assert.Empty(stoppedActivities); + } + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/OpenTelemetry.Contrib.Instrumentation.Owin.Tests.csproj b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/OpenTelemetry.Contrib.Instrumentation.Owin.Tests.csproj new file mode 100644 index 0000000000..7b9472399a --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.Owin.Tests/OpenTelemetry.Contrib.Instrumentation.Owin.Tests.csproj @@ -0,0 +1,27 @@ + + + + Unit test project for OpenTelemetry OWIN instrumentation + net461 + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + +