diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87977e6f..8d2483ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
## Unreleased
+* Add client operation correlation logging: `FunctionInvocationId` is now propagated via gRPC metadata to the host for client operations, enabling correlation with host logs.
## v1.6.2
* Fixing gRPC channel shutdown ([#249](https://github.com/microsoft/durabletask-java/pull/249))
diff --git a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java
index a952db68..a1206572 100644
--- a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java
+++ b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java
@@ -11,6 +11,7 @@
import com.microsoft.durabletask.DurableTaskGrpcClientBuilder;
import com.microsoft.durabletask.OrchestrationMetadata;
import com.microsoft.durabletask.OrchestrationRuntimeStatus;
+import com.microsoft.durabletask.azurefunctions.internal.FunctionInvocationIdInterceptor;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
@@ -29,6 +30,7 @@ public class DurableClientContext {
private String taskHubName;
private String requiredQueryStringParameters;
private DurableTaskClient client;
+ private String functionInvocationId;
/**
* Gets the name of the client binding's task hub.
@@ -39,6 +41,18 @@ public String getTaskHubName() {
return this.taskHubName;
}
+ /**
+ * Sets the function invocation ID for correlation with host-side logs.
+ *
+ * Call this method before calling {@link #getClient()} to enable correlation
+ * between client operations and host-side logs.
+ *
+ * @param invocationId the Azure Functions invocation ID
+ */
+ public void setFunctionInvocationId(String invocationId) {
+ this.functionInvocationId = invocationId;
+ }
+
/**
* Gets the durable task client associated with the current function invocation.
*
@@ -56,7 +70,14 @@ public DurableTaskClient getClient() {
throw new IllegalStateException("The client context RPC base URL was invalid!", ex);
}
- this.client = new DurableTaskGrpcClientBuilder().port(rpcURL.getPort()).build();
+ DurableTaskGrpcClientBuilder builder = new DurableTaskGrpcClientBuilder().port(rpcURL.getPort());
+
+ // Add interceptor for function invocation ID correlation if set
+ if (this.functionInvocationId != null && !this.functionInvocationId.isEmpty()) {
+ builder.addInterceptor(new FunctionInvocationIdInterceptor(this.functionInvocationId));
+ }
+
+ this.client = builder.build();
return this.client;
}
diff --git a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptor.java b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptor.java
new file mode 100644
index 00000000..539f0ee3
--- /dev/null
+++ b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptor.java
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.durabletask.azurefunctions.internal;
+
+import io.grpc.*;
+
+/**
+ * A gRPC client interceptor that adds the Azure Functions invocation ID to outgoing calls
+ * for correlation with host-side logs.
+ */
+public final class FunctionInvocationIdInterceptor implements ClientInterceptor {
+ private static final String INVOCATION_ID_METADATA_KEY_NAME = "x-azure-functions-invocationid";
+ private static final Metadata.Key INVOCATION_ID_KEY =
+ Metadata.Key.of(INVOCATION_ID_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER);
+
+ private final String invocationId;
+
+ /**
+ * Creates a new interceptor that will add the specified invocation ID to all gRPC calls.
+ *
+ * @param invocationId the Azure Functions invocation ID to add to calls
+ */
+ public FunctionInvocationIdInterceptor(String invocationId) {
+ this.invocationId = invocationId;
+ }
+
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor method,
+ CallOptions callOptions,
+ Channel next) {
+
+ return new ForwardingClientCall.SimpleForwardingClientCall(
+ next.newCall(method, callOptions)) {
+ @Override
+ public void start(Listener responseListener, Metadata headers) {
+ if (invocationId != null && !invocationId.isEmpty()) {
+ headers.put(INVOCATION_ID_KEY, invocationId);
+ }
+ super.start(responseListener, headers);
+ }
+ };
+ }
+}
diff --git a/azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptorTests.java b/azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptorTests.java
new file mode 100644
index 00000000..be64e10f
--- /dev/null
+++ b/azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/internal/FunctionInvocationIdInterceptorTests.java
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.azurefunctions.internal;
+
+import io.grpc.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for FunctionInvocationIdInterceptor.
+ */
+public class FunctionInvocationIdInterceptorTests {
+
+ private static final Metadata.Key INVOCATION_ID_KEY =
+ Metadata.Key.of("x-azure-functions-invocationid", Metadata.ASCII_STRING_MARSHALLER);
+
+ @Test
+ public void interceptCall_addsInvocationIdToMetadata() {
+ // Arrange
+ String testInvocationId = "test-invocation-id-123";
+ FunctionInvocationIdInterceptor interceptor = new FunctionInvocationIdInterceptor(testInvocationId);
+
+ Channel mockChannel = mock(Channel.class);
+ ClientCall