From 396beda15e680ca969b2ae8c5c53e31760dfc4ac Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 8 May 2026 21:25:49 -0500 Subject: [PATCH] Another fix for reported OpenTelemetry issue. The test in #1795 assigned a listener for `Dapr.Workflow` which showed this as working. By default, .NET returns null silently if there's no activity listener registered for a source meaning the bug persisted when a listener wasn't explicitly registered. Fixed this and added another regression test. Signed-off-by: Whit Waldo --- src/Dapr.Workflow/Worker/WorkflowWorker.cs | 16 +++-- .../Worker/WorkflowWorkerTests.cs | 68 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Workflow/Worker/WorkflowWorker.cs b/src/Dapr.Workflow/Worker/WorkflowWorker.cs index 79d17f623..33de41ef0 100644 --- a/src/Dapr.Workflow/Worker/WorkflowWorker.cs +++ b/src/Dapr.Workflow/Worker/WorkflowWorker.cs @@ -632,16 +632,24 @@ public override async Task StopAsync(CancellationToken cancellationToken) return null; var traceState = request.ParentTraceContext?.TraceState; - if (ActivityContext.TryParse(traceParent, traceState, out var parentCtx)) + if (ActivityContext.TryParse(traceParent, traceState, isRemote: true, out var parentCtx)) { - return WorkflowActivitySource.StartActivity( + // Prefer ActivitySource so registered telemetry listeners (e.g. OpenTelemetry) receive the span. + var started = WorkflowActivitySource.StartActivity( name: $"WorkflowActivity {request.Name}", kind: ActivityKind.Internal, parentContext: parentCtx, []); + if (started != null) + return started; + + // ActivitySource.StartActivity returns null when no listener is registered for "Dapr.Workflow". + // Fall through to ensure Activity.Current is always non-null inside user activity code, + // regardless of whether OpenTelemetry or another telemetry listener is configured. } - - // Fall back to raw parent ID if parsing fails + + // Always create an Activity directly (not via ActivitySource) so that Activity.Current is + // non-null in user activity code even when no telemetry listener is registered. var act = new Activity($"WorkflowActivity {request.Name}"); act.SetParentId(traceParent); if (!string.IsNullOrEmpty(traceState)) diff --git a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs index 3316ce6d9..24e02fd6b 100644 --- a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs +++ b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs @@ -268,7 +268,73 @@ public async Task HandleActivityResponseAsync_ShouldFallBackToSetParentId_WhenTr Assert.Null(response.FailureDetails); Assert.Equal(malformedParentId, observedParentId); } - + + [Fact] + public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_WithoutRegisteredListener() + { + // Regression test for issue #1749: Activity.Current must be non-null inside user activity + // code even when NO ActivityListener is registered for "Dapr.Workflow". The original patch + // in #1795 used WorkflowActivitySource.StartActivity(), which returns null when there is + // no listener — leaving Activity.Current null for users who haven't wired up OpenTelemetry. + // Explicitly do NOT register any ActivityListener here to reproduce the real-world scenario. + var sp = new ServiceCollection().BuildServiceProvider(); + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var options = new WorkflowRuntimeOptions(); + + const string expectedTraceId = "0af7651916cd43dd8448eb211c80319c"; + const string parentSpanId = "b7ad6b7169203331"; + const string traceParent = $"00-{expectedTraceId}-{parentSpanId}-01"; + const string traceState = "vendor=value"; + + Activity? observedCurrent = null; + string? observedTraceId = null; + string? observedParentSpanId = null; + string? observedTraceState = null; + + var factory = new StubWorkflowsFactory(); + factory.AddActivity("act", new InlineActivity( + inputType: typeof(int), + run: (_, _) => + { + observedCurrent = Activity.Current; + observedTraceId = Activity.Current?.TraceId.ToHexString(); + observedParentSpanId = Activity.Current?.ParentSpanId.ToHexString(); + observedTraceState = Activity.Current?.TraceStateString; + return Task.FromResult(null); + })); + + var worker = new WorkflowWorker( + CreateGrpcClientMock().Object, + factory, + NullLoggerFactory.Instance, + serializer, + sp, + options); + + Activity.Current = null; + + var request = new ActivityRequest + { + Name = "act", + TaskId = 5, + Input = string.Empty, + OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-5" }, + ParentTraceContext = new TraceContext + { + TraceParent = traceParent, + TraceState = traceState + } + }; + + var response = await InvokeHandleActivityResponseAsync(worker, request); + + Assert.Null(response.FailureDetails); + Assert.NotNull(observedCurrent); + Assert.Equal(expectedTraceId, observedTraceId); + Assert.Equal(parentSpanId, observedParentSpanId); + Assert.Equal(traceState, observedTraceState); + } + [Fact] public void Constructor_ShouldThrowArgumentNullException_WhenGrpcClientIsNull() {