diff --git a/.github/ISSUE_TEMPLATE/comp_instrumentation_sqlclient.md b/.github/ISSUE_TEMPLATE/comp_instrumentation_sqlclient.md new file mode 100644 index 0000000000..c74e59ca99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/comp_instrumentation_sqlclient.md @@ -0,0 +1,41 @@ +--- +name: OpenTelemetry.Instrumentation.SqlClient +about: Issue with OpenTelemetry.Instrumentation.SqlClient +labels: comp:instrumentation.sqlclient +--- + +# Issue with OpenTelemetry.Instrumentation.SqlClient + +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 74701f6a61..c38a11945c 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -78,12 +78,17 @@ flags: paths: - src/OpenTelemetry.Instrumentation.Runtime + unittests-Instrumentation.SqlClient: + carryforward: true + paths: + - src/OpenTelemetry.Instrumentation.SqlClient + unittests-Instrumentation.StackExchangeRedis: carryforward: true paths: - src/OpenTelemetry.Instrumentation.StackExchangeRedis - unittests-Instrumentation.Wcf: + unittests-Instrumentation.Wcf: carryforward: true paths: - src/OpenTelemetry.Instrumentation.Wcf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 150c7017dc..417259c488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,7 @@ jobs: redis: ['*/OpenTelemetry.Instrumentation.StackExchangeRedis*/**', 'examples/redis/**', '!**/*.md'] resourcedetectors: ['*/OpenTelemetry.ResourceDetectors.*/**', '!**/*.md'] runtime: ['*/OpenTelemetry.Instrumentation.Runtime*/**', 'examples/runtime-instrumentation/**', '!**/*.md'] + sqlclient: ['*/OpenTelemetry.Instrumentation.SqlClient*/**', '!**/*.md'] wcf: ['*/OpenTelemetry.Instrumentation.Wcf*/**', 'examples/wcf/**', '!**/*.md'] solution: [ 'src/**', @@ -64,6 +65,7 @@ jobs: '!*/OpenTelemetry.PersistentStorage*/**', '!*/OpenTelemetry.Instrumentation.Process*/**', '!examples/process-instrumentation/**', + '!*/OpenTelemetry.Instrumentation.SqlClient*/**', '!*/OpenTelemetry.Instrumentation.StackExchangeRedis*/**', '!examples/redis/**', '!*/OpenTelemetry.Instrumentation.Runtime*/**', @@ -254,6 +256,17 @@ jobs: project-name: OpenTelemetry.Instrumentation.Runtime code-cov-name: Instrumentation.Runtime + build-test-sqlclient: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'sqlclient') + || 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.SqlClient + code-cov-name: Instrumentation.SqlClient + build-test-wcf: needs: detect-changes if: | @@ -303,6 +316,7 @@ jobs: OpenTelemetry.Instrumentation.Owin.Tests.csproj, OpenTelemetry.Instrumentation.Process.Tests.csproj, OpenTelemetry.Instrumentation.Runtime.Tests.csproj, + OpenTelemetry.Instrumentation.SqlClient.Tests.csproj, OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj, OpenTelemetry.Instrumentation.Wcf.Tests.csproj, OpenTelemetry.PersistentStorage.FileSystem.Tests.csproj, @@ -365,6 +379,7 @@ jobs: || contains(needs.detect-changes.outputs.changes, 'geneva') || contains(needs.detect-changes.outputs.changes, 'onecollector') || contains(needs.detect-changes.outputs.changes, 'redis') + || contains(needs.detect-changes.outputs.changes, 'sqlclient') || contains(needs.detect-changes.outputs.changes, 'shared') uses: ./.github/workflows/verifyaotcompat.yml @@ -389,6 +404,7 @@ jobs: build-test-redis, build-test-redis-integration, build-test-runtime, + build-test-sqlclient, build-test-wcf, build-test-solution, verify-aot-compat diff --git a/.github/workflows/package-Instrumentation.SqlClient.yml b/.github/workflows/package-Instrumentation.SqlClient.yml new file mode 100644 index 0000000000..1226183035 --- /dev/null +++ b/.github/workflows/package-Instrumentation.SqlClient.yml @@ -0,0 +1,21 @@ +name: Pack OpenTelemetry.Instrumentation.SqlClient + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Instrumentation.SqlClient-*' # trigger when we create a tag with prefix "Instrumentation.SqlClient-" + +jobs: + call-build-test-pack: + permissions: + contents: write + uses: ./.github/workflows/Component.Package.yml + with: + project-name: OpenTelemetry.Instrumentation.SqlClient + secrets: inherit diff --git a/build/Common.props b/build/Common.props index 358fe0ba41..343b061a6a 100644 --- a/build/Common.props +++ b/build/Common.props @@ -33,6 +33,7 @@ [8.0.1,) [2.1.0,5.0) [3.1.0,) + 8.0.0 [1.0.3,2.0) [4.2.2,5.0) [3.11.0-beta1.23525.2] diff --git a/build/Projects/OpenTelemetry.Instrumentation.SqlClient.proj b/build/Projects/OpenTelemetry.Instrumentation.SqlClient.proj new file mode 100644 index 0000000000..3cde77cdda --- /dev/null +++ b/build/Projects/OpenTelemetry.Instrumentation.SqlClient.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 e2de239aec..6d755deb10 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -42,9 +42,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Exporter.OneCollector.yml = .github\workflows\package-Exporter.OneCollector.yml .github\workflows\package-Exporter.Stackdriver.yml = .github\workflows\package-Exporter.Stackdriver.yml .github\workflows\package-Extensions.AWS.yml = .github\workflows\package-Extensions.AWS.yml - .github\workflows\package-Extensions.AWSXRay.yml = .github\workflows\package-Extensions.AWSXRay.yml - .github\workflows\package-Extensions.AzureMonitor.yml = .github\workflows\package-Extensions.AzureMonitor.yml - .github\workflows\package-Extensions.Docker.yml = .github\workflows\package-Extensions.Docker.yml .github\workflows\package-Extensions.Enrichment.yml = .github\workflows\package-Extensions.Enrichment.yml .github\workflows\package-Extensions.yml = .github\workflows\package-Extensions.yml .github\workflows\package-Instrumentation.AspNet.yml = .github\workflows\package-Instrumentation.AspNet.yml @@ -61,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Instrumentation.Process.yml = .github\workflows\package-Instrumentation.Process.yml .github\workflows\package-Instrumentation.Quartz.yml = .github\workflows\package-Instrumentation.Quartz.yml .github\workflows\package-Instrumentation.Runtime.yml = .github\workflows\package-Instrumentation.Runtime.yml + .github\workflows\package-Instrumentation.SqlClient.yml = .github\workflows\package-Instrumentation.SqlClient.yml .github\workflows\package-Instrumentation.StackExchangeRedis.yml = .github\workflows\package-Instrumentation.StackExchangeRedis.yml .github\workflows\package-Instrumentation.Wcf.yml = .github\workflows\package-Instrumentation.Wcf.yml .github\workflows\package-PersistentStorage.yml = .github\workflows\package-PersistentStorage.yml @@ -324,6 +322,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projects", "Projects", "{04 build\Projects\OpenTelemetry.Instrumentation.Owin.proj = build\Projects\OpenTelemetry.Instrumentation.Owin.proj build\Projects\OpenTelemetry.Instrumentation.Process.proj = build\Projects\OpenTelemetry.Instrumentation.Process.proj build\Projects\OpenTelemetry.Instrumentation.Runtime.proj = build\Projects\OpenTelemetry.Instrumentation.Runtime.proj + build\Projects\OpenTelemetry.Instrumentation.SqlClient.proj = build\Projects\OpenTelemetry.Instrumentation.SqlClient.proj build\Projects\OpenTelemetry.Instrumentation.StackExchangeRedis.proj = build\Projects\OpenTelemetry.Instrumentation.StackExchangeRedis.proj build\Projects\OpenTelemetry.Instrumentation.Wcf.proj = build\Projects\OpenTelemetry.Instrumentation.Wcf.proj build\Projects\OpenTelemetry.PersistentStorage.proj = build\Projects\OpenTelemetry.PersistentStorage.proj @@ -347,6 +346,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.ResourceDetec EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.PersistentStorage.Abstractions.Tests", "test\OpenTelemetry.PersistentStorage.Abstractions.Tests\OpenTelemetry.PersistentStorage.Abstractions.Tests.csproj", "{7AD707F9-DC6D-430A-8834-D5DCD517BF6E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Instrumentation.SqlClient", "src\OpenTelemetry.Instrumentation.SqlClient\OpenTelemetry.Instrumentation.SqlClient.csproj", "{737D1A9E-5A1A-4F4F-830B-E98ED100994C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Instrumentation.SqlClient.Tests", "test\OpenTelemetry.Instrumentation.SqlClient.Tests\OpenTelemetry.Instrumentation.SqlClient.Tests.csproj", "{9C996130-74D7-4FB7-8277-2EE6EBA2BFA6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -693,6 +696,14 @@ Global {7AD707F9-DC6D-430A-8834-D5DCD517BF6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {7AD707F9-DC6D-430A-8834-D5DCD517BF6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AD707F9-DC6D-430A-8834-D5DCD517BF6E}.Release|Any CPU.Build.0 = Release|Any CPU + {737D1A9E-5A1A-4F4F-830B-E98ED100994C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {737D1A9E-5A1A-4F4F-830B-E98ED100994C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {737D1A9E-5A1A-4F4F-830B-E98ED100994C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {737D1A9E-5A1A-4F4F-830B-E98ED100994C}.Release|Any CPU.Build.0 = Release|Any CPU + {9C996130-74D7-4FB7-8277-2EE6EBA2BFA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C996130-74D7-4FB7-8277-2EE6EBA2BFA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C996130-74D7-4FB7-8277-2EE6EBA2BFA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C996130-74D7-4FB7-8277-2EE6EBA2BFA6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -796,6 +807,8 @@ Global {033CA8D4-1529-413A-B244-07958D5F9A48} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {36271347-2055-438E-9659-B71542A17A73} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {7AD707F9-DC6D-430A-8834-D5DCD517BF6E} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {737D1A9E-5A1A-4F4F-830B-E98ED100994C} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {9C996130-74D7-4FB7-8277-2EE6EBA2BFA6} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..82f785235c --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.EnableConnectionLevelAttributes.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.EnableConnectionLevelAttributes.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Enrich.get -> System.Action +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForStoredProcedure.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SqlClientTraceInstrumentationOptions() -> void +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs new file mode 100644 index 0000000000..70e3c273ab --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/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.SqlClient.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.SqlClient.Tests")] +#endif diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md new file mode 100644 index 0000000000..d6ac6ff546 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -0,0 +1,274 @@ +# Changelog + +## Unreleased + +* `ActivitySource.Version` is set to NuGet package version. + ([#5498](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5498)) +* Update `OpenTelemetry.Api.ProviderBuilderExtensions` to `1.8.1`. + * Update `OpenTelemetry.Api` 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 + +* 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. + ([#5270](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5270)) +* **Breaking Change**: Renamed `SqlClientInstrumentationOptions` to + `SqlClientTraceInstrumentationOptions`. + ([#5285](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5285)) +* **Breaking Change**: Stop emitting `db.statement_type` attribute. + This attribute was never a part of the [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/database/database-spans.md#call-level-attributes). + ([#5301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5301)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + +* Updated `Microsoft.Extensions.Configuration` and + `Microsoft.Extensions.Options` package version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +## 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/database/database-spans.md). + ([#4644](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4644)) + +## 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 OpenTelemetry.Api.ProviderBuilderExtensions 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 + +* **Breaking change**: The same API is now exposed for `net462` and + `netstandard2.0` targets. `SetDbStatement` has been removed. Use + `SetDbStatementForText` to capture command text and stored procedure names on + .NET Framework. Note: `Enrich`, `Filter`, `RecordException`, and + `SetDbStatementForStoredProcedure` options are NOT supported on .NET + Framework. + ([#3900](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3900)) + +* Added overloads which accept a name to the `TracerProviderBuilder` + `AddSqlClientInstrumentation` extension to allow for more fine-grained options + management + ([#3994](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3994)) + +## 1.0.0-rc9.9 + +Released 2022-Nov-07 + +## 1.0.0-rc9.8 + +Released 2022-Oct-17 + +* Use `Activity.Status` and `Activity.StatusDescription` properties instead of + `OpenTelemetry.Trace.Status` and `OpenTelemetry.Trace.Status.Description` + respectively to set activity status. + ([#3118](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3118)) + ([#3751](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3751)) +* Add support for Filter option for non .NET Framework Targets + ([#3743](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3743)) + +## 1.0.0-rc9.7 + +Released 2022-Sep-29 + +## 1.0.0-rc9.6 + +Released 2022-Aug-18 + +## 1.0.0-rc9.5 + +Released 2022-Aug-02 + +* Update the `ActivitySource.Name` from "OpenTelemetry.SqlClient" to + "OpenTelemetry.Instrumentation.SqlClient". + ([#3435](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3435)) + +## 1.0.0-rc9.4 + +Released 2022-Jun-03 + +## 1.0.0-rc9.3 + +Released 2022-Apr-15 + +* Removes .NET Framework 4.6.1. The minimum .NET Framework version supported is + .NET 4.6.2. + ([#3190](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3190)) + +## 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 + +* Removes .NET Framework 4.5.2 support. The minimum .NET Framework version + supported is .NET 4.6.1. + ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) + +## 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 + +* Instrumentation modified to depend only on the API. +* Activities are now created with the `db.system` attribute set for usage during + sampling. + ([#1979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1979)) + +## 1.0.0-rc3 + +Released 2021-Mar-19 + +## 1.0.0-rc2 + +Released 2021-Jan-29 + +* Microsoft.Data.SqlClient v2.0.0 and higher is now properly instrumented on + .NET Framework. + ([#1599](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1599)) +* SqlClientInstrumentationOptions API changes: `SetStoredProcedureCommandName` + and `SetTextCommandContent` have been renamed to + `SetDbStatementForStoredProcedure` and `SetDbStatementForText`. They are now + only available on .NET Core. On .NET Framework they are replaced by a single + `SetDbStatement` property. +* On .NET Framework, "db.statement_type" attribute is no longer set for + activities created by the instrumentation. +* New setting on SqlClientInstrumentationOptions on .NET Core: `RecordException` + can be set to instruct the instrumentation to record SqlExceptions as Activity + [events](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md). + ([#1592](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1592)) + +## 1.0.0-rc1.1 + +Released 2020-Nov-17 + +* SqlInstrumentation 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 `object` 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 + +## 0.5.0-beta.2 + +Released 2020-08-28 + +* .NET Core SqlClient instrumentation will now add the raw Command object to the + Activity it creates + ([#1099](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1099)) +* Renamed from `AddSqlClientDependencyInstrumentation` to + `AddSqlClientInstrumentation` + +## 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.SqlClient/Implementation/SqlActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs new file mode 100644 index 0000000000..ea6080bafd --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Reflection; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// Helper class to hold common properties used by both SqlClientDiagnosticListener on .NET Core +/// and SqlEventSourceListener on .NET Framework. +/// +internal sealed class SqlActivitySourceHelper +{ + public const string MicrosoftSqlServerDatabaseSystemName = "mssql"; + + public static readonly Assembly Assembly = typeof(SqlActivitySourceHelper).Assembly; + public static readonly AssemblyName AssemblyName = Assembly.GetName(); + public static readonly string ActivitySourceName = AssemblyName.Name; + public static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion()); + public static readonly string ActivityName = ActivitySourceName + ".Execute"; + + public static readonly IEnumerable> CreationTags = new[] + { + new KeyValuePair(SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName), + }; +} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs new file mode 100644 index 0000000000..589ae603f7 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs @@ -0,0 +1,204 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NETFRAMEWORK +using System.Data; +using System.Diagnostics; +using OpenTelemetry.Trace; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif +internal sealed class SqlClientDiagnosticListener : ListenerHandler +{ + public const string SqlDataBeforeExecuteCommand = "System.Data.SqlClient.WriteCommandBefore"; + public const string SqlMicrosoftBeforeExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandBefore"; + + public const string SqlDataAfterExecuteCommand = "System.Data.SqlClient.WriteCommandAfter"; + public const string SqlMicrosoftAfterExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandAfter"; + + public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError"; + public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError"; + + private readonly PropertyFetcher commandFetcher = new("Command"); + private readonly PropertyFetcher connectionFetcher = new("Connection"); + private readonly PropertyFetcher dataSourceFetcher = new("DataSource"); + private readonly PropertyFetcher databaseFetcher = new("Database"); + private readonly PropertyFetcher commandTypeFetcher = new("CommandType"); + private readonly PropertyFetcher commandTextFetcher = new("CommandText"); + private readonly PropertyFetcher exceptionFetcher = new("Exception"); + private readonly SqlClientTraceInstrumentationOptions options; + + public SqlClientDiagnosticListener(string sourceName, SqlClientTraceInstrumentationOptions options) + : base(sourceName) + { + this.options = options ?? new SqlClientTraceInstrumentationOptions(); + } + + public override bool SupportsNullActivity => true; + + public override void OnEventWritten(string name, object payload) + { + var activity = Activity.Current; + switch (name) + { + case SqlDataBeforeExecuteCommand: + case SqlMicrosoftBeforeExecuteCommand: + { + // SqlClient does not create an Activity. So the activity coming in here will be null or the root span. + activity = SqlActivitySourceHelper.ActivitySource.StartActivity( + SqlActivitySourceHelper.ActivityName, + ActivityKind.Client, + default(ActivityContext), + SqlActivitySourceHelper.CreationTags); + + if (activity == null) + { + // There is no listener or it decided not to sample the current request. + return; + } + + _ = this.commandFetcher.TryFetch(payload, out var command); + if (command == null) + { + SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + activity.Stop(); + return; + } + + if (activity.IsAllDataRequested) + { + try + { + if (this.options.Filter?.Invoke(command) == false) + { + SqlClientInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + } + catch (Exception ex) + { + SqlClientInstrumentationEventSource.Log.CommandFilterException(ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + + _ = this.connectionFetcher.TryFetch(command, out var connection); + _ = this.databaseFetcher.TryFetch(connection, out var database); + + activity.DisplayName = (string)database; + + _ = this.dataSourceFetcher.TryFetch(connection, out var dataSource); + _ = this.commandTextFetcher.TryFetch(command, out var commandText); + + activity.SetTag(SemanticConventions.AttributeDbName, (string)database); + + this.options.AddConnectionLevelDetailsToActivity((string)dataSource, activity); + + if (this.commandTypeFetcher.TryFetch(command, out CommandType commandType)) + { + switch (commandType) + { + case CommandType.StoredProcedure: + if (this.options.SetDbStatementForStoredProcedure) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); + } + + break; + + case CommandType.Text: + if (this.options.SetDbStatementForText) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); + } + + break; + + case CommandType.TableDirect: + break; + } + } + + try + { + this.options.Enrich?.Invoke(activity, "OnCustom", command); + } + catch (Exception ex) + { + SqlClientInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + } + + break; + case SqlDataAfterExecuteCommand: + case SqlMicrosoftAfterExecuteCommand: + { + if (activity == null) + { + SqlClientInstrumentationEventSource.Log.NullActivity(name); + return; + } + + if (activity.Source != SqlActivitySourceHelper.ActivitySource) + { + return; + } + + activity.Stop(); + } + + break; + case SqlDataWriteCommandError: + case SqlMicrosoftWriteCommandError: + { + if (activity == null) + { + SqlClientInstrumentationEventSource.Log.NullActivity(name); + return; + } + + if (activity.Source != SqlActivitySourceHelper.ActivitySource) + { + return; + } + + try + { + if (activity.IsAllDataRequested) + { + if (this.exceptionFetcher.TryFetch(payload, out Exception exception) && exception != null) + { + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + + if (this.options.RecordException) + { + activity.RecordException(exception); + } + } + else + { + SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + } + } + } + finally + { + activity.Stop(); + } + } + + break; + } + } +} +#endif diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs new file mode 100644 index 0000000000..ccd86471d7 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-SqlClient")] +internal sealed class SqlClientInstrumentationEventSource : EventSource +{ + public static SqlClientInstrumentationEventSource Log = new(); + + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); + } + } + + [Event(1, 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(1, handlerName, eventName, ex); + } + + [Event(2, Message = "Current Activity is NULL in the '{0}' callback. Span will not be recorded.", Level = EventLevel.Warning)] + public void NullActivity(string eventName) + { + this.WriteEvent(2, eventName); + } + + [Event(3, 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(3, handlerName, eventName); + } + + [Event(4, Message = "Payload is invalid in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void InvalidPayload(string handlerName, string eventName) + { + this.WriteEvent(4, handlerName, eventName); + } + + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.EnrichmentException(ex.ToInvariantString()); + } + } + + [Event(5, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(5, exception); + } + + [Event(6, Message = "Command is filtered out. Activity {0}", Level = EventLevel.Verbose)] + public void CommandIsFilteredOut(string activityName) + { + this.WriteEvent(6, activityName); + } + + [NonEvent] + public void CommandFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.CommandFilterException(ex.ToInvariantString()); + } + } + + [Event(7, Message = "Command filter threw exception. Command will not be collected. Exception {0}.", Level = EventLevel.Error)] + public void CommandFilterException(string exception) + { + this.WriteEvent(7, exception); + } +} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs new file mode 100644 index 0000000000..111e4878d3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs @@ -0,0 +1,191 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Diagnostics; +using System.Diagnostics.Tracing; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// On .NET Framework, neither System.Data.SqlClient nor Microsoft.Data.SqlClient emit DiagnosticSource events. +/// Instead they use EventSource: +/// For System.Data.SqlClient see: reference source. +/// For Microsoft.Data.SqlClient see: SqlClientEventSource. +/// +/// We hook into these event sources and process their BeginExecute/EndExecute events. +/// +/// +/// Note that before version 2.0.0, Microsoft.Data.SqlClient used +/// "Microsoft-AdoNet-SystemData" (same as System.Data.SqlClient), but since +/// 2.0.0 has switched to "Microsoft.Data.SqlClient.EventSource". +/// +internal sealed class SqlEventSourceListener : EventListener +{ + internal const string AdoNetEventSourceName = "Microsoft-AdoNet-SystemData"; + internal const string MdsEventSourceName = "Microsoft.Data.SqlClient.EventSource"; + + internal const int BeginExecuteEventId = 1; + internal const int EndExecuteEventId = 2; + + private readonly SqlClientTraceInstrumentationOptions options; + private EventSource adoNetEventSource; + private EventSource mdsEventSource; + + public SqlEventSourceListener(SqlClientTraceInstrumentationOptions options = null) + { + this.options = options ?? new SqlClientTraceInstrumentationOptions(); + } + + public override void Dispose() + { + if (this.adoNetEventSource != null) + { + this.DisableEvents(this.adoNetEventSource); + } + + if (this.mdsEventSource != null) + { + this.DisableEvents(this.mdsEventSource); + } + + base.Dispose(); + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource?.Name.StartsWith(AdoNetEventSourceName, StringComparison.Ordinal) == true) + { + this.adoNetEventSource = eventSource; + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); + } + else if (eventSource?.Name.StartsWith(MdsEventSourceName, StringComparison.Ordinal) == true) + { + this.mdsEventSource = eventSource; + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); + } + + base.OnEventSourceCreated(eventSource); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + try + { + if (eventData.EventId == BeginExecuteEventId) + { + this.OnBeginExecute(eventData); + } + else if (eventData.EventId == EndExecuteEventId) + { + this.OnEndExecute(eventData); + } + } + catch (Exception exc) + { + SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent(nameof(SqlEventSourceListener), nameof(this.OnEventWritten), exc); + } + } + + private void OnBeginExecute(EventWrittenEventArgs eventData) + { + /* + Expected payload: + [0] -> ObjectId + [1] -> DataSource + [2] -> Database + [3] -> CommandText + + Note: + - For "Microsoft-AdoNet-SystemData" v1.0: [3] CommandText = CommandType == CommandType.StoredProcedure ? CommandText : string.Empty; (so it is set for only StoredProcedure command types) + (https://github.com/dotnet/SqlClient/blob/v1.0.19239.1/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6369) + - For "Microsoft-AdoNet-SystemData" v1.1: [3] CommandText = sqlCommand.CommandText (so it is set for all command types) + (https://github.com/dotnet/SqlClient/blob/v1.1.0/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L7459) + - For "Microsoft.Data.SqlClient.EventSource" v2.0+: [3] CommandText = sqlCommand.CommandText (so it is set for all command types). + (https://github.com/dotnet/SqlClient/blob/f4568ce68da21db3fe88c0e72e1287368aaa1dc8/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6641) + */ + + if ((eventData?.Payload?.Count ?? 0) < 4) + { + SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnBeginExecute)); + return; + } + + var activity = SqlActivitySourceHelper.ActivitySource.StartActivity( + SqlActivitySourceHelper.ActivityName, + ActivityKind.Client, + default(ActivityContext), + SqlActivitySourceHelper.CreationTags); + + if (activity == null) + { + // There is no listener or it decided not to sample the current request. + return; + } + + string databaseName = (string)eventData.Payload[2]; + + activity.DisplayName = databaseName; + + if (activity.IsAllDataRequested) + { + activity.SetTag(SemanticConventions.AttributeDbName, databaseName); + + this.options.AddConnectionLevelDetailsToActivity((string)eventData.Payload[1], activity); + + string commandText = (string)eventData.Payload[3]; + if (!string.IsNullOrEmpty(commandText) && this.options.SetDbStatementForText) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); + } + } + } + + private void OnEndExecute(EventWrittenEventArgs eventData) + { + /* + Expected payload: + [0] -> ObjectId + [1] -> CompositeState bitmask (0b001 -> successFlag, 0b010 -> isSqlExceptionFlag , 0b100 -> synchronousFlag) + [2] -> SqlExceptionNumber + */ + + if ((eventData?.Payload?.Count ?? 0) < 3) + { + SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnEndExecute)); + return; + } + + var activity = Activity.Current; + if (activity?.Source != SqlActivitySourceHelper.ActivitySource) + { + return; + } + + try + { + if (activity.IsAllDataRequested) + { + int compositeState = (int)eventData.Payload[1]; + if ((compositeState & 0b001) != 0b001) + { + if ((compositeState & 0b010) == 0b010) + { + var errorText = $"SqlExceptionNumber {eventData.Payload[2]} thrown."; + activity.SetStatus(ActivityStatusCode.Error, errorText); + } + else + { + activity.SetStatus(ActivityStatusCode.Error, "Unknown Sql failure."); + } + } + } + } + finally + { + activity.Stop(); + } + } +} +#endif diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj new file mode 100644 index 0000000000..bddef25ac1 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj @@ -0,0 +1,39 @@ + + + + net8.0;net6.0;netstandard2.0 + $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) + SqlClient instrumentation for OpenTelemetry .NET + $(PackageTags);distributed-tracing + Instrumentation.SqlClient- + + enable + + disable + + + + + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/README.md b/src/OpenTelemetry.Instrumentation.SqlClient/README.md new file mode 100644 index 0000000000..1df0435250 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/README.md @@ -0,0 +1,276 @@ +# SqlClient Instrumentation for OpenTelemetry + +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.SqlClient.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.SqlClient) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.SqlClient.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.SqlClient) + +This is an [Instrumentation +Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), +which instruments +[Microsoft.Data.SqlClient](https://www.nuget.org/packages/Microsoft.Data.SqlClient) +and +[System.Data.SqlClient](https://www.nuget.org/packages/System.Data.SqlClient) +and collects traces about database operations. + +> [!WARNING] +> Instrumentation is not working with `Microsoft.Data.SqlClient` v3.* due to +the [issue](https://github.com/dotnet/SqlClient/pull/1258). It was fixed in 4.0 +and later. + +> [!CAUTION] +> This component is based on the OpenTelemetry semantic conventions for +[traces](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-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). + +## Steps to enable OpenTelemetry.Instrumentation.SqlClient + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Instrumentation.SqlClient`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.SqlClient) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package --prerelease OpenTelemetry.Instrumentation.SqlClient +``` + +### Step 2: Enable SqlClient Instrumentation at application startup + +SqlClient instrumentation must be enabled at application startup. + +The following example demonstrates adding SqlClient instrumentation to a console +application. This example also sets up the OpenTelemetry Console exporter, which +requires adding the package +[`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md) +to the application. + +```csharp +using OpenTelemetry.Trace; + +public class Program +{ + public static void Main(string[] args) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation() + .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). + +For an ASP.NET application, adding instrumentation is typically done in the +`Global.asax.cs`. Refer to the documentation for +[OpenTelemetry.Instrumentation.AspNet](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.AspNet/README.md). + +## Advanced configuration + +This instrumentation can be configured to change the default behavior by using +`SqlClientTraceInstrumentationOptions`. + +### Capturing database statements + +The `SqlClientTraceInstrumentationOptions` class exposes two properties that can +be used to configure how the +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) +attribute is captured upon execution of a query but the behavior depends on the +runtime used. + +#### .NET and .NET Core + +On .NET and .NET Core, two properties are available: +`SetDbStatementForStoredProcedure` and `SetDbStatementForText`. These properties +control capturing of `CommandType.StoredProcedure` and `CommandType.Text` +respectively. + +`SetDbStatementForStoredProcedure` is _true_ by default and will set +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) +attribute to the stored procedure command name. + +`SetDbStatementForText` is _false_ by default (to prevent accidental capture of +sensitive data that might be part of the SQL statement text). When set to +`true`, the instrumentation will set +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) +attribute to the text of the SQL command being executed. + +To disable capturing stored procedure commands use configuration like below. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => options.SetDbStatementForStoredProcedure = false) + .AddConsoleExporter() + .Build(); +``` + +To enable capturing of `sqlCommand.CommandText` for `CommandType.Text` use the +following configuration. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => options.SetDbStatementForText = true) + .AddConsoleExporter() + .Build(); +``` + +#### .NET Framework + +On .NET Framework, the `SetDbStatementForText` property controls whether or not +this instrumentation will set the +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) +attribute to the text of the `SqlCommand` being executed. This could either be +the name of a stored procedure (when `CommandType.StoredProcedure` is used) or +the full text of a `CommandType.Text` query. `SetDbStatementForStoredProcedure` +is ignored because on .NET Framework there is no way to determine the type of +command being executed. + +Since `CommandType.Text` might contain sensitive data, all SQL capturing is +_disabled_ by default to protect against accidentally sending full query text to +a telemetry backend. If you are only using stored procedures or have no +sensitive data in your `sqlCommand.CommandText`, you can enable SQL capturing +using the options like below: + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => options.SetDbStatementForText = true) + .AddConsoleExporter() + .Build(); +``` + +> [!NOTE] +> When using the built-in `System.Data.SqlClient` only stored procedure +command names will ever be captured. When using the `Microsoft.Data.SqlClient` +NuGet package (v1.1+) stored procedure command names, full query text, and other +command text will be captured. + +### EnableConnectionLevelAttributes + +> [!NOTE] +> EnableConnectionLevelAttributes is supported on all runtimes. + +By default, `EnabledConnectionLevelAttributes` is disabled and this +instrumentation sets the `peer.service` attribute to the +[`DataSource`](https://docs.microsoft.com/dotnet/api/system.data.common.dbconnection.datasource) +property of the connection. If `EnabledConnectionLevelAttributes` is enabled, +the `DataSource` will be parsed and the server name will be sent as the +`net.peer.name` or `net.peer.ip` attribute, the instance name will be sent as +the `db.mssql.instance_name` attribute, and the port will be sent as the +`net.peer.port` attribute if it is not 1433 (the default port). + +The following example shows how to use `EnableConnectionLevelAttributes`. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => options.EnableConnectionLevelAttributes = true) + .AddConsoleExporter() + .Build(); +``` + +### Enrich + +> [!NOTE] +> Enrich is supported on .NET and .NET Core runtimes only. + +This option can be used to enrich the activity with additional information from +the raw `SqlCommand` object. The `Enrich` action is 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. + +Currently there is only one event name reported, "OnCustom". The actual object +is `Microsoft.Data.SqlClient.SqlCommand` for `Microsoft.Data.SqlClient` and +`System.Data.SqlClient.SqlCommand` for `System.Data.SqlClient`. + +The following code snippet shows how to add additional tags using `Enrich`. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation(opt => opt.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnCustom")) + { + if (rawObject is SqlCommand cmd) + { + activity.SetTag("db.commandTimeout", cmd.CommandTimeout); + } + }; + }) + .Build(); +``` + +[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 `SqlCommand` object. + +### RecordException + +> [!NOTE] +> RecordException is supported on .NET and .NET Core runtimes only. + +This option can be set to instruct the instrumentation to record SqlExceptions +as Activity +[events](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md). + +The default value is `false` and can be changed by the code like below. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => options.RecordException = true) + .AddConsoleExporter() + .Build(); +``` + +### Filter + +> [!NOTE] +> Filter is supported on .NET and .NET Core runtimes only. + +This option can be used to filter out activities based on the properties of the +`SqlCommand` object being instrumented using a `Func`. The +function receives an instance of the raw `SqlCommand` and should return `true` +if the telemetry is to be collected, and `false` if it should not. The parameter +of the Func delegate is of type `object` and needs to be cast to the appropriate +type of `SqlCommand`, either `Microsoft.Data.SqlClient.SqlCommand` or +`System.Data.SqlClient.SqlCommand`. The example below filters out all commands +that are not stored procedures. + +```csharp +using var traceProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + opt => + { + opt.Filter = cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandType == CommandType.StoredProcedure; + } + + return false; + }; + }) + .AddConsoleExporter() + .Build(); +{ +``` + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) + +* [OpenTelemetry semantic conventions for database + calls](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md) diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs new file mode 100644 index 0000000000..9b4230a460 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using OpenTelemetry.Instrumentation.SqlClient.Implementation; + +namespace OpenTelemetry.Instrumentation.SqlClient; + +/// +/// SqlClient instrumentation. +/// +internal sealed class SqlClientInstrumentation : IDisposable +{ + internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener"; +#if NET6_0_OR_GREATER + internal const string SqlClientTrimmingUnsupportedMessage = "Trimming is not yet supported with SqlClient instrumentation."; +#endif +#if NETFRAMEWORK + private readonly SqlEventSourceListener sqlEventSourceListener; +#else + private static readonly HashSet DiagnosticSourceEvents = new() + { + "System.Data.SqlClient.WriteCommandBefore", + "Microsoft.Data.SqlClient.WriteCommandBefore", + "System.Data.SqlClient.WriteCommandAfter", + "Microsoft.Data.SqlClient.WriteCommandAfter", + "System.Data.SqlClient.WriteCommandError", + "Microsoft.Data.SqlClient.WriteCommandError", + }; + + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; +#endif + + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for sql instrumentation. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] +#endif + public SqlClientInstrumentation( + SqlClientTraceInstrumentationOptions options = null) + { +#if NETFRAMEWORK + this.sqlEventSourceListener = new SqlEventSourceListener(options); +#else + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( + name => new SqlClientDiagnosticListener(name, options), + listener => listener.Name == SqlClientDiagnosticListenerName, + this.isEnabled, + SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); +#endif + } + + /// + public void Dispose() + { +#if NETFRAMEWORK + this.sqlEventSourceListener?.Dispose(); +#else + this.diagnosticSourceSubscriber?.Dispose(); +#endif + } +} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs new file mode 100644 index 0000000000..3dec363163 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs @@ -0,0 +1,302 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Data; +using System.Diagnostics; +using System.Text.RegularExpressions; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.SqlClient; + +/// +/// Options for . +/// +/// +/// For help and examples see: . +/// +public class SqlClientTraceInstrumentationOptions +{ + /* + * Match... + * protocol[ ]:[ ]serverName + * serverName + * serverName[ ]\[ ]instanceName + * serverName[ ],[ ]port + * serverName[ ]\[ ]instanceName[ ],[ ]port + * + * [ ] can be any number of white-space, SQL allows it for some reason. + * + * Optional "protocol" can be "tcp", "lpc" (shared memory), or "np" (named pipes). See: + * https://docs.microsoft.com/troubleshoot/sql/connect/use-server-name-parameter-connection-string, and + * https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-5.0 + * + * In case of named pipes the Data Source string can take form of: + * np:serverName\instanceName, or + * np:\\serverName\pipe\pipeName, or + * np:\\serverName\pipe\MSSQL$instanceName\pipeName - in this case a separate regex (see NamedPipeRegex below) + * is used to extract instanceName + */ + private static readonly Regex DataSourceRegex = new("^(.*\\s*:\\s*\\\\{0,2})?(.*?)\\s*(?:[\\\\,]|$)\\s*(.*?)\\s*(?:,|$)\\s*(.*)$", RegexOptions.Compiled); + + /// + /// In a Data Source string like "np:\\serverName\pipe\MSSQL$instanceName\pipeName" match the + /// "pipe\MSSQL$instanceName" segment to extract instanceName if it is available. + /// + /// + /// + /// + private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled); + + private static readonly ConcurrentDictionary ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets a value indicating whether or not the should add the names of commands as the tag. Default + /// value: . + /// + /// + /// SetDbStatementForStoredProcedure is only supported on .NET + /// and .NET Core runtimes. + /// + public bool SetDbStatementForStoredProcedure { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not the should add the text of commands as + /// the tag. + /// Default value: . + /// + /// + /// + /// WARNING: SetDbStatementForText will capture the raw + /// CommandText. Make sure your CommandText property never + /// contains any sensitive data. + /// + /// SetDbStatementForText is supported on all runtimes. + /// + /// On .NET and .NET Core SetDbStatementForText only applies to + /// SqlCommands with . + /// On .NET Framework SetDbStatementForText applies to all + /// SqlCommands regardless of . + /// + /// When using System.Data.SqlClient use + /// SetDbStatementForText to capture StoredProcedure command + /// names. + /// When using Microsoft.Data.SqlClient use + /// SetDbStatementForText to capture Text, StoredProcedure, and all + /// other command text. + /// + /// + /// + /// + public bool SetDbStatementForText { get; set; } + + /// + /// Gets or sets a value indicating whether or not the should parse the DataSource on a + /// SqlConnection into server name, instance name, and/or port + /// connection-level attribute tags. Default value: . + /// + /// + /// + /// EnableConnectionLevelAttributes is supported on all runtimes. + /// + /// + /// The default behavior is to set the SqlConnection DataSource as the tag. + /// If enabled, SqlConnection DataSource will be parsed and the server name will be sent as the + /// or tag, + /// the instance name will be sent as the tag, + /// and the port will be sent as the tag if it is not 1433 (the default port). + /// + /// + public bool EnableConnectionLevelAttributes { get; set; } + + /// + /// Gets or sets an action to enrich an with the + /// raw SqlCommand object. + /// + /// + /// Enrich is only executed on .NET and .NET Core + /// runtimes. + /// The parameters passed to the enrich action are: + /// + /// The being enriched. + /// The name of the event. Currently only "OnCustom" is + /// used but more events may be added in the future. + /// The raw SqlCommand object from which additional + /// information can be extracted to enrich the . + /// + /// + public Action Enrich { get; set; } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry about a command. + /// + /// + /// Filter is only executed on .NET and .NET Core + /// runtimes. + /// Notes: + /// + /// The first parameter passed to the filter function is the raw + /// SqlCommand object for the command being executed. + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the command is + /// collected. + /// If filter returns or throws an + /// exception the command is NOT collected. + /// + /// + /// + public Func Filter { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be + /// recorded as or not. Default value: . + /// + /// + /// RecordException is only supported on .NET and .NET Core + /// runtimes. + /// For specification details see: . + /// + public bool RecordException { get; set; } + + internal static SqlConnectionDetails ParseDataSource(string dataSource) + { + Match match = DataSourceRegex.Match(dataSource); + + string serverHostName = match.Groups[2].Value; + string serverIpAddress = null; + + string instanceName; + + var uriHostNameType = Uri.CheckHostName(serverHostName); + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + serverIpAddress = serverHostName; + serverHostName = null; + } + + string maybeProtocol = match.Groups[1].Value; + bool isNamedPipe = maybeProtocol.Length > 0 && + maybeProtocol.StartsWith("np", StringComparison.OrdinalIgnoreCase); + + if (isNamedPipe) + { + string pipeName = match.Groups[3].Value; + if (pipeName.Length > 0) + { + var namedInstancePipeMatch = NamedPipeRegex.Match(pipeName); + if (namedInstancePipeMatch.Success) + { + instanceName = namedInstancePipeMatch.Groups[1].Value; + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = instanceName, + Port = null, + }; + } + } + + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = null, + Port = null, + }; + } + + string port; + if (match.Groups[4].Length > 0) + { + instanceName = match.Groups[3].Value; + port = match.Groups[4].Value; + if (port == "1433") + { + port = null; + } + } + else if (int.TryParse(match.Groups[3].Value, out int parsedPort)) + { + port = parsedPort == 1433 ? null : match.Groups[3].Value; + instanceName = null; + } + else + { + instanceName = match.Groups[3].Value; + + if (string.IsNullOrEmpty(instanceName)) + { + instanceName = null; + } + + port = null; + } + + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = instanceName, + Port = port, + }; + } + + internal void AddConnectionLevelDetailsToActivity(string dataSource, Activity sqlActivity) + { + if (!this.EnableConnectionLevelAttributes) + { + sqlActivity.SetTag(SemanticConventions.AttributePeerService, dataSource); + } + else + { + if (!ConnectionDetailCache.TryGetValue(dataSource, out SqlConnectionDetails connectionDetails)) + { + connectionDetails = ParseDataSource(dataSource); + ConnectionDetailCache.TryAdd(dataSource, connectionDetails); + } + + if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) + { + sqlActivity.SetTag(SemanticConventions.AttributeDbMsSqlInstanceName, connectionDetails.InstanceName); + } + + if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) + { + sqlActivity.SetTag(SemanticConventions.AttributeServerAddress, connectionDetails.ServerHostName); + } + else + { + sqlActivity.SetTag(SemanticConventions.AttributeServerSocketAddress, connectionDetails.ServerIpAddress); + } + + if (!string.IsNullOrEmpty(connectionDetails.Port)) + { + // TODO: Should we continue to emit this if the default port (1433) is being used? + sqlActivity.SetTag(SemanticConventions.AttributeServerPort, connectionDetails.Port); + } + } + } + + internal sealed class SqlConnectionDetails + { + public string ServerHostName { get; set; } + + public string ServerIpAddress { get; set; } + + public string InstanceName { get; set; } + + public string Port { get; set; } + } +} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs new file mode 100644 index 0000000000..9634759bcc --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs @@ -0,0 +1,81 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.SqlClient; +using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of dependency instrumentation. +/// +public static class TracerProviderBuilderExtensions +{ + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProviderBuilder builder) + => AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions: null); + + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + public static TracerProviderBuilder AddSqlClientInstrumentation( + this TracerProviderBuilder builder, + Action configureSqlClientTraceInstrumentationOptions) + => AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions); + + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + + public static TracerProviderBuilder AddSqlClientInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureSqlClientTraceInstrumentationOptions) + { + Guard.ThrowIfNull(builder); + + name ??= Options.DefaultName; + + if (configureSqlClientTraceInstrumentationOptions != null) + { + builder.ConfigureServices(services => services.Configure(name, configureSqlClientTraceInstrumentationOptions)); + } + + builder.AddInstrumentation(sp => + { + var sqlOptions = sp.GetRequiredService>().Get(name); + + return new SqlClientInstrumentation(sqlOptions); + }); + + builder.AddSource(SqlActivitySourceHelper.ActivitySourceName); + + return builder; + } +} diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index 0d9fe00f87..6c70b8c54c 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -103,12 +103,17 @@ internal static class SemanticConventions public const string AttributeHttpRequestMethodOriginal = "http.request.method_original"; public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode) public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme) + public const string AttributeUrlFull = "url.full"; // replaces: "http.url" (AttributeHttpUrl) public const string AttributeUrlPath = "url.path"; // replaces: "http.target" (AttributeHttpTarget) public const string AttributeUrlQuery = "url.query"; // replaces: "http.target" (AttributeHttpTarget) + public const string AttributeServerSocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) // v1.23.0 // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md#http-server + public const string AttributeClientAddress = "client.address"; + public const string AttributeClientPort = "client.port"; public const string AttributeNetworkProtocolVersion = "network.protocol.version"; // replaces: "http.flavor" (AttributeHttpFlavor) + public const string AttributeNetworkProtocolName = "network.protocol.name"; public const string AttributeServerAddress = "server.address"; // replaces: "net.host.name" (AttributeNetHostName) public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort) public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: http.user_agent (AttributeHttpUserAgent) diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj index 2e29e2d52c..1b5143a588 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj +++ b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj @@ -22,6 +22,7 @@ + diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/EnabledOnDockerPlatformTheoryAttribute.cs b/test/OpenTelemetry.Contrib.Tests.Shared/EnabledOnDockerPlatformTheoryAttribute.cs new file mode 100644 index 0000000000..ef96054645 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Tests.Shared/EnabledOnDockerPlatformTheoryAttribute.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma warning disable IDE0005 // Using directive is unnecessary.using System; +using System; +using System.Diagnostics; +using System.Text; +using Xunit; +#pragma warning restore IDE0005 // Using directive is unnecessary. + +namespace OpenTelemetry.Tests; + +/// +/// This skips tests if the required Docker engine is not available. +/// +internal class EnabledOnDockerPlatformTheoryAttribute : TheoryAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public EnabledOnDockerPlatformTheoryAttribute(DockerPlatform dockerPlatform) + { + const string executable = "docker"; + + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + + void AppendStdout(object sender, DataReceivedEventArgs e) => stdout.Append(e.Data); + void AppendStderr(object sender, DataReceivedEventArgs e) => stderr.Append(e.Data); + + var processStartInfo = new ProcessStartInfo(); + processStartInfo.FileName = executable; + processStartInfo.Arguments = string.Join(" ", "version", "--format '{{.Server.Os}}'"); + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + processStartInfo.UseShellExecute = false; + + var process = new Process(); + process.StartInfo = processStartInfo; + process.OutputDataReceived += AppendStdout; + process.ErrorDataReceived += AppendStderr; + + try + { + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + finally + { + process.OutputDataReceived -= AppendStdout; + process.ErrorDataReceived -= AppendStderr; + } + + if (0.Equals(process.ExitCode) && stdout.ToString().IndexOf(dockerPlatform.ToString(), StringComparison.OrdinalIgnoreCase) > 0) + { + return; + } + + this.Skip = $"The Docker {dockerPlatform} engine is not available."; + } + + public enum DockerPlatform + { + /// + /// Docker Linux engine. + /// + Linux, + + /// + /// Docker Windows engine. + /// + Windows, + } +} diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/EventSourceTestHelper.cs b/test/OpenTelemetry.Contrib.Tests.Shared/EventSourceTestHelper.cs index ced10b60e9..b7a88fa1af 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/EventSourceTestHelper.cs +++ b/test/OpenTelemetry.Contrib.Tests.Shared/EventSourceTestHelper.cs @@ -3,12 +3,14 @@ #nullable enable +#pragma warning disable IDE0005 // Using directive is unnecessary. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Globalization; using System.Linq; using System.Reflection; +#pragma warning restore IDE0005 // Using directive is unnecessary. namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/OpenTelemetry.Contrib.Tests.Shared.csproj b/test/OpenTelemetry.Contrib.Tests.Shared/OpenTelemetry.Contrib.Tests.Shared.csproj index d6e2eeff92..78b3d66688 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/OpenTelemetry.Contrib.Tests.Shared.csproj +++ b/test/OpenTelemetry.Contrib.Tests.Shared/OpenTelemetry.Contrib.Tests.Shared.csproj @@ -1,7 +1,8 @@ - netstandard2.0;net462;net6.0;net8.0 + net8.0;net6.0 + $(TargetFrameworks);net462 false diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/TestEventListener.cs b/test/OpenTelemetry.Contrib.Tests.Shared/TestEventListener.cs index d0a102d9d2..57182d6232 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/TestEventListener.cs +++ b/test/OpenTelemetry.Contrib.Tests.Shared/TestEventListener.cs @@ -3,10 +3,12 @@ #nullable enable +#pragma warning disable IDE0005 // Using directive is unnecessary. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Threading; +#pragma warning restore IDE0005 // Using directive is unnecessary. namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Contrib.Tests.Shared/TestSampler.cs b/test/OpenTelemetry.Contrib.Tests.Shared/TestSampler.cs index 8bee1c8e66..90c3e769c7 100644 --- a/test/OpenTelemetry.Contrib.Tests.Shared/TestSampler.cs +++ b/test/OpenTelemetry.Contrib.Tests.Shared/TestSampler.cs @@ -3,8 +3,10 @@ #nullable enable +#pragma warning disable IDE0005 // Using directive is unnecessary. using System; using OpenTelemetry.Trace; +#pragma warning restore IDE0005 // Using directive is unnecessary. namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs new file mode 100644 index 0000000000..27a4886967 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class EventSourceTest +{ + [Fact] + public void EventSourceTest_SqlClientInstrumentationEventSource() + { + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(SqlClientInstrumentationEventSource.Log); + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj new file mode 100644 index 0000000000..4d979780d9 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj @@ -0,0 +1,31 @@ + + + Unit test project for OpenTelemetry SqlClient instrumentations + net8.0;net7.0;net6.0 + $(TargetFrameworks);net462 + enable + + disable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs new file mode 100644 index 0000000000..9b77fe5deb --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs @@ -0,0 +1,118 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Data; +using System.Diagnostics; +using System.Runtime.InteropServices; +using DotNet.Testcontainers.Containers; +using Microsoft.Data.SqlClient; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Testcontainers.MsSql; +using Testcontainers.SqlEdge; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public sealed class SqlClientIntegrationTests : IAsyncLifetime +{ + // The Microsoft SQL Server Docker image is not compatible with ARM devices, such as Macs with Apple Silicon. + private readonly IContainer databaseContainer = Architecture.Arm64.Equals(RuntimeInformation.ProcessArchitecture) ? new SqlEdgeBuilder().Build() : new MsSqlBuilder().Build(); + + public Task InitializeAsync() + { + return this.databaseContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return this.databaseContainer.DisposeAsync().AsTask(); + } + + [Trait("CategoryName", "SqlIntegrationTests")] + [EnabledOnDockerPlatformTheory(EnabledOnDockerPlatformTheoryAttribute.DockerPlatform.Linux)] + [InlineData(CommandType.Text, "select 1/1", false)] + [InlineData(CommandType.Text, "select 1/1", false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, false, false)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, true, false)] + [InlineData(CommandType.StoredProcedure, "sp_who", false)] + [InlineData(CommandType.StoredProcedure, "sp_who", true)] + public void SuccessfulCommandTest( + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent = false, + bool isFailure = false, + bool recordException = false, + bool shouldEnrich = true) + { +#if NETFRAMEWORK + // Disable things not available on netfx + recordException = false; + shouldEnrich = false; +#endif + + var sampler = new TestSampler(); + var activities = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddInMemoryExporter(activities) + .AddSqlClientInstrumentation(options => + { +#if !NETFRAMEWORK + options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + options.SetDbStatementForText = captureTextCommandContent; +#else + options.SetDbStatementForText = captureStoredProcedureCommandName || captureTextCommandContent; +#endif + options.RecordException = recordException; + if (shouldEnrich) + { + options.Enrich = SqlClientTests.ActivityEnrichment; + } + }) + .Build(); + + using SqlConnection sqlConnection = new SqlConnection(this.GetConnectionString()); + + sqlConnection.Open(); + + string dataSource = sqlConnection.DataSource; + + sqlConnection.ChangeDatabase("master"); +#pragma warning disable CA2100 + using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) +#pragma warning restore CA2100 + { + CommandType = commandType, + }; + + try + { + sqlCommand.ExecuteNonQuery(); + } + catch + { + } + + Assert.Single(activities); + var activity = activities[0]; + + SqlClientTests.VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, recordException, shouldEnrich, dataSource, activity); + SqlClientTests.VerifySamplingParameters(sampler.LatestSamplingParameters); + } + + private string GetConnectionString() + { + switch (this.databaseContainer) + { + case SqlEdgeContainer container: + return container.GetConnectionString(); + case MsSqlContainer container: + return container.GetConnectionString(); + default: + throw new InvalidOperationException($"Container type ${this.databaseContainer.GetType().Name} not supported."); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs new file mode 100644 index 0000000000..6dd7e7e3fb --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -0,0 +1,468 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Data; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlClientTests : IDisposable +{ +#if !NETFRAMEWORK + private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master"; +#endif + + private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource; + + public SqlClientTests() + { + this.fakeSqlClientDiagnosticSource = new FakeSqlClientDiagnosticSource(); + } + + public void Dispose() + { + this.fakeSqlClientDiagnosticSource.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void SqlClient_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddSqlClientInstrumentation()); + } + + [Fact] + public void SqlClient_NamedOptions() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddSqlClientInstrumentation() + .AddSqlClientInstrumentation("Instrumentation2", configureSqlClientTraceInstrumentationOptions: null) + .Build(); + + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } + + // DiagnosticListener-based instrumentation is only available on .NET Core +#if !NETFRAMEWORK + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true, false)] + public void SqlClientCallsAreCollectedSuccessfully( + string beforeCommand, + string afterCommand, + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent, + bool shouldEnrich = true) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + (opt) => + { + opt.SetDbStatementForText = captureTextCommandContent; + opt.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + if (shouldEnrich) + { + opt.Enrich = ActivityEnrichment; + } + }) + .AddInMemoryExporter(activities) + .Build()) + { + var operationId = Guid.NewGuid(); + sqlCommand.CommandType = commandType; +#pragma warning disable CA2100 + sqlCommand.CommandText = commandText; +#pragma warning restore CA2100 + + var beforeExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + beforeCommand, + beforeExecuteEventData); + + var afterExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = 2000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + afterCommand, + afterExecuteEventData); + } + + Assert.Single(activities); + var activity = activities[0]; + + VerifyActivityData( + sqlCommand.CommandType, + sqlCommand.CommandText, + captureStoredProcedureCommandName, + captureTextCommandContent, + false, + false, + shouldEnrich, + sqlConnection.DataSource, + activity); + } + + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false, true)] + public void SqlClientErrorsAreCollectedSuccessfully(string beforeCommand, string errorCommand, bool shouldEnrich = true, bool recordException = false) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation(options => + { + options.RecordException = recordException; + if (shouldEnrich) + { + options.Enrich = ActivityEnrichment; + } + }) + .AddInMemoryExporter(activities) + .Build()) + { + var operationId = Guid.NewGuid(); + sqlCommand.CommandText = "SP_GetOrders"; + sqlCommand.CommandType = CommandType.StoredProcedure; + + var beforeExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + beforeCommand, + beforeExecuteEventData); + + var commandErrorEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Exception = new Exception("Boom!"), + Timestamp = 2000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + errorCommand, + commandErrorEventData); + } + + Assert.Single(activities); + var activity = activities[0]; + + VerifyActivityData( + sqlCommand.CommandType, + sqlCommand.CommandText, + true, + false, + true, + recordException, + shouldEnrich, + sqlConnection.DataSource, + activity); + } + + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand)] + public void SqlClientCreatesActivityWithDbSystem( + string beforeCommand) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var sampler = new TestSampler + { + SamplingAction = _ => new SamplingResult(SamplingDecision.Drop), + }; + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation() + .SetSampler(sampler) + .Build()) + { + this.fakeSqlClientDiagnosticSource.Write(beforeCommand, new { }); + } + + VerifySamplingParameters(sampler.LatestSamplingParameters); + } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrue() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 2"; + }, + cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandText == "select 2"; + } + + return true; + }); + + Assert.Single(activities); + Assert.True(activities[0].IsAllDataRequested); + Assert.True(activities[0].ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + } + + [Fact] + public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalse() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandText == "select 2"; + } + + return true; + }); + + Assert.Empty(activities); + } + + [Fact] + public void ShouldNotCollectTelemetryAndShouldNotPropagateExceptionWhenFilterThrowsException() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => throw new InvalidOperationException("foobar")); + + Assert.Empty(activities); + } +#endif + + internal static void VerifyActivityData( + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent, + bool isFailure, + bool recordException, + bool shouldEnrich, + string dataSource, + Activity activity) + { + Assert.Equal("master", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + + if (!isFailure) + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + else + { + var status = activity.GetStatus(); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.NotNull(activity.StatusDescription); + + if (recordException) + { + var events = activity.Events.ToList(); + Assert.Single(events); + + Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); + } + else + { + Assert.Empty(activity.Events); + } + } + + if (shouldEnrich) + { + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enriched")); + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enriched").FirstOrDefault().Value); + } + else + { + Assert.Empty(activity.Tags.Where(tag => tag.Key == "enriched")); + } + + Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); + Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); + + switch (commandType) + { + case CommandType.StoredProcedure: + if (captureStoredProcedureCommandName) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + + break; + + case CommandType.Text: + if (captureTextCommandContent) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + + break; + } + + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + + internal static void VerifySamplingParameters(SamplingParameters samplingParameters) + { + Assert.NotNull(samplingParameters.Tags); + Assert.Contains( + samplingParameters.Tags, + kvp => kvp.Key == SemanticConventions.AttributeDbSystem + && (string)kvp.Value == SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName); + } + + internal static void ActivityEnrichment(Activity activity, string method, object obj) + { + activity.SetTag("enriched", "yes"); + + switch (method) + { + case "OnCustom": + Assert.True(obj is SqlCommand); + break; + + default: + break; + } + } + +#if !NETFRAMEWORK + private Activity[] RunCommandWithFilter( + Action sqlCommandSetup, + Func filter) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => + { + options.Filter = filter; + }) + .AddInMemoryExporter(activities) + .Build()) + { + var operationId = Guid.NewGuid(); + sqlCommandSetup(sqlCommand); + + var beforeExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, + beforeExecuteEventData); + + var afterExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = 2000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, + afterExecuteEventData); + } + + return activities.ToArray(); + } +#endif + + private class FakeSqlClientDiagnosticSource : IDisposable + { + private readonly DiagnosticListener listener; + + public FakeSqlClientDiagnosticSource() + { + this.listener = new DiagnosticListener(SqlClientInstrumentation.SqlClientDiagnosticListenerName); + } + + public void Write(string name, object value) + { + if (this.listener.IsEnabled(name)) + { + this.listener.Write(name, value); + } + } + + public void Dispose() + { + this.listener.Dispose(); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs new file mode 100644 index 0000000000..306804929c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs @@ -0,0 +1,93 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlClientTraceInstrumentationOptionsTests +{ + static SqlClientTraceInstrumentationOptionsTests() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + var listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + } + + [Theory] + [InlineData("localhost", "localhost", null, null, null)] + [InlineData("127.0.0.1", null, "127.0.0.1", null, null)] + [InlineData("127.0.0.1,1433", null, "127.0.0.1", null, null)] + [InlineData("127.0.0.1, 1818", null, "127.0.0.1", null, "1818")] + [InlineData("127.0.0.1 \\ instanceName", null, "127.0.0.1", "instanceName", null)] + [InlineData("127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData("tcp:127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData("tcp:localhost", "localhost", null, null, null)] + [InlineData("tcp : localhost", "localhost", null, null, null)] + [InlineData("np : localhost", "localhost", null, null, null)] + [InlineData("lpc:localhost", "localhost", null, null, null)] + [InlineData("np:\\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] + [InlineData("np : \\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] + [InlineData("np:\\\\localhost\\pipe\\MSSQL$instanceName\\sql\\query", "localhost", null, "instanceName", null)] + public void ParseDataSourceTests( + string dataSource, + string expectedServerHostName, + string expectedServerIpAddress, + string expectedInstanceName, + string expectedPort) + { + var sqlConnectionDetails = SqlClientTraceInstrumentationOptions.ParseDataSource(dataSource); + + Assert.NotNull(sqlConnectionDetails); + Assert.Equal(expectedServerHostName, sqlConnectionDetails.ServerHostName); + Assert.Equal(expectedServerIpAddress, sqlConnectionDetails.ServerIpAddress); + Assert.Equal(expectedInstanceName, sqlConnectionDetails.InstanceName); + Assert.Equal(expectedPort, sqlConnectionDetails.Port); + } + + [Theory] + [InlineData(true, "localhost", "localhost", null, null, null)] + [InlineData(true, "127.0.0.1,1433", null, "127.0.0.1", null, null)] + [InlineData(true, "127.0.0.1,1434", null, "127.0.0.1", null, "1434")] + [InlineData(true, "127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData(false, "localhost", "localhost", null, null, null)] + public void SqlClientTraceInstrumentationOptions_EnableConnectionLevelAttributes( + bool enableConnectionLevelAttributes, + string dataSource, + string expectedServerHostName, + string expectedServerIpAddress, + string expectedInstanceName, + string expectedPort) + { + var source = new ActivitySource("sql-client-instrumentation"); + var activity = source.StartActivity("Test Sql Activity"); + var options = new SqlClientTraceInstrumentationOptions() + { + EnableConnectionLevelAttributes = enableConnectionLevelAttributes, + }; + options.AddConnectionLevelDetailsToActivity(dataSource, activity); + + if (!enableConnectionLevelAttributes) + { + Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + else + { + Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } + + Assert.Equal(expectedServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Equal(expectedInstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); + Assert.Equal(expectedPort, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs new file mode 100644 index 0000000000..1f48748e28 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs @@ -0,0 +1,371 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlEventSourceTests +{ + /* + To run the integration tests, set the OTEL_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string. + + To use Docker... + 1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest + 2) Set OTEL_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word + */ + + private const string SqlConnectionStringEnvVarName = "OTEL_SQLCONNECTIONSTRING"; + private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName); + + [Trait("CategoryName", "SqlIntegrationTests")] + [SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)] + [InlineData(CommandType.Text, "select 1/1", false)] + [InlineData(CommandType.Text, "select 1/0", false, true)] + [InlineData(CommandType.StoredProcedure, "sp_who", false)] + [InlineData(CommandType.StoredProcedure, "sp_who", true)] + public async Task SuccessfulCommandTest(CommandType commandType, string commandText, bool captureText, bool isFailure = false) + { + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = captureText; + }) + .Build(); + + using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); + + await sqlConnection.OpenAsync(); + + string dataSource = sqlConnection.DataSource; + + sqlConnection.ChangeDatabase("master"); + +#pragma warning disable CA2100 + using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) +#pragma warning restore CA2100 + { + CommandType = commandType, + }; + + try + { + await sqlCommand.ExecuteNonQueryAsync(); + } + catch + { + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + VerifyActivityData(commandText, captureText, isFailure, dataSource, activity); + } + + [Theory] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/1", false)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/0", false, true)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/1", false)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/0", false, true)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] + public void EventSourceFakeTests( + Type eventSourceType, + CommandType commandType, + string commandText, + bool captureText, + bool isFailure = false, + int sqlExceptionNumber = 0, + bool enableConnectionLevelAttributes = false) + { + using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = captureText; + options.EnableConnectionLevelAttributes = enableConnectionLevelAttributes; + }) + .Build(); + + int objectId = Guid.NewGuid().GetHashCode(); + + fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandType == CommandType.StoredProcedure ? commandText : string.Empty); + + // success is stored in the first bit in compositeState 0b001 + int successFlag = !isFailure ? 1 : 0; + + // isSqlException is stored in the second bit in compositeState 0b010 + int isSqlExceptionFlag = sqlExceptionNumber > 0 ? 2 : 0; + + // synchronous state is stored in the third bit in compositeState 0b100 + int synchronousFlag = false ? 4 : 0; + + int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; + + fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, sqlExceptionNumber); + shutdownSignal.Dispose(); + Assert.Single(exportedItems); + + var activity = exportedItems[0]; + + VerifyActivityData(commandText, captureText, isFailure, "127.0.0.1", activity, enableConnectionLevelAttributes); + } + + [Theory] + [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] + public void EventSourceFakeUnknownEventWithNullPayloadTest(Type eventSourceType) + { + using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); + + fakeSqlEventSource.WriteUnknownEventWithNullPayload(); + + shutdownSignal.Dispose(); + + Assert.Empty(exportedItems); + } + + [Theory] + [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] + public void EventSourceFakeInvalidPayloadTest(Type eventSourceType) + { + using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); + + fakeSqlEventSource.WriteBeginExecuteEvent("arg1"); + + fakeSqlEventSource.WriteEndExecuteEvent("arg1", "arg2", "arg3", "arg4"); + shutdownSignal.Dispose(); + + Assert.Empty(exportedItems); + } + + [Theory] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeBehavingMdsSqlEventSource))] + public void DefaultCaptureTextFalse(Type eventSourceType) + { + using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); + + int objectId = Guid.NewGuid().GetHashCode(); + + const string commandText = "TestCommandTest"; + fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandText); + + // success is stored in the first bit in compositeState 0b001 + int successFlag = 1; + + // isSqlException is stored in the second bit in compositeState 0b010 + int isSqlExceptionFlag = 2; + + // synchronous state is stored in the third bit in compositeState 0b100 + int synchronousFlag = 4; + + int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; + + fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, 0); + shutdownSignal.Dispose(); + Assert.Single(exportedItems); + + var activity = exportedItems[0]; + + const bool captureText = false; + VerifyActivityData(commandText, captureText, false, "127.0.0.1", activity, false); + } + + private static void VerifyActivityData( + string commandText, + bool captureText, + bool isFailure, + string dataSource, + Activity activity, + bool enableConnectionLevelAttributes = false) + { + Assert.Equal("master", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); + + if (!enableConnectionLevelAttributes) + { + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + else + { + var connectionDetails = SqlClientTraceInstrumentationOptions.ParseDataSource(dataSource); + + if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) + { + Assert.Equal(connectionDetails.ServerHostName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + } + else + { + Assert.Equal(connectionDetails.ServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + } + + if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) + { + Assert.Equal(connectionDetails.InstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); + } + + if (!string.IsNullOrEmpty(connectionDetails.Port)) + { + Assert.Equal(connectionDetails.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + } + } + + Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); + + if (captureText) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + + if (!isFailure) + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + else + { + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.NotNull(activity.StatusDescription); + } + } + +#pragma warning disable SA1201 // Elements should appear in the correct order + + // Helper interface to be able to have single test method for multiple EventSources, want to keep it close to the event sources themselves. + private interface IFakeBehavingSqlEventSource : IDisposable +#pragma warning restore SA1201 // Elements should appear in the correct order + { + void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText); + + void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber); + } + + private interface IFakeMisbehavingSqlEventSource : IDisposable + { + void WriteBeginExecuteEvent(string arg1); + + void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4); + + void WriteUnknownEventWithNullPayload(); + } + + [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeFriendly")] + private class FakeBehavingAdoNetSqlEventSource : EventSource, IFakeBehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) + { + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); + } + + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); + } + } + + [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeFriendly")] + private class FakeBehavingMdsSqlEventSource : EventSource, IFakeBehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) + { + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); + } + + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); + } + } + + [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeEvil")] + private class FakeMisbehavingAdoNetSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(string arg1) + { + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); + } + + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); + } + + [Event(3)] + public void WriteUnknownEventWithNullPayload() + { + object[] args = null; + + this.WriteEvent(3, args); + } + } + + [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeEvil")] + private class FakeMisbehavingMdsSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(string arg1) + { + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); + } + + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); + } + + [Event(3)] + public void WriteUnknownEventWithNullPayload() + { + object[] args = null; + + this.WriteEvent(3, args); + } + } +} +#endif