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 ?