Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions examples/src/main/java/io/dapr/examples/baggage/BaggageClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.examples.baggage;

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.Headers;
import io.dapr.client.domain.HttpExtension;
import io.dapr.utils.TypeRef;
import reactor.util.context.Context;

/**
* Example demonstrating W3C Baggage propagation with the Dapr Java SDK.
*
* <p>Baggage allows propagating key-value pairs across service boundaries alongside
* distributed traces. This is useful for passing contextual information (e.g., user IDs,
* tenant IDs, feature flags) without modifying request payloads.
*
* <p>The Dapr runtime supports baggage propagation as defined by the
* <a href="https://www.w3.org/TR/baggage/">W3C Baggage specification</a>.
*
* <h2>Usage</h2>
* <ol>
* <li>Build and install jars: {@code mvn clean install}</li>
* <li>{@code cd [repo root]/examples}</li>
* <li>Start the target service:
* {@code dapr run --app-id target-service --app-port 3000 -- java -jar target/dapr-java-sdk-examples-exec.jar
* io.dapr.examples.invoke.http.DemoService -p 3000}</li>
* <li>Run the client:
* {@code dapr run -- java -jar target/dapr-java-sdk-examples-exec.jar
* io.dapr.examples.baggage.BaggageClient}</li>
* </ol>
*/
public class BaggageClient {

/**
* The main method to run the baggage example.
*
* @param args command line arguments (unused).
* @throws Exception on any error.
*/
public static void main(String[] args) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {

// Build the W3C Baggage header value.
// Format: key1=value1,key2=value2
// See https://www.w3.org/TR/baggage/#header-content
String baggageValue = "userId=alice,tenantId=acme-corp,featureFlag=new-ui";

System.out.println("Invoking service with baggage: " + baggageValue);

// Propagate baggage via Reactor context.
// The SDK automatically injects the "baggage" header into outgoing gRPC
// and HTTP requests when present in the Reactor context.
byte[] response = client.invokeMethod(
"target-service",
"say",
"hello with baggage",
HttpExtension.POST,
null,
byte[].class)
.contextWrite(Context.of(Headers.BAGGAGE, baggageValue))
.block();

if (response != null) {
System.out.println("Response: " + new String(response));
}

// You can also combine baggage with tracing context.
System.out.println("\nInvoking service with baggage and tracing context...");
response = client.invokeMethod(
"target-service",
"say",
"hello with baggage and tracing",
HttpExtension.POST,
null,
byte[].class)
.contextWrite(Context.of(Headers.BAGGAGE, baggageValue)
.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"))
.block();

if (response != null) {
System.out.println("Response: " + new String(response));
}

System.out.println("Done.");
}
}
}
96 changes: 96 additions & 0 deletions examples/src/main/java/io/dapr/examples/baggage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Baggage Propagation Example

