Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
the new conventions.
([#2811](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2811))

* Added `OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION`
environment variable to propagate trace context to SQL Server databases.
([#2709](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2709))

> Propagate `traceparent` information to SQL Server databases
(see [SET CONTEXT_INFO](https://learn.microsoft.com/en-us/sql/t-sql/statements/set-context-info-transact-sql?view=sql-server-ver16)).
Note that this option incurs an additional round-trip to the database.

## 1.12.0-beta.1

Released 2025-May-06
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Diagnostics.CodeAnalysis;
#endif
using System.Globalization;
using System.Text;
using OpenTelemetry.Trace;

namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
Expand All @@ -26,6 +27,9 @@ internal sealed class SqlClientDiagnosticListener : ListenerHandler
public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError";
public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError";

private const string ContextInfoParameterName = "@opentelemetry_traceparent";
private const string SetContextSql = $"set context_info {ContextInfoParameterName}";

private readonly PropertyFetcher<object> commandFetcher = new("Command");
private readonly PropertyFetcher<object> connectionFetcher = new("Connection");
private readonly PropertyFetcher<string> dataSourceFetcher = new("DataSource");
Expand Down Expand Up @@ -64,6 +68,13 @@ public override void OnEventWritten(string name, object? payload)
return;
}

// skip if this is an injected query
if (options.EnableTraceContextPropagation &&
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
{
return;
}

_ = this.connectionFetcher.TryFetch(command, out var connection);
_ = this.databaseFetcher.TryFetch(connection, out var databaseName);
_ = this.dataSourceFetcher.TryFetch(connection, out var dataSource);
Expand All @@ -82,6 +93,26 @@ public override void OnEventWritten(string name, object? payload)
return;
}

if (options.EnableTraceContextPropagation &&
command is IDbCommand { CommandType: CommandType.Text, Connection.State: ConnectionState.Open } iDbCommand)
{
var setContextCommand = iDbCommand.Connection.CreateCommand();
setContextCommand.Transaction = iDbCommand.Transaction;
setContextCommand.CommandText = SetContextSql;
setContextCommand.CommandType = CommandType.Text;
var parameter = setContextCommand.CreateParameter();
parameter.ParameterName = ContextInfoParameterName;

var tracedflags = (activity.ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0 ? "01" : "00";
var traceparent = $"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-{tracedflags}";

parameter.DbType = DbType.Binary;
parameter.Value = Encoding.UTF8.GetBytes(traceparent);
setContextCommand.Parameters.Add(parameter);

setContextCommand.ExecuteNonQuery();
}

if (activity.IsAllDataRequested)
{
try
Expand Down Expand Up @@ -168,6 +199,15 @@ public override void OnEventWritten(string name, object? payload)
case SqlDataAfterExecuteCommand:
case SqlMicrosoftAfterExecuteCommand:
{
_ = this.commandFetcher.TryFetch(payload, out var command);

// skip if this is an injected query
if (options.EnableTraceContextPropagation &&
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
{
return;
}

if (activity == null)
{
SqlClientInstrumentationEventSource.Log.NullActivity(name);
Expand All @@ -189,6 +229,15 @@ public override void OnEventWritten(string name, object? payload)
case SqlDataWriteCommandError:
case SqlMicrosoftWriteCommandError:
{
_ = this.commandFetcher.TryFetch(payload, out var command);

// skip if this is an injected query
if (options.EnableTraceContextPropagation &&
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
{
return;
}

if (activity == null)
{
SqlClientInstrumentationEventSource.Log.NullActivity(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Tracing;
using Microsoft.Extensions.Configuration;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
Expand All @@ -10,7 +11,7 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
/// EventSource events emitted from the project.
/// </summary>
[EventSource(Name = "OpenTelemetry-Instrumentation-SqlClient")]
internal sealed class SqlClientInstrumentationEventSource : EventSource
internal sealed class SqlClientInstrumentationEventSource : EventSource, IConfigurationExtensionsLogger
{
public static SqlClientInstrumentationEventSource Log = new();

Expand Down Expand Up @@ -82,4 +83,15 @@ public void CommandFilterException(string exception)
{
this.WriteEvent(7, exception);
}

[Event(8, Message = "Configuration key '{0}' has an invalid value: '{1}'", Level = EventLevel.Warning)]
public void InvalidConfigurationValue(string key, string value)
{
this.WriteEvent(8, key, value);
}

void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value)
{
this.InvalidConfigurationValue(key, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Configuration\*.cs" Link="Includes\Configuration\%(Filename).cs" />
<Compile Include="$(RepoRoot)\src\Shared\DatabaseSemanticConventionHelper.cs" Link="Includes\DatabaseSemanticConventionHelper.cs" />
<Compile Include="$(RepoRoot)\src\Shared\DiagnosticSourceListener.cs" Link="Includes\DiagnosticSourceListener.cs" />
<Compile Include="$(RepoRoot)\src\Shared\DiagnosticSourceSubscriber.cs" Link="Includes\DiagnosticSourceSubscriber.cs" />
Expand Down
13 changes: 13 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,19 @@ using var traceProvider = Sdk.CreateTracerProviderBuilder()
{
```

### Trace Context Propagation

> [!NOTE]
> Only `CommandType.Text` commands are supported for trace context propagation.

Database trace context propagation can be enabled by setting
`OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION`
environment variable to `true`.
This uses the [SET CONTEXT_INFO](https://learn.microsoft.com/en-us/sql/t-sql/statements/set-context-info-transact-sql?view=sql-server-ver16)
command to set [traceparent](https://www.w3.org/TR/trace-context/#traceparent-header)information
for the current connection, which results in
**an additional round-trip to the database**.

## References

* [OpenTelemetry Project](https://opentelemetry.io/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Data;
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using OpenTelemetry.Instrumentation.SqlClient.Implementation;
using OpenTelemetry.Trace;
using static OpenTelemetry.Internal.DatabaseSemanticConventionHelper;

Expand All @@ -17,6 +18,8 @@ namespace OpenTelemetry.Instrumentation.SqlClient;
/// </remarks>
public class SqlClientTraceInstrumentationOptions
{
internal const string ContextPropagationLevelEnvVar = "OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION";

/// <summary>
/// Initializes a new instance of the <see cref="SqlClientTraceInstrumentationOptions"/> class.
/// </summary>
Expand All @@ -30,6 +33,16 @@ internal SqlClientTraceInstrumentationOptions(IConfiguration configuration)
var databaseSemanticConvention = GetSemanticConventionOptIn(configuration);
this.EmitOldAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.Old);
this.EmitNewAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.New);

Debug.Assert(configuration != null, "configuration was null");

if (configuration!.TryGetBoolValue(
SqlClientInstrumentationEventSource.Log,
ContextPropagationLevelEnvVar,
out var enableTraceContextPropagation))
{
this.EnableTraceContextPropagation = enableTraceContextPropagation;
}
}

/// <summary>
Expand Down Expand Up @@ -126,4 +139,16 @@ internal SqlClientTraceInstrumentationOptions(IConfiguration configuration)
/// Gets or sets a value indicating whether the new database attributes should be emitted.
/// </summary>
internal bool EmitNewAttributes { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to send traceparent information to SQL Server database.
/// </summary>
/// <remarks>
/// <para>
/// <b>Only `CommandType.Text` commands are supported for trace context propagation.</b>
/// Note: This uses the SET CONTEXT_INFO command to set traceparent information
/// for the current connection, which results in an additional round-trip to the database.
/// </para>
/// </remarks>
internal bool EnableTraceContextPropagation { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Data;
using System.Diagnostics;
using System.Text;
using Microsoft.Data.SqlClient;
using OpenTelemetry.Instrumentation.SqlClient.Implementation;
using OpenTelemetry.Tests;
Expand All @@ -16,6 +17,8 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Tests;
[Trait("CategoryName", "SqlIntegrationTests")]
public sealed class SqlClientIntegrationTests : IClassFixture<SqlClientIntegrationTestsFixture>
{
private const string GetContextInfoQuery = "SELECT CONTEXT_INFO()";

private readonly SqlClientIntegrationTestsFixture fixture;

public SqlClientIntegrationTests(SqlClientIntegrationTestsFixture fixture)
Expand All @@ -28,6 +31,8 @@ public SqlClientIntegrationTests(SqlClientIntegrationTestsFixture fixture)
[InlineData(CommandType.Text, "select 1/1", true, "select ?/?")]
[InlineData(CommandType.Text, "select 1/0", false, null, true)]
[InlineData(CommandType.Text, "select 1/0", false, null, true, true)]
[InlineData(CommandType.Text, GetContextInfoQuery, false, null, false, false, false)]
[InlineData(CommandType.Text, GetContextInfoQuery, false, null, false, false, true)]
#if NETFRAMEWORK
[InlineData(CommandType.StoredProcedure, "sp_who", false, null)]
#else
Expand All @@ -40,8 +45,14 @@ public void SuccessfulCommandTest(
bool captureTextCommandContent = false,
string? sanitizedCommandText = null,
bool isFailure = false,
bool recordException = false)
bool recordException = false,
bool enableTransaction = false)
{
if (commandText == GetContextInfoQuery)
{
Environment.SetEnvironmentVariable(SqlClientTraceInstrumentationOptions.ContextPropagationLevelEnvVar, "true");
}

#if NETFRAMEWORK
// Disable things not available on netfx
recordException = false;
Expand All @@ -66,24 +77,37 @@ public void SuccessfulCommandTest(
var dataSource = sqlConnection.DataSource;

sqlConnection.ChangeDatabase("master");
SqlTransaction? transaction = null;
#pragma warning disable CA2100
using var sqlCommand = new SqlCommand(commandText, sqlConnection)
#pragma warning restore CA2100
{
CommandType = commandType,
};

if (enableTransaction)
{
transaction = sqlConnection.BeginTransaction();
sqlCommand.Transaction = transaction;
}

object commandResult = DBNull.Value;
try
{
sqlCommand.ExecuteNonQuery();
commandResult = sqlCommand.ExecuteScalar();
}
catch
{
}

transaction?.Commit();

Assert.Single(activities);
var activity = activities[0];

#if !NETFRAMEWORK
VerifyContextInfo(commandText, commandResult, activity);
#endif
VerifyActivityData(commandType, sanitizedCommandText, captureTextCommandContent, isFailure, recordException, activity);
VerifySamplingParameters(sampler.LatestSamplingParameters);

Expand All @@ -103,6 +127,20 @@ public void SuccessfulCommandTest(
}
}

private static void VerifyContextInfo(
string? commandText,
object commandResult,
Activity activity)
{
if (commandText == GetContextInfoQuery)
{
Assert.NotEqual(commandResult, DBNull.Value);
Assert.True(commandResult is byte[]);
var contextInfo = Encoding.ASCII.GetString((byte[])commandResult).TrimEnd('\0');
Assert.Equal(contextInfo, activity.Id);
}
}

private static void VerifyActivityData(
CommandType commandType,
string? commandText,
Expand Down