Open telemetry context propagation conflict

Hi,

I’m having an issue with trace propagation when starting a new span. I’ve added to my worker during the initialization a creation of a span and scoped the entire main method code (which includes worker initialization and workflow and activities registration) under it:
try (Scope scope = mainSpan.makeCurrent()) {
… worker init code…
}
The problem is that this breaks the propagation from the caller to the worker once the workflow is executed (the remote span gets lost and the workflow/activity executed inside the worker are under the mainSpan I’ve created).

I’m getting this
External Service Span (lost)
Worker Main Span (separate trace)
└── Workflow Span (wrong parent)
└── Activity Span (wrong parent)

instead of this (when removing the makeCurrent):
External Service Span
└── Workflow Span (propagated)
└── Activity Span (propagated)

Is there a workaround to having the main span but still having the correct propagation during WF execution ?

Thanks,
Luke

looking in SpanFactory it seems that if there is an active span (created by my MakeCurrent) it prefers it over the remote one:
Span activeSpan = tracer.activeSpan();
if (activeSpan != null) {
// we prefer an actual opentracing active span if it exists
parent = activeSpan.context();
} else {
// next we try to use the parent span context from parameters
parent = parentSpanContext;
}

Is this by design ? Why should it be preferred over the one propagated by the caller ?

Hi,

are both the caller are the worker using the same TextMapPropagator?

Hi,

I’m using the Java agent for both and have the following settings:
-otel.propagators=tracecontext,baggage,jaeger,b3

I see,
Right now, I can’t think of anything else but I might be wrong.

are you able to provide an standalone reproduction?

Antonio

Hi Antonio,

the following standalone reproduction is based on Temporal’s Java example (HelloActivity). I’ve simply added some OTEL related parts and run it via gradle task (with auto instrumentation).

package io.temporal.samples.hello;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.*;
import io.opentelemetry.context.Scope;
import io.opentelemetry.opentracingshim.OpenTracingShim;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.client.WorkflowOptions;
import io.temporal.opentracing.OpenTracingClientInterceptor;
import io.temporal.opentracing.OpenTracingOptions;
import io.temporal.opentracing.OpenTracingSpanContextCodec;
import io.temporal.opentracing.OpenTracingWorkerInterceptor;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
import java.time.Duration;

