Skip to content

gRPC trace context is not properly propagated to the sidecar #1857

Closed

Description

@mtaghavi2005

Expected Behavior

When a .NET app calls Dapr over gRPC using DaprClient, the trace context sent to the Dapr sidecar should allow sidecar-created spans to be correlated as children of the application operation that initiated the call.

This should apply to the common gRPC call path used by Dapr operations such as pub/sub, service invocation, state, bindings, secrets, and other gRPC-backed APIs.

According to the Dapr W3C trace context documentation, Dapr uses traceparent for HTTP requests and grpc-trace-bin for gRPC requests. The same documentation states that in gRPC API calls, trace context is passed through the grpc-trace-bin header:

https://docs.dapr.io/operations/observability/tracing/w3c-tracing-overview/

Actual Behavior

Some trace information appears to be propagated, since the resulting spans can share the same trace ID. However, the span parent-child relationship is not preserved correctly for Dapr .NET SDK gRPC calls.

As a result, spans created by the Dapr sidecar may appear as separate branches instead of being parented under the application operation that initiated the Dapr call. In tracing tools like Aspire, this makes the trace look disconnected even though the spans may still belong to the same trace.

In the attached screenshots, the issue is shown with a pub/sub flow because it is easy to observe across publisher sidecar, subscriber sidecar, and subscriber app spans. However, the underlying issue is not pub/sub-specific, but generally included all grpc operations from them app to sidecar.

Image

After adding the missing gRPC trace metadata, including grpc-trace-bin, the sidecar-created spans are correlated under the original application operation with the expected parent-child relationship.

Image

Steps to Reproduce the Problem

  1. Create a .NET app that uses DaprClient over gRPC.
  2. Configure OpenTelemetry/Aspire tracing.
  3. Start an application request that has an active Activity.Current, for example POST /payments.
  4. Inside that request, call any Dapr gRPC API through DaprClient, for example:PublishEventAsync , GetStateAsync / SaveStateAsync or other gRPC-backed Dapr operations
    5- Observe the trace tree in Aspire or another OpenTelemetry viewer.
    6- Dapr sidecar spans may not be parented under the application span that initiated the SDK call.

Release Note

