From 6d349c412fdffb1b542662e30d47f9d3b7337198 Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Thu, 2 May 2024 14:06:36 -0700 Subject: [PATCH 1/2] [Exporter.Geneva] Release prep 1.8.0-rc.1 (#1697) --- src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index 9113ce3932..03533c9b0a 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -1,12 +1,17 @@ # Changelog -## Unreleased +## 1.8.0-rc.1 + +Released 2024-May-02 * Update OpenTelemetry SDK version to `1.8.0-rc.1`. ([#1689](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1689)) ## 1.8.0-beta.1 +**(This version has been unlisted due to incorrect dependency on stable sdk +version 1.8.1 that prevents ability to use exemplars)** + Released 2024-Apr-23 * Fix a bug in `GenevaMetricExporter` where the `MetricEtwDataTransport` singleton From 6aed975f113ecf3c7994f0a519669e660c8974a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Mon, 6 May 2024 22:54:37 +0200 Subject: [PATCH 2/2] [Instrumentation.GrpcNetClient] Move package from main repository (#1704) --- .../comp_instrumentation_grpcnetclient.md | 41 ++ .github/codecov.yml | 5 + .github/workflows/ci.yml | 16 + .../package-Instrumentation.GrpcNetClient.yml | 21 + ...lemetry.Instrumentation.GrpcNetClient.proj | 32 ++ opentelemetry-dotnet-contrib.sln | 16 + .../.publicApi/PublicAPI.Shipped.txt | 0 .../.publicApi/PublicAPI.Unshipped.txt | 12 + .../AssemblyInfo.cs | 10 + .../CHANGELOG.md | 266 +++++++++++ .../GrpcClientInstrumentation.cs | 30 ++ .../GrpcClientTraceInstrumentationOptions.cs | 35 ++ .../GrpcTagHelper.cs | 76 +++ .../GrpcClientDiagnosticListener.cs | 201 ++++++++ .../GrpcInstrumentationEventSource.cs | 52 +++ ...metry.Instrumentation.GrpcNetClient.csproj | 40 ++ .../README.md | 140 ++++++ .../StatusCanonicalCode.cs | 135 ++++++ .../TracerProviderBuilderExtensions.cs | 68 +++ .../OpenTelemetry.Instrumentation.Http.csproj | 1 + .../Shared}/ActivityHelperExtensions.cs | 2 +- src/Shared/AssemblyVersionExtensions.cs | 2 +- .../HttpRequestMessageContextPropagation.cs | 2 +- ...nTelemetry.AotCompatibility.TestApp.csproj | 1 + .../TestActivityExportProcessor.cs | 2 + .../AWSLambdaWrapperTests.cs | 1 - ...try.Instrumentation.AWSLambda.Tests.csproj | 2 +- ...emetry.Instrumentation.AspNet.Tests.csproj | 2 +- ...mentation.ElasticsearchClient.Tests.csproj | 2 +- .../EventSourceTest.cs | 17 + .../GrpcServer.cs | 90 ++++ .../GrpcTagHelperTests.cs | 66 +++ .../GrpcTestHelpers/ClientTestHelpers.cs | 76 +++ .../GrpcTestHelpers/ResponseUtils.cs | 76 +++ .../GrpcTestHelpers/TestHttpMessageHandler.cs | 41 ++ .../GrpcTestHelpers/TrailingHeadersHelpers.cs | 47 ++ .../GrpcTests.client.cs | 441 ++++++++++++++++++ .../GrpcTests.server.cs | 205 ++++++++ ...Instrumentation.GrpcNetClient.Tests.csproj | 43 ++ .../Proto/greet.proto | 35 ++ .../Services/GreeterService.cs | 39 ++ ...elemetry.Instrumentation.Http.Tests.csproj | 2 +- ...try.Instrumentation.SqlClient.Tests.csproj | 2 +- .../SqlClientTests.cs | 2 + ...lClientTraceInstrumentationOptionsTests.cs | 1 - ...isProfilerEntryToActivityConverterTests.cs | 1 - ...umentation.StackExchangeRedis.Tests.csproj | 2 +- 47 files changed, 2387 insertions(+), 12 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/comp_instrumentation_grpcnetclient.md create mode 100644 .github/workflows/package-Instrumentation.GrpcNetClient.yml create mode 100644 build/Projects/OpenTelemetry.Instrumentation.GrpcNetClient.proj create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Shipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs create mode 100644 src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs rename {test/OpenTelemetry.Contrib.Tests.Shared => src/Shared}/ActivityHelperExtensions.cs (93%) rename src/{OpenTelemetry.Instrumentation.Http => Shared}/HttpRequestMessageContextPropagation.cs (89%) create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/EventSourceTest.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcServer.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTagHelperTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ClientTestHelpers.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ResponseUtils.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.client.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.server.cs create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Proto/greet.proto create mode 100644 test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Services/GreeterService.cs diff --git a/.github/ISSUE_TEMPLATE/comp_instrumentation_grpcnetclient.md b/.github/ISSUE_TEMPLATE/comp_instrumentation_grpcnetclient.md new file mode 100644 index 0000000000..fb38e35552 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/comp_instrumentation_grpcnetclient.md @@ -0,0 +1,41 @@ +--- +name: OpenTelemetry.Instrumentation.GrpcNetClient +about: Issue with OpenTelemetry.Instrumentation.GrpcNetClient +labels: comp:instrumentation.grpcnetclient +--- + +# Issue with OpenTelemetry.Instrumentation.GrpcNetClient + +List of [all OpenTelemetry NuGet +packages](https://www.nuget.org/profiles/OpenTelemetry) and version that you are +using (e.g. `OpenTelemetry 1.3.2`): + +* TBD + +Runtime version (e.g. `net462`, `net48`, `net6.0`, `net7.0` etc. You can +find this information from the `*.csproj` file): + +* TBD + +**Is this a feature request or a bug?** + +* [ ] Feature Request +* [ ] Bug + +**What is the expected behavior?** + +What do you expect to see? + +**What is the actual behavior?** + +What did you see instead? If you are reporting a bug, create a self-contained +project using the template of your choice and apply the minimum required code to +result in the issue you're observing. We will close this issue if: + +* The repro project you share with us is complex. We can't investigate custom + projects, so don't point us to such, please. +* If we can not reproduce the behavior you're reporting. + +## Additional Context + +Add any other context about the feature request here. diff --git a/.github/codecov.yml b/.github/codecov.yml index 027a50a663..3198fe49bb 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -68,6 +68,11 @@ flags: paths: - src/OpenTelemetry.Instrumentation.Http + unittests-Instrumentation.GrpcNetClient: + carryforward: true + paths: + - src/OpenTelemetry.Instrumentation.GrpcNetClient + unittests-Instrumentation.Owin: carryforward: true paths: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d789293259..1e01fa622b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: eventcounters: ['*/OpenTelemetry.Instrumentation.EventCounters*/**', 'examples/event-counters/**', '!**/*.md'] extensions: ['*/OpenTelemetry.Extensions/**', '*/OpenTelemetry.Extensions.Tests/**', '!**/*.md'] geneva: ['*/OpenTelemetry.Exporter.Geneva*/**', '!**/*.md'] + grpcnetclient: ['*/OpenTelemetry.Instrumentation.GrpcNetClient*/**', '!**/*.md'] host: ['*/OpenTelemetry.ResourceDetectors.Host*/**', '!**/*.md'] http: ['*/OpenTelemetry.Instrumentation.Http*/**', '!**/*.md'] onecollector: ['*/OpenTelemetry.Instrumentation.OneCollector*/**', '!**/*.md'] @@ -65,6 +66,7 @@ jobs: '!*/OpenTelemetry.Instrumentation.Owin*/**', '!examples/owin/**', '!*/OpenTelemetry.PersistentStorage*/**', + '!*/OpenTelemetry.Instrumentation.GrpcNetClient*/**', '!*/OpenTelemetry.Instrumentation.Http*/**', '!*/OpenTelemetry.Instrumentation.Process*/**', '!examples/process-instrumentation/**', @@ -151,6 +153,17 @@ jobs: project-name: OpenTelemetry.Exporter.Geneva code-cov-name: Exporter.Geneva + build-test-grpcnetclient: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'grpcnetclient') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: OpenTelemetry.Instrumentation.GrpcNetClient + code-cov-name: Instrumentation.GrpcNetClient + build-test-host: needs: detect-changes if: | @@ -339,6 +352,7 @@ jobs: OpenTelemetry.Instrumentation.AspNet.Tests.csproj, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests.csproj, OpenTelemetry.Instrumentation.EventCounters.Tests.csproj, + OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj, OpenTelemetry.Instrumentation.Http.Tests.csproj, OpenTelemetry.Instrumentation.Owin.Tests.csproj, OpenTelemetry.Instrumentation.Process.Tests.csproj, @@ -396,6 +410,7 @@ jobs: || contains(needs.detect-changes.outputs.changes, 'aws') || contains(needs.detect-changes.outputs.changes, 'azure') || contains(needs.detect-changes.outputs.changes, 'extensions') + || contains(needs.detect-changes.outputs.changes, 'grpcnetclient') || contains(needs.detect-changes.outputs.changes, 'host') || contains(needs.detect-changes.outputs.changes, 'http') || contains(needs.detect-changes.outputs.changes, 'processdetector') @@ -422,6 +437,7 @@ jobs: build-test-eventcounters, build-test-extensions, build-test-geneva, + build-test-grpcnetclient, build-test-host, build-test-http, build-test-onecollector, diff --git a/.github/workflows/package-Instrumentation.GrpcNetClient.yml b/.github/workflows/package-Instrumentation.GrpcNetClient.yml new file mode 100644 index 0000000000..3d5410499d --- /dev/null +++ b/.github/workflows/package-Instrumentation.GrpcNetClient.yml @@ -0,0 +1,21 @@ +name: Pack OpenTelemetry.Instrumentation.GrpcNetClient + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Instrumentation.GrpcNetClient-*' # trigger when we create a tag with prefix "Instrumentation.GrpcNetClient-" + +jobs: + call-build-test-pack: + permissions: + contents: write + uses: ./.github/workflows/Component.Package.yml + with: + project-name: OpenTelemetry.Instrumentation.GrpcNetClient + secrets: inherit diff --git a/build/Projects/OpenTelemetry.Instrumentation.GrpcNetClient.proj b/build/Projects/OpenTelemetry.Instrumentation.GrpcNetClient.proj new file mode 100644 index 0000000000..86cbf25ef5 --- /dev/null +++ b/build/Projects/OpenTelemetry.Instrumentation.GrpcNetClient.proj @@ -0,0 +1,32 @@ + + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.Parent.FullName) + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index a49632c03c..1830e85346 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -52,6 +52,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Instrumentation.EntityFrameworkCore.yml = .github\workflows\package-Instrumentation.EntityFrameworkCore.yml .github\workflows\package-Instrumentation.EventCounters.yml = .github\workflows\package-Instrumentation.EventCounters.yml .github\workflows\package-Instrumentation.GrpcCore.yml = .github\workflows\package-Instrumentation.GrpcCore.yml + .github\workflows\package-Instrumentation.GrpcNetClient.yml = .github\workflows\package-Instrumentation.GrpcNetClient.yml .github\workflows\package-Instrumentation.Hangfire.yml = .github\workflows\package-Instrumentation.Hangfire.yml .github\workflows\package-Instrumentation.Http.yml = .github\workflows\package-Instrumentation.Http.yml .github\workflows\package-Instrumentation.MassTransit.yml = .github\workflows\package-Instrumentation.MassTransit.yml @@ -322,6 +323,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projects", "Projects", "{04 build\Projects\OpenTelemetry.Extensions.proj = build\Projects\OpenTelemetry.Extensions.proj build\Projects\OpenTelemetry.Instrumentation.AspNet.proj = build\Projects\OpenTelemetry.Instrumentation.AspNet.proj build\Projects\OpenTelemetry.Instrumentation.EventCounters.proj = build\Projects\OpenTelemetry.Instrumentation.EventCounters.proj + build\Projects\OpenTelemetry.Instrumentation.GrpcNetClient.proj = build\Projects\OpenTelemetry.Instrumentation.GrpcNetClient.proj build\Projects\OpenTelemetry.Instrumentation.Http.proj = build\Projects\OpenTelemetry.Instrumentation.Http.proj build\Projects\OpenTelemetry.Instrumentation.Owin.proj = build\Projects\OpenTelemetry.Instrumentation.Owin.proj build\Projects\OpenTelemetry.Instrumentation.Process.proj = build\Projects\OpenTelemetry.Instrumentation.Process.proj @@ -363,6 +365,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Http.Benchmark", "test\OpenTelemetry.Instrumentation.Http.Benchmark\OpenTelemetry.Instrumentation.Http.Benchmark.csproj", "{1156D564-2E3C-47D6-97C1-FF3ADEDC41C8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.GrpcNetClient", "src\OpenTelemetry.Instrumentation.GrpcNetClient\OpenTelemetry.Instrumentation.GrpcNetClient.csproj", "{0156E342-CE63-46F5-992D-691A7CCB50F8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.GrpcNetClient.Tests", "test\OpenTelemetry.Instrumentation.GrpcNetClient.Tests\OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj", "{2E1A5759-1431-4724-8885-3E9447FBF617}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -733,6 +739,14 @@ Global {1156D564-2E3C-47D6-97C1-FF3ADEDC41C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {1156D564-2E3C-47D6-97C1-FF3ADEDC41C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {1156D564-2E3C-47D6-97C1-FF3ADEDC41C8}.Release|Any CPU.Build.0 = Release|Any CPU + {0156E342-CE63-46F5-992D-691A7CCB50F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0156E342-CE63-46F5-992D-691A7CCB50F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0156E342-CE63-46F5-992D-691A7CCB50F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0156E342-CE63-46F5-992D-691A7CCB50F8}.Release|Any CPU.Build.0 = Release|Any CPU + {2E1A5759-1431-4724-8885-3E9447FBF617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E1A5759-1431-4724-8885-3E9447FBF617}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E1A5759-1431-4724-8885-3E9447FBF617}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E1A5759-1431-4724-8885-3E9447FBF617}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -842,6 +856,8 @@ Global {BE92357F-DE09-477D-AFDB-6AD1D7AC7BA1} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {7371E920-ECD0-403A-A009-7A1A301D9763} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {1156D564-2E3C-47D6-97C1-FF3ADEDC41C8} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {0156E342-CE63-46F5-992D-691A7CCB50F8} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {2E1A5759-1431-4724-8885-3E9447FBF617} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..32e717dc82 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.GrpcClientTraceInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.SuppressDownstreamInstrumentation.get -> bool +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.SuppressDownstreamInstrumentation.set -> void +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs new file mode 100644 index 0000000000..cd07ce49a2 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.GrpcNetClient.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.GrpcNetClient.Tests")] +#endif diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md new file mode 100644 index 0000000000..a7d27f0e5b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md @@ -0,0 +1,266 @@ +# Changelog + +## Unreleased + +* `ActivitySource.Version` is set to NuGet package version. + ([#5498](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5498)) +* Update `OpenTelemetry` to `1.8.1`. + ([#1668](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1668)) + +## 1.8.0-beta.1 + +Released 2024-Apr-04 + +## 1.7.0-beta.1 + +Released 2024-Feb-09 + +* **Breaking Change**: + Please be advised that the + [SuppressDownstreamInstrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.GrpcNetClient#suppressdownstreaminstrumentation) + option no longer works when used in conjunction with the + `OpenTelemetry.Instrumentation.Http` package version `1.6.0` or greater. + This is not a result of a change in the `OpenTelemetry.Instrumentation.GrpcNetClient` + package therefore this also affects versions prior to this release. See this + [issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092) + for details and workaround. +* Removed support for the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable + which toggled the use of the new conventions for the + [server, client, and shared network attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/general/attributes.md#server-client-and-shared-network-attributes). + Now that this suite of attributes are stable, this instrumentation will only + emit the new attributes. + ([#5259](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5259)) +* **Breaking Change**: Renamed `GrpcClientInstrumentationOptions` to + `GrpcClientTraceInstrumentationOptions`. + ([#5272](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5272)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + +## 1.6.0-beta.2 + +Released 2023-Oct-26 + +## 1.5.1-beta.1 + +Released 2023-Jul-20 + +* The new network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) networking + attributes, and stop emitting the old experimental networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) + networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new + network semantic conventions are marked stable. Refer to the + specification for more information regarding the new network + semantic conventions for + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/rpc/rpc-spans.md). + ([#4658](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4658)) + +## 1.5.0-beta.1 + +Released 2023-Jun-05 + +* Bumped the package version to `1.5.0-beta.1` to keep its major and minor + version in sync with that of the core packages. This would make it more + intuitive for users to figure out what version of core packages would work + with a given version of this package. The pre-release identifier has also been + changed from `rc` to `beta` as we believe this more accurately reflects the + status of this package. We believe the `rc` identifier will be more + appropriate as semantic conventions reach stability. + +## 1.0.0-rc9.14 + +Released 2023-Feb-24 + +* Updated OTel SDK dependency to 1.4.0 + +## 1.4.0-rc9.13 + +Released 2023-Feb-10 + +## 1.0.0-rc9.12 + +Released 2023-Feb-01 + +## 1.0.0-rc9.11 + +Released 2023-Jan-09 + +## 1.0.0-rc9.10 + +Released 2022-Dec-12 + +## 1.0.0-rc9.9 + +Released 2022-Nov-07 + + **Breaking change** The `Enrich` callback option has been removed. For better + usability, it has been replaced by two separate options: + `EnrichWithHttpRequestMessage`and `EnrichWithHttpResponseMessage`. Previously, + the single `Enrich` callback required the consumer to detect which event + triggered the callback to be invoked (e.g., request start or response end) and + then cast the object received to the appropriate type: `HttpRequestMessage` + and `HttpResponseMessage`. The separate callbacks make it clear what event + triggers them and there is no longer the need to cast the argument to the + expected type. + ([#3804](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3804)) + +## 1.0.0-rc9.8 + +Released 2022-Oct-17 + +## 1.0.0-rc9.7 + +Released 2022-Sep-29 + +* Added overloads which accept a name to the `TracerProviderBuilder` + `AddGrpcClientInstrumentation` extension to allow for more fine-grained + options management + ([#3665](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3665)) + +## 1.0.0-rc9.6 + +Released 2022-Aug-18 + +* Updated to use Activity native support from `System.Diagnostics.DiagnosticSource` + to set activity status. + ([#3118](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3118)) + ([#3569](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3569)) + +## 1.0.0-rc9.5 + +Released 2022-Aug-02 + +## 1.0.0-rc9.4 + +Released 2022-Jun-03 + +* Add `netstandard2.0` target enabling the Grpc.Net.Client instrumentation to + be consumed by .NET Framework applications. + ([#3105](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3105)) + +## 1.0.0-rc9.3 + +Released 2022-Apr-15 + +## 1.0.0-rc9.2 + +Released 2022-Apr-12 + +## 1.0.0-rc9.1 + +Released 2022-Mar-30 + +## 1.0.0-rc10 (broken. use 1.0.0-rc9.1 and newer) + +Released 2022-Mar-04 + +## 1.0.0-rc9 + +Released 2022-Feb-02 + +## 1.0.0-rc8 + +Released 2021-Oct-08 + +## 1.0.0-rc7 + +Released 2021-Jul-12 + +## 1.0.0-rc6 + +Released 2021-Jun-25 + +## 1.0.0-rc5 + +Released 2021-Jun-09 + +## 1.0.0-rc4 + +Released 2021-Apr-23 + +## 1.0.0-rc3 + +Released 2021-Mar-19 + +* Leverages added AddLegacySource API from OpenTelemetry SDK to trigger Samplers + and ActivityProcessors. Samplers, ActivityProcessor.OnStart will now get the + Activity before any enrichment done by the instrumentation. + ([#1836](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1836)) +* Performance optimization by leveraging sampling decision and short circuiting + activity enrichment. + ([#1903](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1904)) + +## 1.0.0-rc2 + +Released 2021-Jan-29 + +## 1.0.0-rc1.1 + +Released 2020-Nov-17 + +* Add context propagation, when SuppressDownstreamInstrumentation is enabled. + [#1464](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1464) +* GrpcNetClientInstrumentation sets ActivitySource to activities created outside + ActivitySource. + ([#1515](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1515/)) + +## 0.8.0-beta.1 + +Released 2020-Nov-5 + +## 0.7.0-beta.1 + +Released 2020-Oct-16 + +* Instrumentation no longer store raw objects like `HttpRequestMessage` in + Activity.CustomProperty. To enrich activity, use the Enrich action on the + instrumentation. + ([#1261](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1261)) +* Span Status is populated as per new spec + ([#1313](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1313)) + +## 0.6.0-beta.1 + +Released 2020-Sep-15 + +* The `grpc.method` and `grpc.status_code` attributes added by the library are + removed from the span. The information from these attributes is contained in + other attributes that follow the conventions of OpenTelemetry. + ([#1260](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1260)) + +## 0.5.0-beta.2 + +Released 2020-08-28 + +* NuGet package renamed to OpenTelemetry.Instrumentation.GrpcNetClient to more + clearly indicate that this package is specifically for gRPC client + instrumentation. The package was previously named + OpenTelemetry.Instrumentation.Grpc. + ([#1136](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1136)) +* Grpc.Net.Client Instrumentation automatically populates HttpRequest in + Activity custom property + ([#1099](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1099)) + ([#1128](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1128)) + +## 0.4.0-beta.2 + +Released 2020-07-24 + +* First beta release + +## 0.3.0-beta + +Released 2020-07-23 + +* Initial release diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs new file mode 100644 index 0000000000..961ddc1774 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// GrpcClient instrumentation. +/// +internal sealed class GrpcClientInstrumentation : IDisposable +{ + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for Grpc client instrumentation. + public GrpcClientInstrumentation(GrpcClientTraceInstrumentationOptions options = null) + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new GrpcClientDiagnosticListener(options), isEnabledFilter: null, GrpcInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber.Dispose(); + } +} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs new file mode 100644 index 0000000000..b4cd871980 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Options for GrpcClient instrumentation. +/// +public class GrpcClientTraceInstrumentationOptions +{ + /// + /// 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 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 new file mode 100644 index 0000000000..836beb226a --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Text.RegularExpressions; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +internal static class GrpcTagHelper +{ + 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"; + + private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); + + public static string GetGrpcMethodFromActivity(Activity activity) + { + return activity.GetTagValue(GrpcMethodTagName) as string; + } + + 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); + } + + 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; + } + else + { + 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/semantic-conventions/blob/main/docs/rpc/grpc.md#grpc-attributes. + /// + /// 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)) + { + status = ((StatusCanonicalCode)statusCode) switch + { + 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 new file mode 100644 index 0000000000..99e5e89e02 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs @@ -0,0 +1,201 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Reflection; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +internal sealed class GrpcClientDiagnosticListener : ListenerHandler +{ + internal static readonly Assembly Assembly = typeof(GrpcClientDiagnosticListener).Assembly; + internal static readonly AssemblyName AssemblyName = Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly string Version = Assembly.GetPackageVersion(); + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version); + + private const string OnStartEvent = "Grpc.Net.Client.GrpcOut.Start"; + private const string OnStopEvent = "Grpc.Net.Client.GrpcOut.Stop"; + + private static readonly PropertyFetcher StartRequestFetcher = new("Request"); + private static readonly PropertyFetcher StopResponseFetcher = new("Response"); + + private readonly GrpcClientTraceInstrumentationOptions options; + + public GrpcClientDiagnosticListener(GrpcClientTraceInstrumentationOptions options) + : base("Grpc.Net.Client") + { + this.options = options; + } + + public override void OnEventWritten(string name, object payload) + { + switch (name) + { + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } + + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } + + break; + } + } + + 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) + { + return; + } + + // Ensure context propagation irrespective of sampling decision + if (!TryFetchRequest(payload, out HttpRequestMessage request)) + { + GrpcInstrumentationEventSource.Log.NullPayload(nameof(GrpcClientDiagnosticListener), nameof(this.OnStartActivity)); + 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); + } + + if (activity.IsAllDataRequested) + { + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); + + var grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + + activity.DisplayName = grpcMethod?.Trim('/'); + + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + { + 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); + } + + var uriHostNameType = Uri.CheckHostName(request.RequestUri.Host); + + 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); + + try + { + this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + } + catch (Exception ex) + { + GrpcInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + + // See https://github.com/grpc/grpc-dotnet/blob/ff1a07b90c498f259e6d9f4a50cdad7c89ecd3c0/src/Grpc.Net.Client/Internal/GrpcCall.cs#L1180-L1183 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchRequest(object payload, out HttpRequestMessage request) + => StartRequestFetcher.TryFetch(payload, out request) && request != null; + } + + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + if (validConversion) + { + if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + } + + // 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 (TryFetchResponse(payload, out HttpResponseMessage response)) + { + try + { + this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); + } + catch (Exception ex) + { + GrpcInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + } + + // See https://github.com/grpc/grpc-dotnet/blob/ff1a07b90c498f259e6d9f4a50cdad7c89ecd3c0/src/Grpc.Net.Client/Internal/GrpcCall.cs#L1180-L1183 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchResponse(object payload, out HttpResponseMessage response) + => StopResponseFetcher.TryFetch(payload, out response) && response != null; + } +} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs new file mode 100644 index 0000000000..0bfbd38076 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-Grpc")] +internal sealed class GrpcInstrumentationEventSource : EventSource +{ + 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); + } + + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.EnrichmentException(ex.ToInvariantString()); + } + } + + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); + } + } + + [Event(2, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(2, exception); + } + + [Event(3, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(3, handlerName, eventName, ex); + } +} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj new file mode 100644 index 0000000000..f2c3bfdbb3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj @@ -0,0 +1,40 @@ + + + + net8.0;net6.0;netstandard2.1;netstandard2.0 + gRPC for .NET client instrumentation for OpenTelemetry .NET + $(PackageTags);distributed-tracing + Instrumentation.GrpcNetClient- + + enable + + disable + + + + + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md new file mode 100644 index 0000000000..eb6bdd12d9 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md @@ -0,0 +1,140 @@ +# Grpc.Net.Client Instrumentation for OpenTelemetry + +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.GrpcNetClient.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.GrpcNetClient) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.GrpcNetClient.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.GrpcNetClient) + +This is an [Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library) +which instruments [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) +and collects traces about outgoing gRPC requests. + +> [!CAUTION] +> This component is based on the OpenTelemetry semantic conventions for +[traces](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md). +These conventions are +[Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), +and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). +Until a [stable +version](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/telemetry-stability.md) +is released, there can be breaking changes. You can track the progress from +[milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23). + +## Supported .NET Versions + +This package targets +[`NETSTANDARD2.1`](https://docs.microsoft.com/dotnet/standard/net-standard#net-implementation-support) +and hence can be used in any .NET versions implementing `NETSTANDARD2.1`. + +## Steps to enable OpenTelemetry.Instrumentation.GrpcNetClient + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Instrumentation.GrpcNetClient`](https://www.nuget.org/packages/opentelemetry.instrumentation.grpcnetclient) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package --prerelease OpenTelemetry.Instrumentation.GrpcNetClient +``` + +### Step 2: Enable Grpc.Net.Client Instrumentation at application startup + +Grpc.Net.Client instrumentation must be enabled at application startup. + +The following example demonstrates adding Grpc.Net.Client instrumentation to a +console application. This example also sets up the OpenTelemetry Console +exporter and adds instrumentation for HttpClient, which requires adding the +packages +[`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md) +and +[`OpenTelemetry.Instrumentation.Http`](../OpenTelemetry.Instrumentation.Http/README.md) +to the application. As Grpc.Net.Client uses HttpClient underneath, it is +recommended to enable HttpClient instrumentation as well to ensure proper +context propagation. This would cause an activity being produced for both a gRPC +call and its underlying HTTP call. This behavior can be +[configured](#suppressdownstreaminstrumentation). + +```csharp +using OpenTelemetry.Trace; + +public class Program +{ + public static void Main(string[] args) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddConsoleExporter() + .Build(); + } +} +``` + +For an ASP.NET Core application, adding instrumentation is typically done in +the `ConfigureServices` of your `Startup` class. Refer to documentation for +[OpenTelemetry.Instrumentation.AspNetCore](../OpenTelemetry.Instrumentation.AspNetCore/README.md). + +## Advanced configuration + +This instrumentation can be configured to change the default behavior by using +`GrpcClientInstrumentationOptions`. + +### SuppressDownstreamInstrumentation + +> [!CAUTION] +> `SuppressDownstreamInstrumentation` no longer works when used in conjunction +with the `OpenTelemetry.Instrumentation.Http` package version `1.6.0` and greater. +This option may change or even be removed in a future release. + +This option prevents downstream instrumentation from being invoked. +Grpc.Net.Client is built on top of HttpClient. When instrumentation for both +libraries is enabled, `SuppressDownstreamInstrumentation` prevents the +HttpClient instrumentation from generating an additional activity. Additionally, +since HttpClient instrumentation is normally responsible for propagating context +(ActivityContext and Baggage), Grpc.Net.Client instrumentation propagates +context when `SuppressDownstreamInstrumentation` is enabled. + +The following example shows how to use `SuppressDownstreamInstrumentation`. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddGrpcClientInstrumentation( + opt => opt.SuppressDownstreamInstrumentation = true) + .AddHttpClientInstrumentation() + .Build(); +``` + +### Enrich + +This instrumentation library provides `EnrichWithHttpRequestMessage` and +`EnrichWithHttpResponseMessage` options that can be used to enrich the activity +with additional information from the raw `HttpRequestMessage` and +`HttpResponseMessage` objects respectively. These actions are called only when +`activity.IsAllDataRequested` is `true`. It contains the activity itself (which +can be enriched), the name of the event, and the actual raw object. The +following code snippet shows how to add additional tags using these options. + +```csharp +services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddGrpcClientInstrumentation(options => + { + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("requestVersion", httpRequestMessage.Version); + }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => + { + activity.SetTag("responseVersion", httpResponseMessage.Version); + }; + }); +``` + +[Processor](../../docs/trace/extending-the-sdk/README.md#processor), +is the general extensibility point to add additional properties to any activity. +The `Enrich` option is specific to this instrumentation, and is provided to +get access to `HttpRequest` and `HttpResponse`. + +## References + +* [gRPC for .NET](https://github.com/grpc/grpc-dotnet) +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs new file mode 100644 index 0000000000..4ddb1ec16c --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs @@ -0,0 +1,135 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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 +{ + /// + /// 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 new file mode 100644 index 0000000000..aa87988869 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.GrpcNetClient; +using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of 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); + + /// + /// 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); + + name ??= Options.DefaultName; + + if (configure != null) + { + builder.ConfigureServices(services => services.Configure(name, configure)); + } + + builder.AddSource(GrpcClientDiagnosticListener.ActivitySourceName); + builder.AddLegacySource("Grpc.Net.Client.GrpcOut"); + + return builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); + + return new GrpcClientInstrumentation(options); + }); + } +} diff --git a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj index 66a1c2ecb0..d64d96feb9 100644 --- a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj +++ b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj @@ -19,6 +19,7 @@ + diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/ActivityHelperExtensions.cs b/src/Shared/ActivityHelperExtensions.cs similarity index 93% rename from test/OpenTelemetry.Contrib.Tests.Shared/ActivityHelperExtensions.cs rename to src/Shared/ActivityHelperExtensions.cs index a1df29f725..6a85d45527 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/ActivityHelperExtensions.cs +++ b/src/Shared/ActivityHelperExtensions.cs @@ -5,7 +5,7 @@ using System.Diagnostics; -namespace OpenTelemetry.Tests; +namespace OpenTelemetry.Trace; internal static class ActivityHelperExtensions { diff --git a/src/Shared/AssemblyVersionExtensions.cs b/src/Shared/AssemblyVersionExtensions.cs index dd147d21e0..1ae08c8d15 100644 --- a/src/Shared/AssemblyVersionExtensions.cs +++ b/src/Shared/AssemblyVersionExtensions.cs @@ -26,7 +26,7 @@ public static string GetPackageVersion(this Assembly assembly) var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; Debug.Assert(!string.IsNullOrEmpty(informationalVersion), "AssemblyInformationalVersionAttribute was not found in assembly"); -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER var indexOfPlusSign = informationalVersion!.IndexOf('+', StringComparison.Ordinal); #else var indexOfPlusSign = informationalVersion!.IndexOf('+'); diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs b/src/Shared/HttpRequestMessageContextPropagation.cs similarity index 89% rename from src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs rename to src/Shared/HttpRequestMessageContextPropagation.cs index 91d2a5c18a..2e81f52e64 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs +++ b/src/Shared/HttpRequestMessageContextPropagation.cs @@ -5,7 +5,7 @@ using System.Net.Http; #endif -namespace OpenTelemetry.Instrumentation.Http; +namespace OpenTelemetry.Instrumentation; internal static class HttpRequestMessageContextPropagation { diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj index 9c68f55714..934b0d638c 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj +++ b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj @@ -21,6 +21,7 @@ + diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/TestActivityExportProcessor.cs b/test/OpenTelemetry.Contrib.Tests.Shared/TestActivityExportProcessor.cs index 7a3d9e08a7..898c9be4dc 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/TestActivityExportProcessor.cs +++ b/test/OpenTelemetry.Contrib.Tests.Shared/TestActivityExportProcessor.cs @@ -3,7 +3,9 @@ #nullable enable +#pragma warning disable IDE0005 // Using directive is unnecessary. using System.Collections.Generic; +#pragma warning restore IDE0005 // Using directive is unnecessary. using System.Diagnostics; namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/AWSLambdaWrapperTests.cs b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/AWSLambdaWrapperTests.cs index 90b9d26388..e41bdf2ee7 100644 --- a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/AWSLambdaWrapperTests.cs +++ b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/AWSLambdaWrapperTests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using OpenTelemetry.Instrumentation.AWSLambda.Implementation; using OpenTelemetry.Resources; -using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; diff --git a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/OpenTelemetry.Instrumentation.AWSLambda.Tests.csproj b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/OpenTelemetry.Instrumentation.AWSLambda.Tests.csproj index d8c85549f4..91a0bb9a75 100644 --- a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/OpenTelemetry.Instrumentation.AWSLambda.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/OpenTelemetry.Instrumentation.AWSLambda.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj index a040ac447c..38b9d15dce 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests.csproj index 8ae7952754..df940472e4 100644 --- a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/EventSourceTest.cs new file mode 100644 index 0000000000..66db176677 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/EventSourceTest.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class EventSourceTest +{ + [Fact] + public void EventSourceTest_GrpcInstrumentationEventSource() + { + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(GrpcInstrumentationEventSource.Log); + } +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcServer.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcServer.cs new file mode 100644 index 0000000000..08fa9e8c7b --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcServer.cs @@ -0,0 +1,90 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NETFRAMEWORK +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; + +public class GrpcServer : IDisposable + where TService : class +{ + private static readonly Random GlobalRandom = new(); + + 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); + + this.Port = 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) + { + retryCount--; + this.host.Dispose(); + } + } + } + + public int Port { get; } + + 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(); + }); + + return hostBuilder.Build(); + } + + private class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + } + } +} +#endif diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTagHelperTests.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTagHelperTests.cs new file mode 100644 index 0000000000..fbb1928117 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTagHelperTests.cs @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Instrumentation.GrpcNetClient; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class GrpcTagHelperTests +{ + [Fact] + public void GrpcTagHelper_GetGrpcMethodFromActivity() + { + var grpcMethod = "/some.service/somemethod"; + using var activity = new Activity("operationName"); + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, grpcMethod); + + var result = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + + 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); + + 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"); + + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + Assert.True(validConversion); + + var statusCode = GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + + Assert.Equal(ActivityStatusCode.Unset, statusCode); + Assert.Equal(status, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } + + [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)); + } +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ClientTestHelpers.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ClientTestHelpers.cs new file mode 100644 index 0000000000..27c8b5a374 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ClientTestHelpers.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; +using Google.Protobuf; +using Grpc.Net.Compression; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class ClientTestHelpers +{ + 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"); + + return httpClient; + } + + 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; + + 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(); + + 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); +#if NET5_0_OR_GREATER + await ms.WriteAsync(data); +#else + await ms.WriteAsync(data, 0, data.Length); +#endif + } + + private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) + { + await WriteResponseAsync(ms, response, compressionProvider); + } + + 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.GrpcNetClient.Tests/GrpcTestHelpers/ResponseUtils.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ResponseUtils.cs new file mode 100644 index 0000000000..01180ebf54 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/ResponseUtils.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers.Binary; +using System.Diagnostics; +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +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) + { + payload.Headers.ContentType = GrpcContentTypeHeaderValue; + + var message = new HttpResponseMessage(statusCode) + { + Content = payload, + Version = ProtocolVersion, + }; + + message.RequestMessage = new HttpRequestMessage(); +#if NETFRAMEWORK + message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); +#endif + message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); + + if (grpcStatusCode != null) + { + message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); + } + + return message; + } + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + 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 +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs new file mode 100644 index 0000000000..4f1837ce70 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +public class TestHttpMessageHandler : HttpMessageHandler +{ + private readonly Func> sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) + { + this.sendAsync = sendAsync; + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + return new TestHttpMessageHandler(async (request, cancellationToken) => + { + using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + + var result = await Task.WhenAny(sendAsync(request), tcs.Task); + return await result; + }); + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.sendAsync(request, cancellationToken); + } +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs new file mode 100644 index 0000000000..22e6e37eb4 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class TrailingHeadersHelpers +{ + public const string ResponseTrailersKey = "__ResponseTrailers"; + + public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) + { +#if !NETFRAMEWORK + return responseMessage.TrailingHeaders; +#else + 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; +#endif + } + +#if NETFRAMEWORK + public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + { + if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) + { + responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); + } + } + + private class ResponseTrailers : HttpHeaders + { + public static readonly ResponseTrailers Empty = new ResponseTrailers(); + } +#endif +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.client.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.client.cs new file mode 100644 index 0000000000..f06eb5a6fc --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.client.cs @@ -0,0 +1,441 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Net; +using Greet; +#if !NETFRAMEWORK +using Grpc.Core; +#endif +using Grpc.Net.Client; +using Microsoft.Extensions.DependencyInjection; +#if !NETFRAMEWORK +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Tests; +#endif +using OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; +using OpenTelemetry.Instrumentation.GrpcNetClient; +using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; +using OpenTelemetry.Trace; +using Xunit; +using Status = OpenTelemetry.Trace.Status; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +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) + { + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; + + 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()); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); + response.TrailingHeaders().Add("grpc-message", "value"); + return response; + }); + + var exportedItems = new List(); + + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) + { + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + } + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions + { + HttpClient = httpClient, + }); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } + + Assert.Single(exportedItems); + var activity = exportedItems[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)); + + 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(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + 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 NET6_0_OR_GREATER + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GrpcAndHttpClientInstrumentationIsInvoked(bool shouldEnrich) + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + + using var parent = new Activity("parent") + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) + { + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("enrichedWithHttpRequestMessage", "yes"); + }; + + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => + { + activity.SetTag("enrichedWithHttpResponseMessage", "yes"); + }; + } + }) + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .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() + { + HttpClient = new HttpClient(), + }); + + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } + + Assert.Equal(2, exportedItems.Count); + var httpSpan = exportedItems.Single(activity => activity.OperationName == OperationNameHttpOut); + var grpcSpan = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); + + ValidateGrpcActivity(grpcSpan); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan.DisplayName); + Assert.Equal(0, grpcSpan.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal("POST", httpSpan.DisplayName); + Assert.Equal(grpcSpan.SpanId, httpSpan.ParentSpanId); + + if (shouldEnrich) + { + Assert.Single(grpcSpan.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage" && tag.Value == "yes"); + Assert.Single(grpcSpan.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage" && tag.Value == "yes"); + } + else + { + Assert.Empty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); + Assert.Empty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); + } + } + + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcAndHttpClientInstrumentationWithSuppressInstrumentation() + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + + using var parent = new Activity("parent") + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(o => o.SuppressDownstreamInstrumentation = true) + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + 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()); + }); + } + + Assert.Equal(4, exportedItems.Count); + var grpcSpan1 = exportedItems[0]; + var grpcSpan2 = exportedItems[1]; + var grpcSpan3 = exportedItems[2]; + var grpcSpan4 = exportedItems[3]; + + 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(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)); + } + + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcPropagatesContextWithSuppressInstrumentationOptionSetToTrue() + { + try + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + + using var source = new ActivitySource("test-source"); + + var propagator = new CustomTextMapPropagator(); + propagator.InjectValues.Add("customField", context => "customValue"); + + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator, + })); + + 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 + .AddInMemoryExporter(exportedItems) + .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()); + } + + var serverActivity = exportedItems.Single(activity => activity.OperationName == OperationNameHttpRequestIn); + var clientActivity = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); + + Assert.Equal($"greet.Greeter/SayHello", clientActivity.DisplayName); + Assert.Equal($"POST /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[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + [Fact] + public void GrpcDoesNotPropagateContextWithSuppressInstrumentationOptionSetToFalse() + { + try + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + using var source = new ActivitySource("test-source"); + + bool isPropagatorCalled = false; + var propagator = new CustomTextMapPropagator + { + Injected = (context) => isPropagatorCalled = true, + }; + + Sdk.SetDefaultTextMapPropagator(propagator); + + var headers = new Metadata(); + + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => + { + o.SuppressDownstreamInstrumentation = false; + }) + .AddInMemoryExporter(exportedItems) + .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(2, exportedItems.Count); + + var parentActivity = exportedItems.Single(activity => activity.OperationName == "parent"); + var clientActivity = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); + + Assert.Equal(clientActivity.ParentSpanId, parentActivity.SpanId); + + // Propagator is not called + Assert.False(isPropagatorCalled); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() + { + try + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + + using var source = new ActivitySource("test-source"); + + bool isPropagatorCalled = false; + var propagator = new CustomTextMapPropagator(); + propagator.Injected = (context) => isPropagatorCalled = true; + + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator, + })); + + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => + { + o.SuppressDownstreamInstrumentation = true; + }) + .AddInMemoryExporter(exportedItems) + .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()); + } + } + + // If suppressed, activity is not emitted and + // propagation is also not performed. + Assert.Single(exportedItems); + 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; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddGrpcClientInstrumentation() + .AddGrpcClientInstrumentation("Instrumentation2", configure: null) + .Build(); + + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } + + [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, activityToValidate.Source.Version); + Assert.Equal(ActivityKind.Client, activityToValidate.Kind); + } +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.server.cs new file mode 100644 index 0000000000..1c397a27ab --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/GrpcTests.server.cs @@ -0,0 +1,205 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET6_0_OR_GREATER +using System.Diagnostics; +using System.Net; +using Greet; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Instrumentation.Grpc.Services.Tests; +using OpenTelemetry.Instrumentation.GrpcNetClient; +using OpenTelemetry.Trace; +using Xunit; +using Status = OpenTelemetry.Trace.Status; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +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() + { + this.server = new GrpcServer(); + } + + [Theory] + [InlineData(null)] + [InlineData("true")] + [InlineData("false")] + [InlineData("True")] + [InlineData("False")] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(string enableGrpcAspNetCoreSupport) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = enableGrpcAspNetCoreSupport, + }) + .Build(); + + var exportedItems = new List(); + using var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; + var uri = new Uri($"http://localhost:{this.server.Port}"); + + using var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var returnMsg = client.SayHello(new HelloRequest()).Message; + Assert.False(string.IsNullOrEmpty(returnMsg)); + + WaitForExporterToReceiveItems(exportedItems, 1); + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(ActivityKind.Server, activity.Kind); + + if (enableGrpcAspNetCoreSupport != null && enableGrpcAspNetCoreSupport.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); + Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); + Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); + Assert.Contains(activity.GetTagValue(SemanticConventions.AttributeClientAddress), clientLoopbackAddresses); + Assert.NotEqual(0, activity.GetTagValue(SemanticConventions.AttributeClientPort)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } + else + { + Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + } + + Assert.Equal(Status.Unset, activity.GetStatus()); + + // The following are http.* attributes that are also included on the span for the gRPC invocation. + Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal(this.server.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); + Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeUrlPath)); + Assert.Equal("2", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); + Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal) as string); + } + +#if NET6_0_OR_GREATER + [Theory(Skip = "Skipping for .NET 6 and higher due to bug #3023")] +#endif + [InlineData(null)] + [InlineData("true")] + [InlineData("false")] + [InlineData("True")] + [InlineData("False")] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewActivity(string 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 configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = enableGrpcAspNetCoreSupport, + }) + .Build(); + + using var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .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 != null && enableGrpcAspNetCoreSupport.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + 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[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + 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))); + } +} +#endif diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj new file mode 100644 index 0000000000..568b59d173 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj @@ -0,0 +1,43 @@ + + + Unit test project for OpenTelemetry Grpc for .NET instrumentation + net8.0;net7.0;net6.0 + $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) + $(NoWarn),CS8981 + enable + + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Proto/greet.proto b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Proto/greet.proto new file mode 100644 index 0000000000..7945286d36 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Proto/greet.proto @@ -0,0 +1,35 @@ +// Copyright 2019 The gRPC 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. + +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc SayHellos (HelloRequest) returns (stream HelloReply); +} + +service SecondGreeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc SayHellos (HelloRequest) returns (stream HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Services/GreeterService.cs b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Services/GreeterService.cs new file mode 100644 index 0000000000..f2f7b4c1aa --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.GrpcNetClient.Tests/Services/GreeterService.cs @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Greet; +using Grpc.Core; +using Microsoft.Extensions.Logging; + +namespace OpenTelemetry.Instrumentation.Grpc.Services.Tests; + +public class GreeterService : Greeter.GreeterBase +{ + private readonly ILogger logger; + + 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 async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + var i = 0; + while (!context.CancellationToken.IsCancellationRequested) + { + var message = $"How are you {request.Name}? {++i}"; + this.logger.LogInformation("Sending greeting {Message}.", message); + + await responseStream.WriteAsync(new HelloReply { Message = message }); + + // Gotta look busy + await Task.Delay(1000); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj index 8c103d0899..9c004bf25d 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj index 7f500d5cce..e9f24130cf 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs index d4adc75169..25d0252557 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -6,7 +6,9 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Instrumentation.SqlClient.Implementation; +#if !NETFRAMEWORK using OpenTelemetry.Tests; +#endif using OpenTelemetry.Trace; using Xunit; diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs index e729190d8c..1387a27577 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs index a6fd35baef..c737a8abd6 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs @@ -9,7 +9,6 @@ #if !NETFRAMEWORK using System.Net.Sockets; #endif -using OpenTelemetry.Tests; using OpenTelemetry.Trace; using StackExchange.Redis; using Xunit; diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj index 8f0ae3aee8..efcb055383 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj @@ -8,7 +8,7 @@ - +