diff --git a/client/build.gradle b/client/build.gradle
index bab0b18..e4233e7 100644
--- a/client/build.gradle
+++ b/client/build.gradle
@@ -17,8 +17,18 @@ archivesBaseName = 'durabletask-client'
def grpcVersion = '1.69.0'
def protocVersion = '3.25.5'
def jacksonVersion = '2.15.3'
+
+def otelVersion = '1.51.0'
+def micrometerVersion = '1.5.1'
+
+// When build on local, you need to set this value to your local jdk11 directory.
+// Java11 is used to compile and run all the tests.
+// Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/
+def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home")
+
// Java 11 is now the minimum required version for both compilation and testing
+
dependencies {
// https://github.com/grpc/grpc-java#download
@@ -27,6 +37,9 @@ dependencies {
implementation 'com.google.protobuf:protobuf-java:3.25.5'
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
+ implementation "io.opentelemetry:opentelemetry-api:${otelVersion}"
+ implementation "io.opentelemetry:opentelemetry-context:${otelVersion}"
+
compileOnly "org.apache.tomcat:annotations-api:6.0.53"
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
diff --git a/client/src/main/java/io/dapr/durabletask/DurableTaskClient.java b/client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
index 3136d80..a69c8da 100644
--- a/client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
+++ b/client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
package io.dapr.durabletask;
+import io.opentelemetry.context.Context;
import javax.annotation.Nullable;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
@@ -38,7 +39,7 @@ public void close() {
* @return the randomly-generated instance ID of the scheduled orchestration instance
*/
public String scheduleNewOrchestrationInstance(String orchestratorName) {
- return this.scheduleNewOrchestrationInstance(orchestratorName, null, null);
+ return this.scheduleNewOrchestrationInstance(orchestratorName, null, null, null);
}
/**
@@ -67,6 +68,19 @@ public String scheduleNewOrchestrationInstance(String orchestratorName, Object i
return this.scheduleNewOrchestrationInstance(orchestratorName, options);
}
+ public String scheduleNewOrchestrationInstance(
+ String orchestratorName,
+ Object input, String instanceId, Context context){
+ NewOrchestrationInstanceOptions options = new NewOrchestrationInstanceOptions()
+ .setInput(input)
+ .setInstanceId(instanceId);
+ return this.scheduleNewOrchestrationInstance(orchestratorName, options, context);
+ }
+
+ public abstract String scheduleNewOrchestrationInstance(
+ String orchestratorName,
+ NewOrchestrationInstanceOptions options, Context context);
+
/**
* Schedules a new orchestration instance with a specified set of options for execution.
*
@@ -98,6 +112,24 @@ public void raiseEvent(String instanceId, String eventName) {
this.raiseEvent(instanceId, eventName, null);
}
+ /**
+ * Sends an event notification message to a waiting orchestration instance.
+ *
+ * In order to handle the event, the target orchestration instance must be waiting for an event named
+ * eventName
using the {@link TaskOrchestrationContext#waitForExternalEvent(String)} method.
+ * If the target orchestration instance is not yet waiting for an event named eventName
,
+ * then the event will be saved in the orchestration instance state and dispatched immediately when the
+ * orchestrator calls {@link TaskOrchestrationContext#waitForExternalEvent(String)}. This event saving occurs even
+ * if the orchestrator has canceled its wait operation before the event was received.
+ *
+ * Raised events for a completed or non-existent orchestration instance will be silently discarded.
+ *
+ * @param instanceId the ID of the orchestration instance that will handle the event
+ * @param eventName the case-insensitive name of the event
+ * @param context Otel context for trace propagation.
+ */
+ public abstract void raiseEvent(String instanceId, String eventName, @Nullable Object eventPayload, Context context);
+
/**
* Sends an event notification message with a payload to a waiting orchestration instance.
*
diff --git a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
index 0ec0291..b174805 100644
--- a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
+++ b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
@@ -8,10 +8,14 @@
import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc;
import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc.*;
+
+import io.dapr.durabletask.interceptors.DaprWorkflowClientGrpcInterceptors;
import io.grpc.*;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.opentelemetry.context.Context;
+
import java.io.FileInputStream;
import java.io.InputStream;
@@ -38,6 +42,7 @@ public final class DurableTaskGrpcClient extends DurableTaskClient {
private final DataConverter dataConverter;
private final ManagedChannel managedSidecarChannel;
private final TaskHubSidecarServiceBlockingStub sidecarClient;
+ private final DaprWorkflowClientGrpcInterceptors interceptors;
DurableTaskGrpcClient(DurableTaskGrpcClientBuilder builder) {
this.dataConverter = builder.dataConverter != null ? builder.dataConverter : new JacksonDataConverter();
@@ -112,6 +117,7 @@ public final class DurableTaskGrpcClient extends DurableTaskClient {
}
this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(sidecarGrpcChannel);
+ this.interceptors = builder.interceptors;
}
/**
@@ -133,6 +139,53 @@ public void close() {
}
}
+ /*
+ * @salaboy TODO: refactor to avoid duplicated code
+ */
+ @Override
+ public String scheduleNewOrchestrationInstance(
+ String orchestratorName,
+ NewOrchestrationInstanceOptions options, Context context) {
+ if (orchestratorName == null || orchestratorName.length() == 0) {
+ throw new IllegalArgumentException("A non-empty orchestrator name must be specified.");
+ }
+
+ Helpers.throwIfArgumentNull(options, "options");
+
+ CreateInstanceRequest.Builder builder = CreateInstanceRequest.newBuilder();
+ builder.setName(orchestratorName);
+
+ String instanceId = options.getInstanceId();
+ if (instanceId == null) {
+ instanceId = UUID.randomUUID().toString();
+ }
+ builder.setInstanceId(instanceId);
+
+ String version = options.getVersion();
+ if (version != null) {
+ builder.setVersion(StringValue.of(version));
+ }
+
+ Object input = options.getInput();
+ if (input != null) {
+ String serializedInput = this.dataConverter.serialize(input);
+ builder.setInput(StringValue.of(serializedInput));
+ }
+
+ Instant startTime = options.getStartTime();
+ if (startTime != null) {
+ Timestamp ts = DataConverter.getTimestampFromInstant(startTime);
+ builder.setScheduledStartTimestamp(ts);
+ }
+
+ CreateInstanceRequest request = builder.build();
+
+ CreateInstanceResponse response = interceptors.intercept(this.sidecarClient, context)
+ .startInstance(request);
+ return response.getInstanceId();
+
+ }
+
@Override
public String scheduleNewOrchestrationInstance(
String orchestratorName,
@@ -170,12 +223,25 @@ public String scheduleNewOrchestrationInstance(
}
CreateInstanceRequest request = builder.build();
- CreateInstanceResponse response = this.sidecarClient.startInstance(request);
+
+ CreateInstanceResponse response = interceptors.intercept(this.sidecarClient)
+ .startInstance(request);
return response.getInstanceId();
}
@Override
public void raiseEvent(String instanceId, String eventName, Object eventPayload) {
+ RaiseEventRequest request = raiseEventRequest(instanceId, eventName, eventPayload);
+ this.sidecarClient.raiseEvent(request);
+ }
+
+ @Override
+ public void raiseEvent(String instanceId, String eventName, Object eventPayload, Context context) {
+ RaiseEventRequest request = raiseEventRequest(instanceId, eventName, eventPayload);
+ interceptors.intercept(this.sidecarClient, context).raiseEvent(request);
+ }
+
+ private RaiseEventRequest raiseEventRequest(String instanceId, String eventName, Object eventPayload){
Helpers.throwIfArgumentNull(instanceId, "instanceId");
Helpers.throwIfArgumentNull(eventName, "eventName");
@@ -186,11 +252,13 @@ public void raiseEvent(String instanceId, String eventName, Object eventPayload)
String serializedPayload = this.dataConverter.serialize(eventPayload);
builder.setInput(StringValue.of(serializedPayload));
}
-
- RaiseEventRequest request = builder.build();
- this.sidecarClient.raiseEvent(request);
+ return builder.build();
}
+
+
+
+
@Override
public OrchestrationMetadata getInstanceMetadata(String instanceId, boolean getInputsAndOutputs) {
GetInstanceRequest request = GetInstanceRequest.newBuilder()
@@ -295,7 +363,7 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer
@Override
public void createTaskHub(boolean recreateIfExists) {
- this.sidecarClient.createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build());
+ interceptors.intercept(this.sidecarClient).createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build());
}
@Override
diff --git a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java
index bac1784..b2cca5f 100644
--- a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java
+++ b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java
@@ -2,7 +2,12 @@
// Licensed under the MIT License.
package io.dapr.durabletask;
+import io.dapr.durabletask.interceptors.DaprWorkflowClientGrpcInterceptors;
import io.grpc.Channel;
+import io.grpc.ClientInterceptor;
+
+import java.util.Arrays;
+import java.util.List;
/**
* Builder class for constructing new {@link DurableTaskClient} objects that communicate with a sidecar process
@@ -16,6 +21,7 @@ public final class DurableTaskGrpcClientBuilder {
String tlsCertPath;
String tlsKeyPath;
boolean insecure;
+ DaprWorkflowClientGrpcInterceptors interceptors;
/**
* Sets the {@link DataConverter} to use for converting serializable data payloads.
@@ -106,6 +112,11 @@ public DurableTaskGrpcClientBuilder insecure(boolean insecure) {
return this;
}
+ public DurableTaskGrpcClientBuilder interceptor(DaprWorkflowClientGrpcInterceptors interceptors){
+ this.interceptors = interceptors;
+ return this;
+ }
+
/**
* Initializes a new {@link DurableTaskClient} object with the settings specified in the current builder object.
* @return a new {@link DurableTaskClient} object
diff --git a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java
index d9e3bc6..f261b71 100644
--- a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java
+++ b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java
@@ -9,8 +9,14 @@
import io.dapr.durabletask.implementation.protobuf.OrchestratorService.WorkItem.RequestCase;
import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc.*;
+
+import io.dapr.durabletask.interceptors.DaprWorkflowClientGrpcInterceptors;
import io.grpc.*;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+
+import javax.annotation.Nullable;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutorService;
@@ -19,6 +25,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+
/**
* Task hub worker that connects to a sidecar process over gRPC to execute
* orchestrator and activity events.
@@ -42,6 +49,7 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
private final boolean isExecutorServiceManaged;
private volatile boolean isNormalShutdown = false;
private Thread workerThread;
+ private DaprWorkflowClientGrpcInterceptors interceptors;
DurableTaskGrpcWorker(DurableTaskGrpcWorkerBuilder builder) {
this.orchestrationFactories.putAll(builder.orchestrationFactories);
@@ -69,10 +77,11 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
}
this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(sidecarGrpcChannel);
+ this.interceptors = builder.interceptors;
this.dataConverter = builder.dataConverter != null ? builder.dataConverter : new JacksonDataConverter();
this.maximumTimerInterval = builder.maximumTimerInterval != null ? builder.maximumTimerInterval
: DEFAULT_MAXIMUM_TIMER_INTERVAL;
- this.workerPool = builder.executorService != null ? builder.executorService : Executors.newCachedThreadPool();
+ this.workerPool = Context.taskWrapping(builder.executorService != null ? builder.executorService : Executors.newCachedThreadPool());
this.isExecutorServiceManaged = builder.executorService == null;
}
@@ -140,7 +149,7 @@ public void startAndBlock() {
while (true) {
try {
GetWorkItemsRequest getWorkItemsRequest = GetWorkItemsRequest.newBuilder().build();
- Iterator workItemStream = this.sidecarClient.getWorkItems(getWorkItemsRequest);
+ Iterator workItemStream = interceptors.intercept(this.sidecarClient, Context.root()).getWorkItems(getWorkItemsRequest);
while (workItemStream.hasNext()) {
WorkItem workItem = workItemStream.next();
RequestCase requestType = workItem.getRequestCase();
@@ -164,10 +173,13 @@ public void startAndBlock() {
.build();
try {
+ interceptors.intercept(this.sidecarClient, Context.root()).completeOrchestratorTask(response);
+
this.sidecarClient.completeOrchestratorTask(response);
logger.log(Level.FINEST,
"Completed orchestrator request for instance: {0}",
orchestratorRequest.getInstanceId());
+
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {
logger.log(Level.WARNING,
@@ -191,7 +203,9 @@ public void startAndBlock() {
activityRequest.getName(),
activityRequest.getOrchestrationInstance().getInstanceId()));
+
// TODO: Error handling
+
this.workerPool.submit(() -> {
String output = null;
TaskFailureDetails failureDetails = null;
@@ -200,7 +214,8 @@ public void startAndBlock() {
activityRequest.getName(),
activityRequest.getInput().getValue(),
activityRequest.getTaskExecutionId(),
- activityRequest.getTaskId());
+ activityRequest.getTaskId(),
+ activityRequest.getParentTraceId());
} catch (Throwable e) {
failureDetails = TaskFailureDetails.newBuilder()
.setErrorType(e.getClass().getName())
@@ -223,7 +238,11 @@ public void startAndBlock() {
}
try {
- this.sidecarClient.completeActivityTask(responseBuilder.build());
+ System.out.println(activityRequest);
+
+
+ Context activityContext = Context.current().with(ContextKey.named("traceparent"), activityRequest.getParentTraceContext().getTraceParent());
+ interceptors.intercept(this.sidecarClient, activityContext).completeActivityTask(responseBuilder.build());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {
logger.log(Level.WARNING,
diff --git a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java
index 8ef1286..6b1ebb6 100644
--- a/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java
+++ b/client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java
@@ -1,153 +1,161 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package io.dapr.durabletask;
-
-import io.grpc.Channel;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.concurrent.ExecutorService;
-
-
-/**
- * Builder object for constructing customized {@link DurableTaskGrpcWorker} instances.
- */
-public final class DurableTaskGrpcWorkerBuilder {
- final HashMap orchestrationFactories = new HashMap<>();
- final HashMap activityFactories = new HashMap<>();
- int port;
- Channel channel;
- DataConverter dataConverter;
- Duration maximumTimerInterval;
- ExecutorService executorService;
- String appId; // App ID for cross-app routing
-
- /**
- * Adds an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}.
- *
- * @param factory an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder addOrchestration(TaskOrchestrationFactory factory) {
- String key = factory.getName();
- if (key == null || key.length() == 0) {
- throw new IllegalArgumentException("A non-empty task orchestration name is required.");
- }
-
- if (this.orchestrationFactories.containsKey(key)) {
- throw new IllegalArgumentException(
- String.format("A task orchestration factory named %s is already registered.", key));
- }
-
- this.orchestrationFactories.put(key, factory);
- return this;
- }
-
- /**
- * Adds an activity factory to be used by the constructed {@link DurableTaskGrpcWorker}.
- *
- * @param factory an activity factory to be used by the constructed {@link DurableTaskGrpcWorker}
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder addActivity(TaskActivityFactory factory) {
- // TODO: Input validation
- String key = factory.getName();
- if (key == null || key.length() == 0) {
- throw new IllegalArgumentException("A non-empty task activity name is required.");
- }
-
- if (this.activityFactories.containsKey(key)) {
- throw new IllegalArgumentException(
- String.format("A task activity factory named %s is already registered.", key));
- }
-
- this.activityFactories.put(key, factory);
- return this;
- }
-
- /**
- * Sets the gRPC channel to use for communicating with the sidecar process.
- *
- * This builder method allows you to provide your own gRPC channel for communicating with the Durable Task sidecar
- * endpoint. Channels provided using this method won't be closed when the worker is closed.
- * Rather, the caller remains responsible for shutting down the channel after disposing the worker.
- *
- * If not specified, a gRPC channel will be created automatically for each constructed
- * {@link DurableTaskGrpcWorker}.
- *
- * @param channel the gRPC channel to use
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder grpcChannel(Channel channel) {
- this.channel = channel;
- return this;
- }
-
- /**
- * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used.
- *
- * @param port the gRPC endpoint port to connect to
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder port(int port) {
- this.port = port;
- return this;
- }
-
- /**
- * Sets the {@link DataConverter} to use for converting serializable data payloads.
- *
- * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder dataConverter(DataConverter dataConverter) {
- this.dataConverter = dataConverter;
- return this;
- }
-
- /**
- * Sets the maximum timer interval. If not specified, the default maximum timer interval duration will be used.
- * The default maximum timer interval duration is 3 days.
- *
- * @param maximumTimerInterval the maximum timer interval
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder maximumTimerInterval(Duration maximumTimerInterval) {
- this.maximumTimerInterval = maximumTimerInterval;
- return this;
- }
-
- /**
- * Sets the executor service that will be used to execute threads.
- *
- * @param executorService {@link ExecutorService}.
- * @return this builder object.
- */
- public DurableTaskGrpcWorkerBuilder withExecutorService(ExecutorService executorService) {
- this.executorService = executorService;
- return this;
- }
-
- /**
- * Sets the app ID for cross-app workflow routing.
- *
- * This app ID is used to identify this worker in cross-app routing scenarios.
- * It should match the app ID configured in the Dapr sidecar.
- *
- *
- * @param appId the app ID for this worker
- * @return this builder object
- */
- public DurableTaskGrpcWorkerBuilder appId(String appId) {
- this.appId = appId;
- return this;
- }
-
- /**
- * Initializes a new {@link DurableTaskGrpcWorker} object with the settings specified in the current builder object.
- * @return a new {@link DurableTaskGrpcWorker} object
- */
- public DurableTaskGrpcWorker build() {
- return new DurableTaskGrpcWorker(this);
- }
-}
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package io.dapr.durabletask;
+
+import io.dapr.durabletask.interceptors.DaprWorkflowClientGrpcInterceptors;
+import io.grpc.Channel;
+import io.opentelemetry.context.Context;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.concurrent.ExecutorService;
+
+
+/**
+ * Builder object for constructing customized {@link DurableTaskGrpcWorker} instances.
+ */
+public final class DurableTaskGrpcWorkerBuilder {
+ final HashMap orchestrationFactories = new HashMap<>();
+ final HashMap activityFactories = new HashMap<>();
+ int port;
+ Channel channel;
+ DataConverter dataConverter;
+ Duration maximumTimerInterval;
+ ExecutorService executorService;
+ String appId; // App ID for cross-app routing
+ DaprWorkflowClientGrpcInterceptors interceptors;
+
+ /**
+ * Adds an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}.
+ *
+ * @param factory an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder addOrchestration(TaskOrchestrationFactory factory) {
+ String key = factory.getName();
+ if (key == null || key.length() == 0) {
+ throw new IllegalArgumentException("A non-empty task orchestration name is required.");
+ }
+
+ if (this.orchestrationFactories.containsKey(key)) {
+ throw new IllegalArgumentException(
+ String.format("A task orchestration factory named %s is already registered.", key));
+ }
+
+ this.orchestrationFactories.put(key, factory);
+ return this;
+ }
+
+ /**
+ * Adds an activity factory to be used by the constructed {@link DurableTaskGrpcWorker}.
+ *
+ * @param factory an activity factory to be used by the constructed {@link DurableTaskGrpcWorker}
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder addActivity(TaskActivityFactory factory) {
+ // TODO: Input validation
+ String key = factory.getName();
+ if (key == null || key.length() == 0) {
+ throw new IllegalArgumentException("A non-empty task activity name is required.");
+ }
+
+ if (this.activityFactories.containsKey(key)) {
+ throw new IllegalArgumentException(
+ String.format("A task activity factory named %s is already registered.", key));
+ }
+
+ this.activityFactories.put(key, factory);
+ return this;
+ }
+
+ /**
+ * Sets the gRPC channel to use for communicating with the sidecar process.
+ *
+ * This builder method allows you to provide your own gRPC channel for communicating with the Durable Task sidecar
+ * endpoint. Channels provided using this method won't be closed when the worker is closed.
+ * Rather, the caller remains responsible for shutting down the channel after disposing the worker.
+ *
+ * If not specified, a gRPC channel will be created automatically for each constructed
+ * {@link DurableTaskGrpcWorker}.
+ *
+ * @param channel the gRPC channel to use
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder grpcChannel(Channel channel) {
+ this.channel = channel;
+ return this;
+ }
+
+ /**
+ * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used.
+ *
+ * @param port the gRPC endpoint port to connect to
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder port(int port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DataConverter} to use for converting serializable data payloads.
+ *
+ * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder dataConverter(DataConverter dataConverter) {
+ this.dataConverter = dataConverter;
+ return this;
+ }
+
+ /**
+ * Sets the maximum timer interval. If not specified, the default maximum timer interval duration will be used.
+ * The default maximum timer interval duration is 3 days.
+ *
+ * @param maximumTimerInterval the maximum timer interval
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder maximumTimerInterval(Duration maximumTimerInterval) {
+ this.maximumTimerInterval = maximumTimerInterval;
+ return this;
+ }
+
+ /**
+ * Sets the executor service that will be used to execute threads.
+ *
+ * @param executorService {@link ExecutorService}.
+ * @return this builder object.
+ */
+ public DurableTaskGrpcWorkerBuilder withExecutorService(ExecutorService executorService) {
+ this.executorService = executorService;
+ return this;
+ }
+
+ /**
+ * Sets the app ID for cross-app workflow routing.
+ *
+ * This app ID is used to identify this worker in cross-app routing scenarios.
+ * It should match the app ID configured in the Dapr sidecar.
+ *
+ *
+ * @param appId the app ID for this worker
+ * @return this builder object
+ */
+ public DurableTaskGrpcWorkerBuilder appId(String appId) {
+ this.appId = appId;
+ return this;
+ }
+
+ public DurableTaskGrpcWorkerBuilder interceptors(DaprWorkflowClientGrpcInterceptors interceptors){
+ this.interceptors = interceptors;
+ return this;
+ }
+
+ /**
+ * Initializes a new {@link DurableTaskGrpcWorker} object with the settings specified in the current builder object.
+ * @return a new {@link DurableTaskGrpcWorker} object
+ */
+ public DurableTaskGrpcWorker build() {
+ return new DurableTaskGrpcWorker(this);
+ }
+}
diff --git a/client/src/main/java/io/dapr/durabletask/TaskActivityContext.java b/client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
index 316fb52..f741877 100644
--- a/client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
+++ b/client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
@@ -34,4 +34,11 @@ public interface TaskActivityContext {
* @return the task id of the current task activity
*/
int getTaskId();
+
+ /**
+ * Get the task parent trace id for Otel trace propagation.
+ * @return the task parent traceId
+ *
+ */
+ String getParentTraceId();
}
diff --git a/client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java b/client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java
index a751319..541b20f 100644
--- a/client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java
+++ b/client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java
@@ -19,7 +19,7 @@ public TaskActivityExecutor(
this.logger = logger;
}
- public String execute(String taskName, String input, String taskExecutionId, int taskId) throws Throwable {
+ public String execute(String taskName, String input, String taskExecutionId, int taskId, String parentTraceId) throws Throwable {
TaskActivityFactory factory = this.activityFactories.get(taskName);
if (factory == null) {
throw new IllegalStateException(
@@ -32,7 +32,7 @@ public String execute(String taskName, String input, String taskExecutionId, int
String.format("The task factory '%s' returned a null TaskActivity object.", taskName));
}
- TaskActivityContextImpl context = new TaskActivityContextImpl(taskName, input, taskExecutionId, taskId);
+ TaskActivityContextImpl context = new TaskActivityContextImpl(taskName, input, taskExecutionId, taskId, parentTraceId);
// Unhandled exceptions are allowed to escape
Object output = activity.run(context);
@@ -48,14 +48,16 @@ private class TaskActivityContextImpl implements TaskActivityContext {
private final String rawInput;
private final String taskExecutionId;
private final int taskId;
+ private final String parentTraceId;
private final DataConverter dataConverter = TaskActivityExecutor.this.dataConverter;
- public TaskActivityContextImpl(String activityName, String rawInput, String taskExecutionId, int taskId) {
+ public TaskActivityContextImpl(String activityName, String rawInput, String taskExecutionId, int taskId, String parentTraceId) {
this.name = activityName;
this.rawInput = rawInput;
this.taskExecutionId = taskExecutionId;
this.taskId = taskId;
+ this.parentTraceId = parentTraceId;
}
@Override
@@ -81,5 +83,10 @@ public String getTaskExecutionId() {
public int getTaskId() {
return this.taskId;
}
+
+ @Override
+ public String getParentTraceId() {
+ return this.parentTraceId;
+ }
}
}
diff --git a/client/src/main/java/io/dapr/durabletask/interceptors/BigendianEncoding.java b/client/src/main/java/io/dapr/durabletask/interceptors/BigendianEncoding.java
new file mode 100644
index 0000000..ddb3ec5
--- /dev/null
+++ b/client/src/main/java/io/dapr/durabletask/interceptors/BigendianEncoding.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2021 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.durabletask.interceptors;
+
+import java.util.Arrays;
+
+/**
+ * Code originally from https://github.com/census-instrumentation/opencensus-java/blob/
+ * 446e9bde9b1f6c0317e3f310644997e5d6d5eab2/api/src/main/java/io/opencensus/trace/BigendianEncoding.java
+ */
+final class BigendianEncoding {
+
+ static final int LONG_BYTES = Long.SIZE / Byte.SIZE;
+
+ static final int BYTE_BASE16 = 2;
+
+ static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES;
+
+ private static final String ALPHABET = "0123456789abcdef";
+
+ private static final int ASCII_CHARACTERS = 128;
+
+ private static final char[] ENCODING = buildEncodingArray();
+
+ private static final byte[] DECODING = buildDecodingArray();
+
+ private static char[] buildEncodingArray() {
+ char[] encoding = new char[512];
+ for (int i = 0; i < 256; ++i) {
+ encoding[i] = ALPHABET.charAt(i >>> 4);
+ encoding[i | 0x100] = ALPHABET.charAt(i & 0xF);
+ }
+ return encoding;
+ }
+
+ private static byte[] buildDecodingArray() {
+ byte[] decoding = new byte[ASCII_CHARACTERS];
+ Arrays.fill(decoding, (byte) -1);
+ for (int i = 0; i < ALPHABET.length(); i++) {
+ char c = ALPHABET.charAt(i);
+ decoding[c] = (byte) i;
+ }
+ return decoding;
+ }
+
+ /**
+ * Returns the {@code long} value whose big-endian representation is stored in the first 8 bytes
+ * of {@code bytes} starting from the {@code offset}.
+ *
+ * @param bytes the byte array representation of the {@code long}.
+ * @param offset the starting offset in the byte array.
+ * @return the {@code long} value whose big-endian representation is given.
+ * @throws IllegalArgumentException if {@code bytes} has fewer than 8 elements.
+ */
+ static long longFromByteArray(byte[] bytes, int offset) {
+ Utils.checkArgument(bytes.length >= offset + LONG_BYTES, "array too small");
+ return (bytes[offset] & 0xFFL) << 56
+ | (bytes[offset + 1] & 0xFFL) << 48
+ | (bytes[offset + 2] & 0xFFL) << 40
+ | (bytes[offset + 3] & 0xFFL) << 32
+ | (bytes[offset + 4] & 0xFFL) << 24
+ | (bytes[offset + 5] & 0xFFL) << 16
+ | (bytes[offset + 6] & 0xFFL) << 8
+ | (bytes[offset + 7] & 0xFFL);
+ }
+
+ /**
+ * Stores the big-endian representation of {@code value} in the {@code dest} starting from the
+ * {@code destOffset}.
+ *
+ * @param value the value to be converted.
+ * @param dest the destination byte array.
+ * @param destOffset the starting offset in the destination byte array.
+ */
+ static void longToByteArray(long value, byte[] dest, int destOffset) {
+ Utils.checkArgument(dest.length >= destOffset + LONG_BYTES, "array too small");
+ dest[destOffset + 7] = (byte) (value & 0xFFL);
+ dest[destOffset + 6] = (byte) (value >> 8 & 0xFFL);
+ dest[destOffset + 5] = (byte) (value >> 16 & 0xFFL);
+ dest[destOffset + 4] = (byte) (value >> 24 & 0xFFL);
+ dest[destOffset + 3] = (byte) (value >> 32 & 0xFFL);
+ dest[destOffset + 2] = (byte) (value >> 40 & 0xFFL);
+ dest[destOffset + 1] = (byte) (value >> 48 & 0xFFL);
+ dest[destOffset] = (byte) (value >> 56 & 0xFFL);
+ }
+
+ /**
+ * Returns the {@code long} value whose base16 representation is stored in the first 16 chars of
+ * {@code chars} starting from the {@code offset}.
+ *
+ * @param chars the base16 representation of the {@code long}.
+ * @param offset the starting offset in the {@code CharSequence}.
+ */
+ static long longFromBase16String(CharSequence chars, int offset) {
+ Utils.checkArgument(chars.length() >= offset + LONG_BASE16, "chars too small");
+ return (decodeByte(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56
+ | (decodeByte(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48
+ | (decodeByte(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40
+ | (decodeByte(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32
+ | (decodeByte(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24
+ | (decodeByte(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16
+ | (decodeByte(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8
+ | (decodeByte(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL);
+ }
+
+ /**
+ * Appends the base16 encoding of the specified {@code value} to the {@code dest}.
+ *
+ * @param value the value to be converted.
+ * @param dest the destination char array.
+ * @param destOffset the starting offset in the destination char array.
+ */
+ static void longToBase16String(long value, char[] dest, int destOffset) {
+ byteToBase16((byte) (value >> 56 & 0xFFL), dest, destOffset);
+ byteToBase16((byte) (value >> 48 & 0xFFL), dest, destOffset + BYTE_BASE16);
+ byteToBase16((byte) (value >> 40 & 0xFFL), dest, destOffset + 2 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 32 & 0xFFL), dest, destOffset + 3 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 24 & 0xFFL), dest, destOffset + 4 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 16 & 0xFFL), dest, destOffset + 5 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 8 & 0xFFL), dest, destOffset + 6 * BYTE_BASE16);
+ byteToBase16((byte) (value & 0xFFL), dest, destOffset + 7 * BYTE_BASE16);
+ }
+
+ /**
+ * Decodes the specified two character sequence, and returns the resulting {@code byte}.
+ *
+ * @param chars the character sequence to be decoded.
+ * @param offset the starting offset in the {@code CharSequence}.
+ * @return the resulting {@code byte}
+ * @throws IllegalArgumentException if the input is not a valid encoded string according to this
+ * encoding.
+ */
+ static byte byteFromBase16String(CharSequence chars, int offset) {
+ Utils.checkArgument(chars.length() >= offset + 2, "chars too small");
+ return decodeByte(chars.charAt(offset), chars.charAt(offset + 1));
+ }
+
+ private static byte decodeByte(char hi, char lo) {
+ Utils.checkArgument(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "invalid character " + lo);
+ Utils.checkArgument(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "invalid character " + hi);
+ int decoded = DECODING[hi] << 4 | DECODING[lo];
+ return (byte) decoded;
+ }
+
+ private static void byteToBase16(byte value, char[] dest, int destOffset) {
+ int b = value & 0xFF;
+ dest[destOffset] = ENCODING[b];
+ dest[destOffset + 1] = ENCODING[b | 0x100];
+ }
+
+ private BigendianEncoding() {
+ }
+}
diff --git a/client/src/main/java/io/dapr/durabletask/interceptors/BinaryFormatImpl.java b/client/src/main/java/io/dapr/durabletask/interceptors/BinaryFormatImpl.java
new file mode 100644
index 0000000..eea265b
--- /dev/null
+++ b/client/src/main/java/io/dapr/durabletask/interceptors/BinaryFormatImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.durabletask.interceptors;
+
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Code originally from https://github.com/census-instrumentation/opencensus-java/blob/
+ * 446e9bde9b1f6c0317e3f310644997e5d6d5eab2/impl_core/src/main/java/io/opencensus/
+ * implcore/trace/propagation/BinaryFormatImpl.java
+ */
+final class BinaryFormatImpl {
+
+ private static final byte VERSION_ID = 0;
+
+ private static final int VERSION_ID_OFFSET = 0;
+
+ // The version_id/field_id size in bytes.
+ private static final byte ID_SIZE = 1;
+
+ private static final byte TRACE_ID_FIELD_ID = 0;
+
+ // TODO: clarify if offsets are correct here. While the specification suggests you should stop
+ // parsing when you hit an unknown field, it does not suggest that fields must be declared in
+ // ID order. Rather it only groups by data type order, in this case Trace Context
+ // https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding
+ // .md#deserialization-rules
+ private static final int TRACE_ID_FIELD_ID_OFFSET = VERSION_ID_OFFSET + ID_SIZE;
+
+ private static final int TRACE_ID_OFFSET = TRACE_ID_FIELD_ID_OFFSET + ID_SIZE;
+
+ private static final byte SPAN_ID_FIELD_ID = 1;
+
+ private static final int SPAN_ID_FIELD_ID_OFFSET = TRACE_ID_OFFSET + TraceId.SIZE;
+
+ private static final int SPAN_ID_OFFSET = SPAN_ID_FIELD_ID_OFFSET + ID_SIZE;
+
+ private static final byte TRACE_OPTION_FIELD_ID = 2;
+
+ private static final int TRACE_OPTION_FIELD_ID_OFFSET = SPAN_ID_OFFSET + SpanId.SIZE;
+
+ private static final int TRACE_OPTIONS_OFFSET = TRACE_OPTION_FIELD_ID_OFFSET + ID_SIZE;
+
+ /**
+ * Version, Trace and Span IDs are required fields.
+ */
+ private static final int REQUIRED_FORMAT_LENGTH = 3 * ID_SIZE + TraceId.SIZE + SpanId.SIZE;
+
+ private static final int ALL_FORMAT_LENGTH = REQUIRED_FORMAT_LENGTH + ID_SIZE + TraceOptions.SIZE;
+
+ /**
+ * Generates the byte array for a span context.
+ * @param spanContext OpenCensus' span context.
+ * @return byte array for span context.
+ */
+ byte[] toByteArray(SpanContext spanContext) {
+ checkNotNull(spanContext, "spanContext");
+ byte[] bytes = new byte[ALL_FORMAT_LENGTH];
+ bytes[VERSION_ID_OFFSET] = VERSION_ID;
+ bytes[TRACE_ID_FIELD_ID_OFFSET] = TRACE_ID_FIELD_ID;
+ spanContext.getTraceId().copyBytesTo(bytes, TRACE_ID_OFFSET);
+ bytes[SPAN_ID_FIELD_ID_OFFSET] = SPAN_ID_FIELD_ID;
+ spanContext.getSpanId().copyBytesTo(bytes, SPAN_ID_OFFSET);
+ bytes[TRACE_OPTION_FIELD_ID_OFFSET] = TRACE_OPTION_FIELD_ID;
+ spanContext.getTraceOptions().copyBytesTo(bytes, TRACE_OPTIONS_OFFSET);
+ return bytes;
+ }
+
+}
diff --git a/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowClientGrpcInterceptors.java b/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowClientGrpcInterceptors.java
new file mode 100644
index 0000000..4a13f80
--- /dev/null
+++ b/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowClientGrpcInterceptors.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.durabletask.interceptors;
+
+import io.grpc.stub.AbstractStub;
+import io.opentelemetry.context.Context;
+
+/**
+ * Class to be used as part of your service's client stub interceptor.
+ * Usage: myClientStub = DaprWorkflowClientGrpcInterceptors.intercept(myClientStub);
+ */
+public class DaprWorkflowClientGrpcInterceptors {
+
+
+ /**
+ * Instantiates a holder of all gRPC interceptors.
+ */
+ public DaprWorkflowClientGrpcInterceptors() {
+
+ }
+
+
+
+ /**
+ * Adds all Dapr interceptors to a gRPC async stub.
+ * @param client gRPC client
+ * @param async client type
+ * @return async client instance with interceptors
+ */
+ public > T intercept(final T client) {
+ return intercept(client, null);
+ }
+
+
+ /**
+ * Adds all Dapr interceptors to a gRPC async stub.
+ * @param client gRPC client
+ * @param context Reactor context for tracing
+ * @param async client type
+ * @return async client instance with interceptors
+ */
+ public > T intercept(
+ final T client,
+ final Context context) {
+ if (client == null) {
+ throw new IllegalArgumentException("client cannot be null");
+ }
+
+ return client.withInterceptors(
+ new DaprWorkflowTracingInterceptor(context));
+ }
+
+}
diff --git a/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowTracingInterceptor.java b/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowTracingInterceptor.java
new file mode 100644
index 0000000..1be8dae
--- /dev/null
+++ b/client/src/main/java/io/dapr/durabletask/interceptors/DaprWorkflowTracingInterceptor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.durabletask.interceptors;
+
+import io.grpc.*;
+import io.opentelemetry.context.Context;
+
+/**
+ * Injects tracing headers to gRPC metadata.
+ */
+public class DaprWorkflowTracingInterceptor implements ClientInterceptor {
+
+ private final Context context;
+
+ /**
+ * Creates an instance of the injector for gRPC context from Reactor's context.
+ * @param context Reactor's context
+ */
+ public DaprWorkflowTracingInterceptor(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor methodDescriptor,
+ CallOptions callOptions,
+ Channel channel) {
+ ClientCall clientCall = channel.newCall(methodDescriptor, callOptions);
+ return new ForwardingClientCall.SimpleForwardingClientCall(clientCall) {
+ @Override
+ public void start(final Listener responseListener, final Metadata metadata) {
+ if (context != null) {
+ GrpcHelper.populateMetadata(context, metadata);
+ }
+ super.start(responseListener, metadata);
+ }
+ };
+ }
+
+}
diff --git a/client/src/main/java/io/dapr/durabletask/interceptors/GrpcHelper.java b/client/src/main/java/io/dapr/durabletask/interceptors/GrpcHelper.java
new file mode 100644
index 0000000..f1697fa
--- /dev/null
+++ b/client/src/main/java/io/dapr/durabletask/interceptors/GrpcHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.durabletask.interceptors;
+
+import io.grpc.Metadata;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.context.propagation.TextMapSetter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Helper to extract tracing information for gRPC calls.
+ */
+public final class GrpcHelper {
+
+ private static final Logger LOGGER = Logger.getLogger(GrpcHelper.class.getName());
+
+ /**
+ * Binary formatter to generate grpc-trace-bin.
+ */
+ private static final BinaryFormatImpl OPENCENSUS_BINARY_FORMAT = new BinaryFormatImpl();
+
+ private static final Metadata.Key GRPC_TRACE_BIN_KEY =
+ Metadata.Key.of("grpc-trace-bin", Metadata.BINARY_BYTE_MARSHALLER);
+
+ private static final Metadata.Key TRACEPARENT_KEY =
+ Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER);
+
+ private static final Metadata.Key TRACESTATE_KEY =
+ Metadata.Key.of("tracestate", Metadata.ASCII_STRING_MARSHALLER);
+
+ private GrpcHelper() {
+ }
+
+ /**
+ * Populates GRPC client's metadata with tracing headers.
+ *
+ * @param context Reactor's context.
+ * @param metadata GRPC client metadata to be populated.
+ */
+ public static void populateMetadata(final Context context, final Metadata metadata) {
+
+ Map map = new HashMap<>();
+ TextMapSetter