Activity

  1. WhitWaldo commented on Jun 24, 2026

    @WhitWaldo
    Contributor

    With the exception of Workflows, the Dapr .NET is a library that wraps the functionality provided by the Dapr runtime via HTTP and gRPC and not something that does anything that needs to be itself instrumented.

    If you really want to track the network hops, I suggest adding their autoinstrumenter for the HTTP client to your own project to automatically capture those traces.

    But absent Workflows where there's a distinct state machine managed in the SDK and the runtime is merely the orchestrator of it (and you already instrumented that last release) which warrant its own telemetry capture, or within the Dapr Agents or other agentic frameworks atop Dapr (which themselves includes relevant spans), this isn't necessary across the Dapr Client and I'm reluctant to add it here.

  2. mtaghavi2005 commented on Jun 25, 2026

    @mtaghavi2005
    ContributorAuthor

    Thanks for the clarification. I completely agree that the Dapr .NET SDK should not create additional spans for its normal client operations. That isn't what I'm proposing.

    The issue I'm trying to address is context propagation, not SDK instrumentation.

    The expected trace hierarchy should look something like this:

    Application operation
        └── gRPC call to Dapr sidecar
                └── Dapr sidecar operation
                        └── Infrastructure / external service
    

    However, what I currently observe is:

    Application operation
        └── gRPC call to Dapr sidecar
    
    Dapr sidecar operation
        └── Infrastructure / external service
    

    Although all spans belong to the same trace, the sidecar-created span is not correlated as a child of the application operation that initiated the Dapr call.

    To me this suggests one of two possibilities:

    1. The trace context is not being propagated to the runtime in the format the runtime expects.
    2. The runtime receives the propagated context but cannot reconstruct the parent span correctly.

    According to the Dapr documentation, the primary propagation mechanism for gRPC is grpc-trace-bin:

    https://docs.dapr.io/operations/observability/tracing/w3c-tracing-overview/

    I also checked the Java SDK, which explicitly propagates the binary gRPC trace context, consistent with the documentation.

    also experimented with [OpenTelemetry.Instrumentation.GrpcNetClient](https://www.nuget.org/packages/opentelemetry.instrumentation.grpcnetclient) (currently still in beta). When it is enabled, the trace hierarchy becomes correct.

    The instrumentation documentation explains that when downstream HttpClient instrumentation is suppressed, the gRPC instrumentation becomes responsible for propagating the ActivityContext and Baggage:

    https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md#suppressdownstreaminstrumentation

    While I haven't yet verified exactly which gRPC metadata is being written by the instrumentation, the fact that enabling it results in the expected parent-child relationship suggests that the missing piece is related to context propagation rather than additional SDK spans.

    Originally, I assumed this should be handled by the .NET SDK, which is why I opened this PR. However, while investigating further, I found that the Dapr runtime already attempts to support both propagation mechanisms: it first looks for grpc-trace-bin, and if that is not present, it falls back to extracting the context from the W3C traceparent header.

    This fallback was originally introduced because grpc-dotnet propagated traceparent instead of grpc-trace-bin:

    The comment for the related code is still here:
    https://github.com/dapr/dapr/blob/5b2ea177b3c7200dea233cbf884436ec40cf968c/pkg/diagnostics/grpc_tracing.go#L309

    Because of that, I'm no longer convinced the SDK is necessarily the right place for the fix. It is possible that the runtime is not reconstructing the parent span correctly from the traceparent fallback. I'm currently investigating that path before drawing any conclusions.

    My PR was intended to demonstrate one possible solution, but my actual goal is simply to ensure that the Dapr sidecar spans are correctly parented under the application operation, regardless of whether the solution ultimately belongs in the SDK or in the runtime.

    If I determine that the issue is in the runtime rather than in the SDK, I'll close this issue and open an issue against the runtime instead.

  3. WhitWaldo commented on Jun 25, 2026

    @WhitWaldo
    Contributor

    We had some issues with trace propagation at the runtime a few releases ago and it's possible that the fix at the time changed this behavior, but I'm not immediately able to find the PR for this that I'm thinking of.

    Please do let me know what your investigation yields - I'll bring this thread up on our maintainer call next week and see if anyone on the runtime team has any thoughts on it.

  4. mtaghavi2005 commented on Jun 27, 2026

    @mtaghavi2005
    ContributorAuthor

    I investigated this further based on your suggestion and wanted to share the findings.

    I added some temporary logging to the Dapr runtime gRPC tracing path and confirmed that the runtime can correctly extract the parent context from the incoming traceparent. So this does not appear to be a Dapr runtime parsing issue.

    What I observed is that, for the same Dapr gRPC call, the application sends a valid traceparent (correct trace ID), but its parent span ID corresponds to the Grpc.Net.Client transport activity rather than the logical application operation.

    Since traceparent is propagated by Grpc.Net.Client, I looked into the grpc-dotnet implementation to understand why the propagated parent corresponds to the transport activity rather than the application activity. During that investigation, I found what appears to be a related issue:

    grpc/grpc-dotnet#2516

    That discussion points out that Grpc.Net.Client creates a transport Activity when Activity.Current != null, even if there is no ActivitySource listener.

    To validate this, I tested a locally patched version of grpc-dotnet that disables creating the transport Activity solely because Activity.Current != null. With that change, without any modifications to the Dapr .NET SDK and without propagating grpc-trace-bin, the trace hierarchy became correct.

    At this point, it appears more likely that the underlying issue originates in grpc-dotnet.

    However, if the Dapr project prefers not to rely on the current grpc-dotnet behavior, the proposed PR still provides an alternative by propagating Dapr's documented gRPC trace context (grpc-trace-bin), preserving the logical application context for the runtime.

  5. WhitWaldo commented on Jun 28, 2026

    @WhitWaldo
    Contributor

    Excellent research - I appreciate you taking the time to work through it. Let me ask you this - if Grpc.Net.Client is patched to fix the issue and I've accepted the PR you proposed to patch the Dapr .NET SDK (e.g. both patches are applied), will it result in the activity being improperly set or have no effect?

    If the latter, I think that makes the case for merging your PR as-is. But if the former, I think you or I should simply submit a PR in their repo to fix the issue upstream and update to the new version when available.

  6. mtaghavi2005 commented on Jun 28, 2026

    @mtaghavi2005
    ContributorAuthor

    Thanks, that's a good question.

    My expectation is that if Grpc.Net.Client is fixed upstream and this SDK PR is also applied, the SDK change should have no negative effect. The SDK change doesn't create or replace any Activity; it only propagates additional context derived from the current logical activity. So with both fixes applied, both traceparent and grpc-trace-bin would represent the same logical parent, and the runtime would simply continue preferring grpc-trace-bin.

    That said, after tracing this through the runtime and Grpc.Net.Client, I actually agree with your suggestion that the proper fix should be in grpc-dotnet rather than relying on a Dapr-specific workaround.

    I'll put together a PR in the grpc-dotnet repository based on these findings and see what the maintainers think. If the issue can be resolved upstream, I think that would be the ideal outcome. In that case, we could simply remove the grpc-trace-bin propagation from the Dapr .NET SDK later, and everything should continue to work as it does today, but with the correct parent-child relationship.

    Update:

    PR in the grpc-dotnet repository created:
    grpc/grpc-dotnet#2744

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions