Skip to content

Commit b99a179

Browse files
feat(csharp): add statement-level trace parent support (#3896)
## Summary Enable setting trace parent at the statement level for connection pooling scenarios like Power BI, where multiple queries reuse the same connection but need different trace IDs for distributed tracing. ## Changes - **TracingStatement**: Add `_statementTraceParent` field and `SetTraceParent()` method - **TracingStatement**: `TraceParent` property now returns statement override or falls back to connection's trace parent - **HiveServer2Statement**: Add `SetOption` case for `AdbcOptions.Telemetry.TraceParent` - Add comprehensive test coverage for statement-level trace parent functionality ## Motivation This brings C# implementation to parity with Go drivers which already support statement-level trace parent via `SetOption`. ### Power BI Use Case Power BI uses connection pooling where multiple queries reuse the same connection. Each Power BI query has its own trace ID for distributed tracing correlation. Without statement-level trace parent support, all queries from a pooled connection would share the same trace context, making it impossible to correlate individual query traces. With this change, Power BI can: ```csharp var connection = pool.GetConnection(); // Query 1 with Trace ID A var stmt1 = connection.CreateStatement(); stmt1.SetOption("adbc.telemetry.trace_parent", powerBiQueryTraceIdA); stmt1.ExecuteQuery(); // Query 2 with Trace ID B (same connection!) var stmt2 = connection.CreateStatement(); stmt2.SetOption("adbc.telemetry.trace_parent", powerBiQueryTraceIdB); stmt2.ExecuteQuery(); ``` ## Test Plan Added `CanSetTraceParentOnStatement` test that verifies: 1. Statement inherits connection's trace parent by default 2. Statement can override with its own trace parent via `SetOption` 3. Activities created by the statement use the statement's trace parent 4. Setting trace parent to null falls back to connection's trace parent All existing tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent dfa8a56 commit b99a179

File tree

3 files changed

+88
-1
lines changed

3 files changed

+88
-1
lines changed

csharp/src/Apache.Arrow.Adbc/Tracing/TracingStatement.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace Apache.Arrow.Adbc.Tracing
2222
public abstract class TracingStatement : AdbcStatement, ITracingStatement
2323
{
2424
private readonly ActivityTrace _trace;
25+
private string? _statementTraceParent;
2526

2627
public TracingStatement(TracingConnection connection)
2728
{
@@ -30,7 +31,12 @@ public TracingStatement(TracingConnection connection)
3031

3132
ActivityTrace IActivityTracer.Trace => _trace;
3233

33-
string? IActivityTracer.TraceParent => _trace.TraceParent;
34+
string? IActivityTracer.TraceParent => _statementTraceParent ?? _trace.TraceParent;
35+
36+
protected void SetTraceParent(string? traceParent)
37+
{
38+
_statementTraceParent = traceParent;
39+
}
3440

3541
public abstract string AssemblyVersion { get; }
3642

csharp/src/Drivers/Apache/Hive2/HiveServer2Statement.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ public override void SetOption(string key, string value)
319319
this.EscapePatternWildcards = escapePatternWildcards;
320320
}
321321
break;
322+
case AdbcOptions.Telemetry.TraceParent:
323+
SetTraceParent(string.IsNullOrWhiteSpace(value) ? null : value);
324+
break;
322325
default:
323326
throw AdbcException.NotImplemented($"Option '{key}' is not implemented.");
324327
}

csharp/test/Apache.Arrow.Adbc.Tests/Tracing/TracingTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,57 @@ internal async Task CanDetectInvalidAsyncCall()
255255
}
256256
}
257257

258+
[Fact]
259+
internal void CanSetTraceParentOnStatement()
260+
{
261+
string activitySourceName = NewName();
262+
Queue<Activity> exportedActivities = new();
263+
using TracerProvider provider = Sdk.CreateTracerProviderBuilder()
264+
.AddSource(activitySourceName)
265+
.AddTestActivityQueueExporter(exportedActivities)
266+
.Build();
267+
268+
// Create a connection with a connection-level trace parent
269+
const string connectionTraceParent = "00-11111111111111111111111111111111-2222222222222222-01";
270+
var connection = new MyTracingConnection(
271+
new Dictionary<string, string> { { AdbcOptions.Telemetry.TraceParent, connectionTraceParent } },
272+
activitySourceName);
273+
274+
// Create a statement and verify it uses the connection's trace parent
275+
var statement = new MyTracingStatement(connection);
276+
Assert.Equal(connectionTraceParent, ((IActivityTracer)statement).TraceParent);
277+
278+
// Test 1: Execute with connection's trace parent
279+
string eventName1 = NewName();
280+
statement.MethodWithActivity(eventName1);
281+
Assert.Single(exportedActivities);
282+
var activity1 = exportedActivities.First();
283+
Assert.Equal(connectionTraceParent, activity1.ParentId);
284+
285+
// Test 2: Set statement-specific trace parent
286+
exportedActivities.Clear();
287+
const string statementTraceParent = "00-33333333333333333333333333333333-4444444444444444-01";
288+
statement.SetOption(AdbcOptions.Telemetry.TraceParent, statementTraceParent);
289+
Assert.Equal(statementTraceParent, ((IActivityTracer)statement).TraceParent);
290+
291+
string eventName2 = NewName();
292+
statement.MethodWithActivity(eventName2);
293+
Assert.Single(exportedActivities);
294+
var activity2 = exportedActivities.First();
295+
Assert.Equal(statementTraceParent, activity2.ParentId);
296+
297+
// Test 3: Set trace parent to null (should fall back to connection's trace parent)
298+
exportedActivities.Clear();
299+
statement.SetOption(AdbcOptions.Telemetry.TraceParent, null);
300+
Assert.Equal(connectionTraceParent, ((IActivityTracer)statement).TraceParent);
301+
302+
string eventName3 = NewName();
303+
statement.MethodWithActivity(eventName3);
304+
Assert.Single(exportedActivities);
305+
var activity3 = exportedActivities.First();
306+
Assert.Equal(connectionTraceParent, activity3.ParentId);
307+
}
308+
258309
internal static string NewName() => Guid.NewGuid().ToString().Replace("-", "").ToLower();
259310

260311
protected virtual void Dispose(bool disposing)
@@ -451,6 +502,33 @@ public void OnCompleted(Action continuation)
451502
public override IArrowArrayStream GetTableTypes() => throw new NotImplementedException();
452503
}
453504

505+
private class MyTracingStatement(TracingConnection connection) : TracingStatement(connection)
506+
{
507+
public override string AssemblyVersion => "1.0.0";
508+
public override string AssemblyName => "TestStatement";
509+
510+
public void MethodWithActivity(string activityName)
511+
{
512+
this.TraceActivity(activity =>
513+
{
514+
activity?.AddTag("testTag", "testValue");
515+
}, activityName);
516+
}
517+
518+
public override void SetOption(string key, string? value)
519+
{
520+
if (key == AdbcOptions.Telemetry.TraceParent)
521+
{
522+
SetTraceParent(string.IsNullOrWhiteSpace(value) ? null : value);
523+
return;
524+
}
525+
throw AdbcException.NotImplemented($"Option '{key}' is not implemented.");
526+
}
527+
528+
public override QueryResult ExecuteQuery() => throw new NotImplementedException();
529+
public override UpdateResult ExecuteUpdate() => throw new NotImplementedException();
530+
}
531+
454532
internal class ActivityQueueExporter(Queue<Activity> exportedActivities) : BaseExporter<Activity>
455533
{
456534
private Queue<Activity> ExportedActivities { get; } = exportedActivities;

0 commit comments

Comments
 (0)