Skip to content

Commit 0237b99

Browse files
sincejuneKielekzacharycmontoyacuichenlialanwest
authored
[Instrumentation.SqlClient] Introducing context propagation to SQL Server (#2709)
Co-authored-by: Piotr Kiełkowicz <[email protected]> Co-authored-by: Zach Montoya <[email protected]> Co-authored-by: Will Li <[email protected]> Co-authored-by: Alan West <[email protected]>
1 parent 8e317c1 commit 0237b99

File tree

7 files changed

+171
-3
lines changed

7 files changed

+171
-3
lines changed

src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
the new conventions.
1111
([#2811](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2811))
1212

13+
* Added `OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION`
14+
environment variable to propagate trace context to SQL Server databases.
15+
This will remain experimental while the [specification](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/sql-server.md#context-propagation)
16+
remains in development.
17+
It is now only available on .NET 8 and newer.
18+
([#2709](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2709))
19+
20+
> Propagate `traceparent` information to SQL Server databases
21+
(see [SET CONTEXT_INFO](https://learn.microsoft.com/en-us/sql/t-sql/statements/set-context-info-transact-sql?view=sql-server-ver16)).
22+
Note that this option incurs an additional round-trip to the database.
23+
1324
## 1.12.0-beta.1
1425

1526
Released 2025-May-06

src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using System.Diagnostics.CodeAnalysis;
99
#endif
1010
using System.Globalization;
11+
#if NET
12+
using System.Text;
13+
#endif
1114
using OpenTelemetry.Trace;
1215

1316
namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
@@ -26,6 +29,11 @@ internal sealed class SqlClientDiagnosticListener : ListenerHandler
2629
public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError";
2730
public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError";
2831

32+
#if NET
33+
private const string ContextInfoParameterName = "@opentelemetry_traceparent";
34+
private const string SetContextSql = $"set context_info {ContextInfoParameterName}";
35+
#endif
36+
2937
private readonly PropertyFetcher<object> commandFetcher = new("Command");
3038
private readonly PropertyFetcher<object> connectionFetcher = new("Connection");
3139
private readonly PropertyFetcher<string> dataSourceFetcher = new("DataSource");
@@ -64,6 +72,15 @@ public override void OnEventWritten(string name, object? payload)
6472
return;
6573
}
6674

75+
#if NET
76+
// skip if this is an injected query
77+
if (options.EnableTraceContextPropagation &&
78+
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
79+
{
80+
return;
81+
}
82+
#endif
83+
6784
_ = this.connectionFetcher.TryFetch(command, out var connection);
6885
_ = this.databaseFetcher.TryFetch(connection, out var databaseName);
6986
_ = this.dataSourceFetcher.TryFetch(connection, out var dataSource);
@@ -82,6 +99,28 @@ public override void OnEventWritten(string name, object? payload)
8299
return;
83100
}
84101

102+
#if NET
103+
if (options.EnableTraceContextPropagation &&
104+
command is IDbCommand { CommandType: CommandType.Text, Connection.State: ConnectionState.Open } iDbCommand)
105+
{
106+
var setContextCommand = iDbCommand.Connection.CreateCommand();
107+
setContextCommand.Transaction = iDbCommand.Transaction;
108+
setContextCommand.CommandText = SetContextSql;
109+
setContextCommand.CommandType = CommandType.Text;
110+
var parameter = setContextCommand.CreateParameter();
111+
parameter.ParameterName = ContextInfoParameterName;
112+
113+
var tracedflags = (activity.ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0 ? "01" : "00";
114+
var traceparent = $"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-{tracedflags}";
115+
116+
parameter.DbType = DbType.Binary;
117+
parameter.Value = Encoding.UTF8.GetBytes(traceparent);
118+
setContextCommand.Parameters.Add(parameter);
119+
120+
setContextCommand.ExecuteNonQuery();
121+
}
122+
#endif
123+
85124
if (activity.IsAllDataRequested)
86125
{
87126
try
@@ -168,6 +207,17 @@ public override void OnEventWritten(string name, object? payload)
168207
case SqlDataAfterExecuteCommand:
169208
case SqlMicrosoftAfterExecuteCommand:
170209
{
210+
_ = this.commandFetcher.TryFetch(payload, out var command);
211+
212+
#if NET
213+
// skip if this is an injected query
214+
if (options.EnableTraceContextPropagation &&
215+
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
216+
{
217+
return;
218+
}
219+
#endif
220+
171221
if (activity == null)
172222
{
173223
SqlClientInstrumentationEventSource.Log.NullActivity(name);
@@ -189,6 +239,17 @@ public override void OnEventWritten(string name, object? payload)
189239
case SqlDataWriteCommandError:
190240
case SqlMicrosoftWriteCommandError:
191241
{
242+
_ = this.commandFetcher.TryFetch(payload, out var command);
243+
244+
#if NET
245+
// skip if this is an injected query
246+
if (options.EnableTraceContextPropagation &&
247+
command is IDbCommand { CommandType: CommandType.Text, CommandText: SetContextSql })
248+
{
249+
return;
250+
}
251+
#endif
252+
192253
if (activity == null)
193254
{
194255
SqlClientInstrumentationEventSource.Log.NullActivity(name);

src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System.Diagnostics.Tracing;
5+
using Microsoft.Extensions.Configuration;
56
using OpenTelemetry.Internal;
67

78
namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
@@ -10,7 +11,7 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Implementation;
1011
/// EventSource events emitted from the project.
1112
/// </summary>
1213
[EventSource(Name = "OpenTelemetry-Instrumentation-SqlClient")]
13-
internal sealed class SqlClientInstrumentationEventSource : EventSource
14+
internal sealed class SqlClientInstrumentationEventSource : EventSource, IConfigurationExtensionsLogger
1415
{
1516
public static SqlClientInstrumentationEventSource Log = new();
1617

@@ -82,4 +83,15 @@ public void CommandFilterException(string exception)
8283
{
8384
this.WriteEvent(7, exception);
8485
}
86+
87+
[Event(8, Message = "Configuration key '{0}' has an invalid value: '{1}'", Level = EventLevel.Warning)]
88+
public void InvalidConfigurationValue(string key, string value)
89+
{
90+
this.WriteEvent(8, key, value);
91+
}
92+
93+
void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value)
94+
{
95+
this.InvalidConfigurationValue(key, value);
96+
}
8597
}

src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
19+
<Compile Include="$(RepoRoot)\src\Shared\Configuration\*.cs" Link="Includes\Configuration\%(Filename).cs" />
1920
<Compile Include="$(RepoRoot)\src\Shared\DatabaseSemanticConventionHelper.cs" Link="Includes\DatabaseSemanticConventionHelper.cs" />
2021
<Compile Include="$(RepoRoot)\src\Shared\DiagnosticSourceListener.cs" Link="Includes\DiagnosticSourceListener.cs" />
2122
<Compile Include="$(RepoRoot)\src\Shared\DiagnosticSourceSubscriber.cs" Link="Includes\DiagnosticSourceSubscriber.cs" />

src/OpenTelemetry.Instrumentation.SqlClient/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ using var traceProvider = Sdk.CreateTracerProviderBuilder()
281281
{
282282
```
283283

284+
### Trace Context Propagation
285+
286+
> [!NOTE]
287+
> Only `CommandType.Text` commands are supported for trace context propagation.
288+
> Only .NET runtimes are supported.
289+
290+
Database trace context propagation can be enabled by setting
291+
`OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION`
292+
environment variable to `true`.
293+
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)
294+
command to set [traceparent](https://www.w3.org/TR/trace-context/#traceparent-header)information
295+
for the current connection, which results in
296+
**an additional round-trip to the database**.
297+
284298
## References
285299

286300
* [OpenTelemetry Project](https://opentelemetry.io/)

src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System.Data;
55
using System.Diagnostics;
66
using Microsoft.Extensions.Configuration;
7+
#if NET
8+
using OpenTelemetry.Instrumentation.SqlClient.Implementation;
9+
#endif
710
using OpenTelemetry.Trace;
811
using static OpenTelemetry.Internal.DatabaseSemanticConventionHelper;
912

@@ -17,6 +20,8 @@ namespace OpenTelemetry.Instrumentation.SqlClient;
1720
/// </remarks>
1821
public class SqlClientTraceInstrumentationOptions
1922
{
23+
internal const string ContextPropagationLevelEnvVar = "OTEL_DOTNET_EXPERIMENTAL_SQLCLIENT_ENABLE_TRACE_CONTEXT_PROPAGATION";
24+
2025
/// <summary>
2126
/// Initializes a new instance of the <see cref="SqlClientTraceInstrumentationOptions"/> class.
2227
/// </summary>
@@ -30,6 +35,18 @@ internal SqlClientTraceInstrumentationOptions(IConfiguration configuration)
3035
var databaseSemanticConvention = GetSemanticConventionOptIn(configuration);
3136
this.EmitOldAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.Old);
3237
this.EmitNewAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.New);
38+
39+
#if NET
40+
Debug.Assert(configuration != null, "configuration was null");
41+
42+
if (configuration!.TryGetBoolValue(
43+
SqlClientInstrumentationEventSource.Log,
44+
ContextPropagationLevelEnvVar,
45+
out var enableTraceContextPropagation))
46+
{
47+
this.EnableTraceContextPropagation = enableTraceContextPropagation;
48+
}
49+
#endif
3350
}
3451

3552
/// <summary>
@@ -126,4 +143,18 @@ internal SqlClientTraceInstrumentationOptions(IConfiguration configuration)
126143
/// Gets or sets a value indicating whether the new database attributes should be emitted.
127144
/// </summary>
128145
internal bool EmitNewAttributes { get; set; }
146+
147+
#if NET
148+
/// <summary>
149+
/// Gets or sets a value indicating whether to send traceparent information to SQL Server database.
150+
/// </summary>
151+
/// <remarks>
152+
/// <para>
153+
/// <b>Only `CommandType.Text` commands are supported for trace context propagation.</b>
154+
/// Note: This uses the SET CONTEXT_INFO command to set traceparent information
155+
/// for the current connection, which results in an additional round-trip to the database.
156+
/// </para>
157+
/// </remarks>
158+
internal bool EnableTraceContextPropagation { get; set; }
159+
#endif
129160
}

test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Data;
55
using System.Diagnostics;
6+
using System.Text;
67
using Microsoft.Data.SqlClient;
78
using OpenTelemetry.Instrumentation.SqlClient.Implementation;
89
using OpenTelemetry.Tests;
@@ -16,6 +17,8 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Tests;
1617
[Trait("CategoryName", "SqlIntegrationTests")]
1718
public sealed class SqlClientIntegrationTests : IClassFixture<SqlClientIntegrationTestsFixture>
1819
{
20+
private const string GetContextInfoQuery = "SELECT CONTEXT_INFO()";
21+
1922
private readonly SqlClientIntegrationTestsFixture fixture;
2023

2124
public SqlClientIntegrationTests(SqlClientIntegrationTestsFixture fixture)
@@ -28,6 +31,10 @@ public SqlClientIntegrationTests(SqlClientIntegrationTestsFixture fixture)
2831
[InlineData(CommandType.Text, "select 1/1", true, "select ?/?")]
2932
[InlineData(CommandType.Text, "select 1/0", false, null, true)]
3033
[InlineData(CommandType.Text, "select 1/0", false, null, true, true)]
34+
#if NET
35+
[InlineData(CommandType.Text, GetContextInfoQuery, false, null, false, false, false)]
36+
[InlineData(CommandType.Text, GetContextInfoQuery, false, null, false, false, true)]
37+
#endif
3138
#if NETFRAMEWORK
3239
[InlineData(CommandType.StoredProcedure, "sp_who", false, null)]
3340
#else
@@ -40,8 +47,14 @@ public void SuccessfulCommandTest(
4047
bool captureTextCommandContent = false,
4148
string? sanitizedCommandText = null,
4249
bool isFailure = false,
43-
bool recordException = false)
50+
bool recordException = false,
51+
bool enableTransaction = false)
4452
{
53+
if (commandText == GetContextInfoQuery)
54+
{
55+
Environment.SetEnvironmentVariable(SqlClientTraceInstrumentationOptions.ContextPropagationLevelEnvVar, "true");
56+
}
57+
4558
#if NETFRAMEWORK
4659
// Disable things not available on netfx
4760
recordException = false;
@@ -66,24 +79,35 @@ public void SuccessfulCommandTest(
6679
var dataSource = sqlConnection.DataSource;
6780

6881
sqlConnection.ChangeDatabase("master");
82+
SqlTransaction? transaction = null;
6983
#pragma warning disable CA2100
7084
using var sqlCommand = new SqlCommand(commandText, sqlConnection)
7185
#pragma warning restore CA2100
7286
{
7387
CommandType = commandType,
7488
};
7589

90+
if (enableTransaction)
91+
{
92+
transaction = sqlConnection.BeginTransaction();
93+
sqlCommand.Transaction = transaction;
94+
}
95+
96+
object commandResult = DBNull.Value;
7697
try
7798
{
78-
sqlCommand.ExecuteNonQuery();
99+
commandResult = sqlCommand.ExecuteScalar();
79100
}
80101
catch
81102
{
82103
}
83104

105+
transaction?.Commit();
106+
84107
Assert.Single(activities);
85108
var activity = activities[0];
86109

110+
VerifyContextInfo(commandText, commandResult, activity);
87111
VerifyActivityData(commandType, sanitizedCommandText, captureTextCommandContent, isFailure, recordException, activity);
88112
VerifySamplingParameters(sampler.LatestSamplingParameters);
89113

@@ -103,6 +127,20 @@ public void SuccessfulCommandTest(
103127
}
104128
}
105129

130+
private static void VerifyContextInfo(
131+
string? commandText,
132+
object commandResult,
133+
Activity activity)
134+
{
135+
if (commandText == GetContextInfoQuery)
136+
{
137+
Assert.NotEqual(commandResult, DBNull.Value);
138+
Assert.True(commandResult is byte[]);
139+
var contextInfo = Encoding.ASCII.GetString((byte[])commandResult).TrimEnd('\0');
140+
Assert.Equal(contextInfo, activity.Id);
141+
}
142+
}
143+
106144
private static void VerifyActivityData(
107145
CommandType commandType,
108146
string? commandText,

0 commit comments

Comments
 (0)