[Instrumentation.SqlClient] Move to contrib repository (#5559)

Co-authored-by: Reiley Yang <reyang@microsoft.com>
This commit is contained in:
Piotr Kiełkowicz 2024-04-24 20:22:36 +02:00 committed by GitHub
parent 994b890be1
commit 98a5d3e5b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 3 additions and 2692 deletions

View File

@ -77,7 +77,6 @@
<PackageVersion Include="Grpc.AspNetCore.Server" Version="[2.59.0, 3.0)" />
<PackageVersion Include="Grpc.Tools" Version="[2.59.0,3.0)" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="[3.11.0-beta1.23525.2]" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.1.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[8.0.0,)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="[8.0.0,)" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="[8.0.0,)" />

View File

@ -130,10 +130,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.AspNetCore", "exam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "test\Benchmarks\Benchmarks.csproj", "{DE9130A4-F30A-49D7-8834-41DE3021218B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.SqlClient.Tests", "test\OpenTelemetry.Instrumentation.SqlClient.Tests\OpenTelemetry.Instrumentation.SqlClient.Tests.csproj", "{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.SqlClient", "src\OpenTelemetry.Instrumentation.SqlClient\OpenTelemetry.Instrumentation.SqlClient.csproj", "{41C30DC1-8C57-4E14-A83A-7635A3C7960B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Http.Tests", "test\OpenTelemetry.Instrumentation.Http.Tests\OpenTelemetry.Instrumentation.Http.Tests.csproj", "{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.Http", "src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj", "{412C64D1-43D6-4E4C-8AD8-E20E63B415BD}"
@ -436,14 +432,6 @@ Global
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE9130A4-F30A-49D7-8834-41DE3021218B}.Release|Any CPU.Build.0 = Release|Any CPU
{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C606039-BE0A-4EE6-B8F7-F75B41E52CB8}.Release|Any CPU.Build.0 = Release|Any CPU
{41C30DC1-8C57-4E14-A83A-7635A3C7960B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41C30DC1-8C57-4E14-A83A-7635A3C7960B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41C30DC1-8C57-4E14-A83A-7635A3C7960B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41C30DC1-8C57-4E14-A83A-7635A3C7960B}.Release|Any CPU.Build.0 = Release|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE9EB7D8-9CC5-4073-90B3-9FBF685B3305}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -53,8 +53,6 @@ libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/ma
[Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md)
* HTTP clients: [System.Net.Http.HttpClient and
System.Net.HttpWebRequest](./src/OpenTelemetry.Instrumentation.Http/README.md)
* SQL clients: [Microsoft.Data.SqlClient and
System.Data.SqlClient](./src/OpenTelemetry.Instrumentation.SqlClient/README.md)
Here are the [exporter
libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#exporter-library):

View File

@ -49,7 +49,6 @@
* Unstable:
* `OpenTelemetry.Instrumentation.GrpcNetClient` (`Instrumentation.GrpcNetClient-`)
* `OpenTelemetry.Instrumentation.SqlClient` (`Instrumentation.SqlClient-`)
* As of the `1.9.0` release cycle instrumentation packages and core
unstable packages always depend on the stable versions of core

View File

@ -114,7 +114,6 @@ the library they instrument, and steps for enabling them.
* [gRPC
client](../../../src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md)
* [HTTP clients](../../../src/OpenTelemetry.Instrumentation.Http/README.md)
* [SQL client](../../../src/OpenTelemetry.Instrumentation.SqlClient/README.md)
More community contributed instrumentations are available in [OpenTelemetry .NET
Contrib](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src).
@ -145,7 +144,7 @@ Writing an instrumentation library typically involves 3 steps.
itself. For example, System.Data.SqlClient when running on .NET Framework
happens to publish events using an `EventSource` which the [SqlClient
instrumentation
library](../../../src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs)
library](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs)
listens to in order to trigger code as Sql commands are executed. The [.NET
Framework HttpWebRequest
instrumentation](../../../src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs)
@ -190,7 +189,7 @@ Writing an instrumentation library typically involves 3 steps.
on the `TracerProviderBuilder` being configured.
An example instrumentation using this approach is [SqlClient
instrumentation](../../../src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs).
instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs).
> [!WARNING]
> The instrumentation libraries requiring state management are

View File

@ -1,18 +0,0 @@
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<System.Diagnostics.Activity, string, object>
OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Enrich.set -> void
OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Filter.get -> System.Func<object, bool>
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<OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions> configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action<OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions> configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder

View File

@ -1,10 +0,0 @@
// 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

View File