/** Sample Temporal Workflow Definition that executes a single Activity. */
public class OTELHelloActivityExample {

// Define the task queue name
static final String TASK_QUEUE = “HelloActivityTaskQueue”;

// Define our workflow unique id
static final String WORKFLOW_ID = “HelloActivityWorkflow”;

@WorkflowInterface
public interface GreetingWorkflow {
/**
 * This is the method that is executed when the Workflow Execution is started. The Workflow
 * Execution completes when this method finishes execution.
 */
@WorkflowMethod
String getGreeting(String name);

}

// Define the workflow implementation which implements our getGreeting workflow method.
public static class GreetingWorkflowImpl implements GreetingWorkflow {

private final OTELHelloActivityWorker.GreetingActivities activities =
    Workflow.newActivityStub(
        OTELHelloActivityWorker.GreetingActivities.class,
        ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());

@Override
public String getGreeting(String name) {
  // This is a blocking call that returns only after the activity has completed.
  return activities.composeGreeting("Hello", name);
}

}


/**

With our Workflow and Activities defined, we can now start execution. The main method starts

the worker and then the workflow.*/
public static void main(String args) 
{
Tracer tracer = GlobalOpenTelemetry.getTracer("my-temporal-sample");

// Create a manual span for worker setup, make it current in the main thread.
// This span is for tracing the worker's own lifecycle.
Span workerSetupSpan =
    tracer.spanBuilder("worker-main-method").setSpanKind(SpanKind.INTERNAL).startSpan();

// Explicitly scope operations to this span without making it globally current
try (Scope setupScope = workerSetupSpan.makeCurrent()) {
  workerSetupSpan.setAttribute("component", "temporal-worker-example");

  // Get a Workflow service stub.
  WorkflowServiceStubsOptions options =
      WorkflowServiceStubsOptions.newBuilder().setTarget("<Temporal-endpoint>").build();
  WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(options);

  OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
  OpenTracingOptions openTracingOptions =
      OpenTracingOptions.newBuilder()
          .setSpanContextCodec(OpenTracingSpanContextCodec.TEXT_MAP_CODEC)
          .setTracer(OpenTracingShim.createTracerShim(openTelemetry))
          .build();
  /*
   * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
   */
  WorkflowClientOptions clientOptions =
      WorkflowClientOptions.newBuilder()
          .setInterceptors(new OpenTracingClientInterceptor(openTracingOptions))
          .build();
  WorkflowClient client = WorkflowClient.newInstance(service, clientOptions);

  /*
   * Define the workflow factory. It is used to create workflow workers for a specific task queue.
   */
  WorkerFactoryOptions factoryOptions =
      WorkerFactoryOptions.newBuilder()
          .setWorkerInterceptors(new OpenTracingWorkerInterceptor(openTracingOptions))
          .build();
  WorkerFactory factory = WorkerFactory.newInstance(client, factoryOptions);

  /*
   * Define the workflow worker. Workflow workers listen to a defined task queue and process
   * workflows and activities.
   */
  Worker worker = factory.newWorker(TASK_QUEUE);

  /*
   * Register our workflow implementation with the worker.
   * Workflow implementations must be known to the worker at runtime in
   * order to dispatch workflow tasks.
   */
  worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);

  /*
   * Start all the workers registered for a specific task queue.
   * The started workers then start polling for workflows and activities.
   */
  factory.start();
  workerSetupSpan.addEvent("Worker factory started successfully.");

  // Create the workflow client stub. It is used to start our workflow execution.
  OTELHelloActivityExample.GreetingWorkflow workflow =
      client.newWorkflowStub(
          OTELHelloActivityExample.GreetingWorkflow.class,
          WorkflowOptions.newBuilder()
              .setWorkflowId(WORKFLOW_ID)
              .setTaskQueue(TASK_QUEUE)
              .build());

 
  String greeting = workflow.getGreeting("World");

  // Display workflow execution results
  System.out.println(greeting);
  System.exit(0);

} catch (Exception e) {
  workerSetupSpan.recordException(e);
  workerSetupSpan.setStatus(StatusCode.ERROR, "Failed to start WF worker");
  throw new RuntimeException(e);
} finally {
  workerSetupSpan.setStatus(StatusCode.OK, "Worker shutdown");
  workerSetupSpan.end();
}
}
}

Activity Worker:

package io.temporal.samples.hello;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.*;
import io.opentelemetry.context.Scope;
import io.opentelemetry.opentracingshim.OpenTracingShim;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.opentracing.OpenTracingClientInterceptor;
import io.temporal.opentracing.OpenTracingOptions;
import io.temporal.opentracing.OpenTracingSpanContextCodec;
import io.temporal.opentracing.OpenTracingWorkerInterceptor;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;

