Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
the new conventions.
([#2811](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2811))

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

Valid options for this environment variable are:
* `trace`: 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.
* `disabled`: No context propagation performed, default value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why trace and disabled? If it's just a matter of this feature being enabled or not, then wouldn't a simple true/false value be sufficient?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATION_LEVEL seems to suggest there might be other "levels" in the future? If not then I'd suggest the name OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_CONTEXT_PROPAGATION.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that, there is high chance that there will be support also for service.name. Please check open-telemetry/semantic-conventions#2495

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see. The name OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATION_LEVEL still doesn't seem quite right to me, though.

It seems possible that users may want to enable propagating both service.name and traceparent.

Two ideas come to my mind.

  1. Rename OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATION_LEVEL to OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATORS. This config would accept a comma-separated list of propagators (e.g., traceparent,servicename), Empty list would be the default.
  2. Two separate configuration options.
    • OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION=true
    • OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_SERVICE_NAME_PROPAGATION=true

Without better understanding the direction the spec is taking on context propagation for database instrumentation, I kind of prefer the second option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.


## 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.ContextPropagationLevel == SqlClientTraceInstrumentationOptions.ContextPropagationLevelTrace &&
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.ContextPropagationLevel == SqlClientTraceInstrumentationOptions.ContextPropagationLevelTrace &&
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.ContextPropagationLevel == SqlClientTraceInstrumentationOptions.ContextPropagationLevelTrace &&
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.ContextPropagationLevel == SqlClientTraceInstrumentationOptions.ContextPropagationLevelTrace &&
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 @@ -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
15 changes: 15 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,21 @@ using var traceProvider = Sdk.CreateTracerProviderBuilder()
{
```

### Trace Context Propagation

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

Database context propagation can be enabled by setting
`OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATION_LEVEL` environment variable.
The valid values are:

* `disabled` - No context propagation is performed. This is the default value.
* `trace` - Enables sending trace information to databases
in [W3C traceparent text format](https://www.w3.org/TR/trace-context/#traceparent-header).
This uses the `SET CONTEXT_INFO` command to set trace context in SQL Server,
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 @@ -17,6 +17,18 @@ namespace OpenTelemetry.Instrumentation.SqlClient;
/// </remarks>
public class SqlClientTraceInstrumentationOptions
{
internal const string ContextPropagationLevelEnvVar = "OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_CONTEXT_PROPAGATION_LEVEL";

/// <summary>
/// Flag to send traceparent information to SQL Server.
/// </summary>
internal const string ContextPropagationLevelTrace = "trace";

/// <summary>
/// Flag to disable sending trace information to SQL Server.
/// </summary>
internal const string ContextPropagationDisabled = "disabled";

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

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

if (configuration!.TryGetStringValue(ContextPropagationLevelEnvVar, out var contextPropagationLevel)
&& contextPropagationLevel == ContextPropagationLevelTrace)
{
this.ContextPropagationLevel = contextPropagationLevel;
}
}

/// <summary>
Expand Down Expand Up @@ -126,4 +147,15 @@ 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 trace information to SQL Server database.
/// Optional values:
/// <see langword="trace"/>:
/// Send traceparent information to SQL Server.
/// <see langword="disabled"/>:
/// Disable sending trace information to SQL Server.
/// Default value: <see langword="disabled"/>.
/// </summary>
internal string ContextPropagationLevel { 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,7 @@ 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)]
#if NETFRAMEWORK
[InlineData(CommandType.StoredProcedure, "sp_who", false, null)]
#else
Expand All @@ -42,6 +46,11 @@ public void SuccessfulCommandTest(
bool isFailure = false,
bool recordException = false)
{
if (commandText == GetContextInfoQuery)
{
Environment.SetEnvironmentVariable(SqlClientTraceInstrumentationOptions.ContextPropagationLevelEnvVar, SqlClientTraceInstrumentationOptions.ContextPropagationLevelTrace);
}

#if NETFRAMEWORK
// Disable things not available on netfx
recordException = false;
Expand Down Expand Up @@ -72,10 +81,10 @@ public void SuccessfulCommandTest(
{
CommandType = commandType,
};

object commandResult = DBNull.Value;
try
{
sqlCommand.ExecuteNonQuery();
commandResult = sqlCommand.ExecuteScalar();
}
catch
{
Expand All @@ -84,6 +93,7 @@ public void SuccessfulCommandTest(
Assert.Single(activities);
var activity = activities[0];

VerifyContextInfo(commandText, commandResult, activity);
VerifyActivityData(commandType, sanitizedCommandText, captureTextCommandContent, isFailure, recordException, activity);
VerifySamplingParameters(sampler.LatestSamplingParameters);

Expand All @@ -103,6 +113,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
Loading