@ -1,271 +0,0 @@
# Changelog
## Unreleased
* `ActivitySource.Version` is set to NuGet package version.
([#5498](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5498))
## 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

View File

@ -1,29 +0,0 @@
// 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;
/// <summary>
/// Helper class to hold common properties used by both SqlClientDiagnosticListener on .NET Core
/// and SqlEventSourceListener on .NET Framework.
/// </summary>
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<KeyValuePair<string, object>> CreationTags = new[]
{
new KeyValuePair<string, object>(SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName),
};
}

View File

@ -1,204 +0,0 @@
// 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<object> commandFetcher = new("Command");
private readonly PropertyFetcher<object> connectionFetcher = new("Connection");
private readonly PropertyFetcher<object> dataSourceFetcher = new("DataSource");
private readonly PropertyFetcher<object> databaseFetcher = new("Database");
private readonly PropertyFetcher<CommandType> commandTypeFetcher = new("CommandType");
private readonly PropertyFetcher<object> commandTextFetcher = new("CommandText");
private readonly PropertyFetcher<Exception> 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

View File

@ -1,85 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Tracing;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
/// <summary>
/// EventSource events emitted from the project.
/// </summary>
[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);
}
}

View File

@ -1,191 +0,0 @@
// 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;
/// <summary>
/// On .NET Framework, neither System.Data.SqlClient nor Microsoft.Data.SqlClient emit DiagnosticSource events.
/// Instead they use EventSource:
/// For System.Data.SqlClient see: <a href="https://github.com/microsoft/referencesource/blob/3b1eaf5203992df69de44c783a3eda37d3d4cd10/System.Data/System/Data/Common/SqlEventSource.cs#L29">reference source</a>.
/// For Microsoft.Data.SqlClient see: <a href="https://github.com/dotnet/SqlClient/blob/ac8bb3f9132e6c104dc3e307fe2d569daed0776f/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs#L15">SqlClientEventSource</a>.
///
/// We hook into these event sources and process their BeginExecute/EndExecute events.
/// </summary>
/// <remarks>
/// 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".
/// </remarks>
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

View File

@ -1,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksForLibraries)</TargetFrameworks>
<Description>SqlClient instrumentation for OpenTelemetry .NET</Description>
<PackageTags>$(PackageTags);distributed-tracing</PackageTags>
<MinVerTagPrefix>Instrumentation.SqlClient-</MinVerTagPrefix>
<IncludeDiagnosticSourceInstrumentationHelpers>true</IncludeDiagnosticSourceInstrumentationHelpers>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<!--Do not run Package Baseline Validation as this package has never released a stable version.
Remove this property once we have released a stable version.-->
<PropertyGroup>
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Shims\NullableAttributes.cs" Link="Includes\Shims\NullableAttributes.cs" />
</ItemGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Api.ProviderBuilderExtensions\OpenTelemetry.Api.ProviderBuilderExtensions.csproj" />
</ItemGroup>
<!-- Instrumentation packages when published should take a dependency only on the latest stable version of the API/SDK. -->
<ItemGroup Condition="'$(RunningDotNetPack)' == 'true'">
<PackageReference Include="OpenTelemetry.Api.ProviderBuilderExtensions" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View File