/** Sample Temporal Workflow Definition that executes a single Activity. */
public class OTELHelloActivityWorker {

// Define the task queue name
static final String TASK_QUEUE = “HelloActivityTaskQueue”;

// Define our workflow unique id
static final String WORKFLOW_ID = “HelloActivityWorkflow”;

@ActivityInterface
public interface GreetingActivities {
// Define your activity method which can be called during workflow execution
@ActivityMethod(name = "greet")
String composeGreeting(String greeting, String name);

}

/** Simple activity implementation, that concatenates two strings. */
public static class GreetingActivitiesImpl implements GreetingActivities {
@Override
public String composeGreeting(String greeting, String name) 
{
return greeting + " " + name + “!”;
}
}

/**

With our Workflow and Activities defined, we can now start execution. The main method starts

the worker and then the workflow.*/
public static void main(String args) 
{
Tracer tracer = GlobalOpenTelemetry.getTracer(“my-temporal-sample”);
// Create a span for the main method
Span mainSpan =
    tracer
        .spanBuilder("main-method")
        .setParent(io.opentelemetry.context.Context.current()) // Link to propagated context
        .setSpanKind(SpanKind.INTERNAL)
        .startSpan();

try (Scope scope = mainSpan.makeCurrent()) {
  mainSpan.setAllAttributes(
      Attributes.of(
          AttributeKey.stringKey("component"), "temporal-worker",
          AttributeKey.stringKey("task.queue"), TASK_QUEUE,
          AttributeKey.stringKey("workflow.id"), WORKFLOW_ID));

  // Get a Workflow service stub.
  WorkflowServiceStubsOptions options =
      WorkflowServiceStubsOptions.newBuilder().setTarget("<Temporal-endpoint>").build();
  WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(options);

  OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
  OpenTracingOptions openTracingOptions =
      OpenTracingOptions.newBuilder()
          .setSpanContextCodec(OpenTracingSpanContextCodec.TEXT_MAP_CODEC)
          .setTracer(OpenTracingShim.createTracerShim(openTelemetry))
          .build();
  /*
   * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
   */
  WorkflowClientOptions clientOptions =
      WorkflowClientOptions.newBuilder()
          .setInterceptors(new OpenTracingClientInterceptor(openTracingOptions))
          .build();
  WorkflowClient client = WorkflowClient.newInstance(service, clientOptions);

  /*
   * Define the workflow factory. It is used to create workflow workers for a specific task queue.
   */
  WorkerFactoryOptions factoryOptions =
      WorkerFactoryOptions.newBuilder()
          .setWorkerInterceptors(new OpenTracingWorkerInterceptor(openTracingOptions))
          .build();
  WorkerFactory factory = WorkerFactory.newInstance(client, factoryOptions);

 
  Worker worker = factory.newWorker(TASK_QUEUE);

  worker.registerActivitiesImplementations(new GreetingActivitiesImpl());

   factory.start();

  // Block main thread to keep worker running
  System.out.println("Activity Worker started. Waiting for workflow tasks...");
  try {
    Thread.currentThread().join();
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
  }
} catch (Exception e) {
  mainSpan.recordException(e);
  mainSpan.setStatus(StatusCode.ERROR, "Failed to start Activity worker");
  throw e;
} finally {
  mainSpan.setStatus(StatusCode.OK, "Activity worker started successfully");
  mainSpan.end();
}
}
}

Gradle Tasks:

task executeOTELExample(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = ‘io.temporal.samples.hello.OTELHelloActivityExample’
jvmArgs = [
‘-javaagent:F://Code/Temporal-samples-java-main-2025/libs/opentelemetry-javaagent.jar’,
‘-Dotel.service.name=my-temporal-sample-opentelemetry’,
‘-Dotel.exporter.otlp.protocol=grpc’,
‘-Dotel.exporter.otlp.endpoint=http://’,
‘-Dotel.propagators=tracecontext,baggage,jaeger,b3’
]
}

task executeOTELActivityWorker(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = ‘io.temporal.samples.hello.OTELHelloActivityWorker’
jvmArgs = [
‘-javaagent:F://Code/Temporal-samples-java-main-2025/libs/opentelemetry-javaagent.jar’,
‘-Dotel.service.name=my-temporal-sample-opentelemetry’,
‘-Dotel.exporter.otlp.protocol=grpc’,
‘-Dotel.exporter.otlp.endpoint=http://’,
‘-Dotel.propagators=tracecontext,baggage,jaeger,b3’
]
}

When calling the makeCurrent the flow is separated into two traces since as Temporal code states it prefers the current trace over the parent (propagated one).

When I’m removing it I’m getting the the entire workflow under one trace:

Is this by design ? Isn’t preferring the propagated context better to get the end to end view ?