This example demonstrates [W3C Baggage](https://www.w3.org/TR/baggage/) propagation using the Dapr Java SDK.

## Overview

Baggage allows you to propagate key-value pairs across service boundaries alongside distributed traces. This is useful for passing contextual information — such as user IDs, tenant IDs, or feature flags — without modifying request payloads.

The Dapr runtime supports baggage propagation as described in [Dapr PR #8649](https://github.com/dapr/dapr/pull/8649). The Java SDK propagates the `baggage` header via both gRPC metadata and HTTP headers automatically when the value is present in Reactor's context.

## How It Works

The SDK reads the `baggage` key from Reactor's `ContextView` and injects it into:
- **gRPC metadata** via `DaprBaggageInterceptor`
- **HTTP headers** via `DaprHttp` (added to the context-to-header allowlist)

To propagate baggage, add it to the Reactor context using `.contextWrite()`:

```java
import io.dapr.client.Headers;
import reactor.util.context.Context;

client.invokeMethod("target-service", "say", "hello", HttpExtension.POST, null, byte[].class)
.contextWrite(Context.of(Headers.BAGGAGE, "userId=alice,tenantId=acme-corp"))
.block();
```

The baggage value follows the [W3C Baggage header format](https://www.w3.org/TR/baggage/#header-content):
```
key1=value1,key2=value2
```

Each list-member can optionally include properties:
```
key1=value1;property1;property2,key2=value2
```

## Pre-requisites

* [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
* Java JDK 11 (or greater):
* [Microsoft JDK 11](https://docs.microsoft.com/en-us/java/openjdk/download#openjdk-11)
* [Oracle JDK 11](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11)
* [OpenJDK 11](https://jdk.java.net/11/)
* [Apache Maven](https://maven.apache.org/install.html) version 3.x.

## Running the Example

### 1. Build and install jars

```sh
# From the java-sdk root directory
mvn clean install
```

### 2. Start the target service

In one terminal, start the demo service:

```sh
cd examples
dapr run --app-id target-service --app-port 3000 -- \
java -jar target/dapr-java-sdk-examples-exec.jar \
io.dapr.examples.invoke.http.DemoService -p 3000
```

### 3. Run the baggage client

In another terminal:

```sh
cd examples
dapr run -- java -jar target/dapr-java-sdk-examples-exec.jar \
io.dapr.examples.baggage.BaggageClient
```

You should see output like:

```
Invoking service with baggage: userId=alice,tenantId=acme-corp,featureFlag=new-ui
Response: ...
Done.
```

## Combining Baggage with Tracing

You can propagate both baggage and tracing context together by adding multiple entries to the Reactor context:

```java
client.invokeMethod("target-service", "say", "hello", HttpExtension.POST, null, byte[].class)
.contextWrite(Context.of(Headers.BAGGAGE, "userId=alice")
.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"))
.block();
```

Both the `baggage` and `traceparent` headers will be propagated to downstream services via Dapr.
3 changes: 2 additions & 1 deletion sdk/src/main/java/io/dapr/client/DaprHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public class DaprHttp implements AutoCloseable {
/**
* Context entries allowed to be in HTTP Headers.
*/
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS = Set.of("grpc-trace-bin", "traceparent", "tracestate");
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
Set.of("grpc-trace-bin", "traceparent", "tracestate", "baggage");

/**
* Object mapper to parse DaprError with or without details.
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/main/java/io/dapr/client/Headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ public final class Headers {
* Header for Api Logging User-Agent.
*/
public static final String DAPR_USER_AGENT = "User-Agent";

/**
* W3C Baggage header for context propagation.
*/
public static final String BAGGAGE = "baggage";
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import io.dapr.internal.grpc.interceptors.DaprApiTokenInterceptor;
import io.dapr.internal.grpc.interceptors.DaprAppIdInterceptor;
import io.dapr.internal.grpc.interceptors.DaprBaggageInterceptor;
import io.dapr.internal.grpc.interceptors.DaprMetadataReceiverInterceptor;
import io.dapr.internal.grpc.interceptors.DaprTimeoutInterceptor;
import io.dapr.internal.grpc.interceptors.DaprTracingInterceptor;
Expand Down Expand Up @@ -126,6 +127,7 @@ public <T extends AbstractStub<T>> T intercept(
new DaprApiTokenInterceptor(this.daprApiToken),
new DaprTimeoutInterceptor(this.timeoutPolicy),
new DaprTracingInterceptor(context),
new DaprBaggageInterceptor(context),
new DaprMetadataReceiverInterceptor(metadataConsumer));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.internal.grpc.interceptors;

import io.dapr.client.Headers;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import reactor.util.context.ContextView;

/**
* Injects W3C Baggage header into gRPC metadata from Reactor's context.
*/
public class DaprBaggageInterceptor implements ClientInterceptor {

private static final Metadata.Key<String> BAGGAGE_KEY =
Metadata.Key.of(Headers.BAGGAGE, Metadata.ASCII_STRING_MARSHALLER);

private final ContextView context;

/**
* Creates an instance of the baggage interceptor for gRPC.
*
* @param context Reactor's context
*/
public DaprBaggageInterceptor(ContextView context) {
this.context = context;
}

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions,
Channel channel) {
ClientCall<ReqT, RespT> clientCall = channel.newCall(methodDescriptor, callOptions);
return new ForwardingClientCall.SimpleForwardingClientCall<>(clientCall) {
@Override
public void start(final Listener<RespT> responseListener, final Metadata metadata) {
if (context != null && context.hasKey(Headers.BAGGAGE)) {
String baggageValue = context.get(Headers.BAGGAGE).toString();
if (baggageValue != null && !baggageValue.isEmpty()) {
metadata.put(BAGGAGE_KEY, baggageValue);
}
}
super.start(responseListener, metadata);
}
};
}
}
Loading