@ -1,276 +0,0 @@
# 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.
<!-- This comment is to make sure the two notes above and below are not merged -->
> [!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<object, bool>`. 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)

View File

@ -1,70 +0,0 @@
// 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;
/// <summary>
/// SqlClient instrumentation.
/// </summary>
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<string> 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<string, object, object, bool> isEnabled = (eventName, _, _)
=> DiagnosticSourceEvents.Contains(eventName);
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
#endif
/// <summary>
/// Initializes a new instance of the <see cref="SqlClientInstrumentation"/> class.
/// </summary>
/// <param name="options">Configuration options for sql instrumentation.</param>
#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
}
/// <inheritdoc/>
public void Dispose()
{
#if NETFRAMEWORK
this.sqlEventSourceListener?.Dispose();
#else
this.diagnosticSourceSubscriber?.Dispose();
#endif
}
}

View File

@ -1,302 +0,0 @@
// 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;
/// <summary>
/// Options for <see cref="SqlClientInstrumentation"/>.
/// </summary>
/// <remarks>
/// For help and examples see: <a href="https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.SqlClient/README.md#advanced-configuration" />.
/// </remarks>
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);
/// <summary>
/// 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.
/// </summary>
/// <see>
/// <a href="https://docs.microsoft.com/previous-versions/sql/sql-server-2016/ms189307(v=sql.130)"/>
/// </see>
private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled);
private static readonly ConcurrentDictionary<string, SqlConnectionDetails> ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets a value indicating whether or not the <see
/// cref="SqlClientInstrumentation"/> should add the names of <see
/// cref="CommandType.StoredProcedure"/> commands as the <see
/// cref="SemanticConventions.AttributeDbStatement"/> tag. Default
/// value: <see langword="true"/>.
/// </summary>
/// <remarks>
/// <para><b>SetDbStatementForStoredProcedure is only supported on .NET
/// and .NET Core runtimes.</b></para>
/// </remarks>
public bool SetDbStatementForStoredProcedure { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the <see
/// cref="SqlClientInstrumentation"/> should add the text of commands as
/// the <see cref="SemanticConventions.AttributeDbStatement"/> tag.
/// Default value: <see langword="false"/>.
/// </summary>
/// <remarks>
/// <para>
/// <b>WARNING: SetDbStatementForText will capture the raw
/// <c>CommandText</c>. Make sure your <c>CommandText</c> property never
/// contains any sensitive data.</b>
/// </para>
/// <para><b>SetDbStatementForText is supported on all runtimes.</b>
/// <list type="bullet">
/// <item>On .NET and .NET Core SetDbStatementForText only applies to
/// <c>SqlCommand</c>s with <see cref="CommandType.Text"/>.</item>
/// <item>On .NET Framework SetDbStatementForText applies to all
/// <c>SqlCommand</c>s regardless of <see cref="CommandType"/>.
/// <list type="bullet">
/// <item>When using <c>System.Data.SqlClient</c> use
/// SetDbStatementForText to capture StoredProcedure command
/// names.</item>
/// <item>When using <c>Microsoft.Data.SqlClient</c> use
/// SetDbStatementForText to capture Text, StoredProcedure, and all
/// other command text.</item>
/// </list></item>
/// </list>
/// </para>
/// </remarks>
public bool SetDbStatementForText { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the <see
/// cref="SqlClientInstrumentation"/> should parse the DataSource on a
/// SqlConnection into server name, instance name, and/or port
/// connection-level attribute tags. Default value: <see
/// langword="false"/>.
/// </summary>
/// <remarks>
/// <para>
/// <b>EnableConnectionLevelAttributes is supported on all runtimes.</b>
/// </para>
/// <para>
/// The default behavior is to set the SqlConnection DataSource as the <see cref="SemanticConventions.AttributePeerService"/> tag.
/// If enabled, SqlConnection DataSource will be parsed and the server name will be sent as the
/// <see cref="SemanticConventions.AttributeServerAddress"/> or <see cref="SemanticConventions.AttributeServerSocketAddress"/> tag,
/// the instance name will be sent as the <see cref="SemanticConventions.AttributeDbMsSqlInstanceName"/> tag,
/// and the port will be sent as the <see cref="SemanticConventions.AttributeServerPort"/> tag if it is not 1433 (the default port).
/// </para>
/// </remarks>
public bool EnableConnectionLevelAttributes { get; set; }
/// <summary>
/// Gets or sets an action to enrich an <see cref="Activity"/> with the
/// raw <c>SqlCommand</c> object.
/// </summary>
/// <remarks>
/// <para><b>Enrich is only executed on .NET and .NET Core
/// runtimes.</b></para>
/// The parameters passed to the enrich action are:
/// <list type="number">
/// <item>The <see cref="Activity"/> being enriched.</item>
/// <item>The name of the event. Currently only <c>"OnCustom"</c> is
/// used but more events may be added in the future.</item>
/// <item>The raw <c>SqlCommand</c> object from which additional
/// information can be extracted to enrich the <see
/// cref="Activity"/>.</item>
/// </list>
/// </remarks>
public Action<Activity, string, object> Enrich { get; set; }
/// <summary>
/// Gets or sets a filter function that determines whether or not to
/// collect telemetry about a command.
/// </summary>
/// <remarks>
/// <para><b>Filter is only executed on .NET and .NET Core
/// runtimes.</b></para>
/// Notes:
/// <list type="bullet">
/// <item>The first parameter passed to the filter function is the raw
/// <c>SqlCommand</c> object for the command being executed.</item>
/// <item>The return value for the filter function is interpreted as:
/// <list type="bullet">
/// <item>If filter returns <see langword="true" />, the command is
/// collected.</item>
/// <item>If filter returns <see langword="false" /> or throws an
/// exception the command is NOT collected.</item>
/// </list></item>
/// </list>
/// </remarks>
public Func<object, bool> Filter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the exception will be
/// recorded as <see cref="ActivityEvent"/> or not. Default value: <see
/// langword="false"/>.
/// </summary>
/// <remarks>
/// <para><b>RecordException is only supported on .NET and .NET Core
/// runtimes.</b></para>
/// <para>For specification details see: <see
/// href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md"/>.</para>
/// </remarks>
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; }
}
}

View File

@ -1,81 +0,0 @@
// 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;
/// <summary>
/// Extension methods to simplify registering of dependency instrumentation.
/// </summary>
public static class TracerProviderBuilderExtensions
{
/// <summary>
/// Enables SqlClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)]
#endif
public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProviderBuilder builder)
=> AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions: null);
/// <summary>
/// Enables SqlClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="configureSqlClientTraceInstrumentationOptions">Callback action for configuring <see cref="SqlClientTraceInstrumentationOptions"/>.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)]
#endif
public static TracerProviderBuilder AddSqlClientInstrumentation(
this TracerProviderBuilder builder,
Action<SqlClientTraceInstrumentationOptions> configureSqlClientTraceInstrumentationOptions)
=> AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions);
/// <summary>
/// Enables SqlClient instrumentation.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="name">Name which is used when retrieving options.</param>
/// <param name="configureSqlClientTraceInstrumentationOptions">Callback action for configuring <see cref="SqlClientTraceInstrumentationOptions"/>.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)]
#endif
public static TracerProviderBuilder AddSqlClientInstrumentation(
this TracerProviderBuilder builder,
string name,
Action<SqlClientTraceInstrumentationOptions> configureSqlClientTraceInstrumentationOptions)
{
Guard.ThrowIfNull(builder);
name ??= Options.DefaultName;
if (configureSqlClientTraceInstrumentationOptions != null)
{
builder.ConfigureServices(services => services.Configure(name, configureSqlClientTraceInstrumentationOptions));
}
builder.AddInstrumentation(sp =>
{
var sqlOptions = sp.GetRequiredService<IOptionsMonitor<SqlClientTraceInstrumentationOptions>>().Get(name);
return new SqlClientInstrumentation(sqlOptions);
});
builder.AddSource(SqlActivitySourceHelper.ActivitySourceName);
return builder;
}
}

View File

@ -26,7 +26,6 @@
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.AspNetCore" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.GrpcNetClient" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.Http" />
<TrimmerRootAssembly Include="OpenTelemetry.Instrumentation.SqlClient" />
<TrimmerRootAssembly Include="OpenTelemetry.Shims.OpenTracing" />
<TrimmerRootAssembly Include="OpenTelemetry" />

View File

@ -5,7 +5,7 @@ using OpenTelemetry.Internal;
using OpenTelemetry.Tests;
using Xunit;
namespace OpenTelemetry.Instrumentation.SqlClient.Tests;
namespace OpenTelemetry.Extensions.Propagators.Tests;
public class EventSourceTest
{

View File

@ -1,17 +0,0 @@
// 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);
}
}

View File

@ -1,34 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Unit test project for OpenTelemetry SqlClient instrumentations</Description>
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\EnabledOnDockerPlatformTheoryAttribute.cs" Link="Includes\EnabledOnDockerPlatformTheoryAttribute.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\SkipUnlessEnvVarFoundTheoryAttribute.cs" Link="Includes\SkipUnlessEnvVarFoundTheoryAttribute.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\EventSourceTestHelper.cs" Link="Includes\EventSourceTestHelper.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestEventListener.cs" Link="Includes\TestEventListener.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestSampler.cs" Link="Includes\TestSampler.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Testcontainers.MsSql" />
<PackageReference Include="Testcontainers.SqlEdge" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.SqlClient\OpenTelemetry.Instrumentation.SqlClient.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>
</Project>

View File

@ -1,117 +0,0 @@
// 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<Activity>();
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");
using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)
{
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.");
}
}
}

View File

@ -1,466 +0,0 @@
// 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;
#if !NETFRAMEWORK
using OpenTelemetry.Tests;
#endif
using OpenTelemetry.Trace;
using Xunit;
namespace OpenTelemetry.Instrumentation.SqlClient.Tests;
public class SqlClientTests : IDisposable
{
private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master";
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<ArgumentNullException>(() => builder.AddSqlClientInstrumentation());
}
[Fact]
public void SqlClient_NamedOptions()
{
int defaultExporterOptionsConfigureOptionsInvocations = 0;
int namedExporterOptionsConfigureOptionsInvocations = 0;
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services =>
{
services.Configure<SqlClientTraceInstrumentationOptions>(o => defaultExporterOptionsConfigureOptionsInvocations++);
services.Configure<SqlClientTraceInstrumentationOptions>("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<Activity>();
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;
sqlCommand.CommandText = commandText;
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<Activity>();
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<SqlCommand> sqlCommandSetup,
Func<object, bool> filter)
{
using var sqlConnection = new SqlConnection(TestConnectionString);
using var sqlCommand = sqlConnection.CreateCommand();
var activities = new List<Activity>();
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();
}
}
}

View File

@ -1,92 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
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<ActivityContext> 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));
}
}

View File

@ -1,369 +0,0 @@
// 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<Activity>();
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");
using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)
{
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<Activity>();
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<Activity>();
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<Activity>();
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<Activity>();
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