Skip to content

Commit a9479ba

Browse files
committed
Automatic baggage propagation from gRPC metadata
Implements automatic propagation of OpenTelemetry baggage between gRPC services via metadata headers. Client side: GrpcHeaderClientInterceptor propagates baggage values as gRPC metadata headers in outbound calls based on management.tracing.baggage.remote-fields configuration. Server side: GrpcHeaderServerInterceptor extracts metadata headers into baggage for downstream propagation and adds them as span tags based on management.tracing.baggage.tag-fields configuration. Both interceptors properly manage BaggageInScope lifecycle with guaranteed cleanup on call completion or cancellation. Signed-off-by: Oleksandr Shevchenko <shevchenko.olexandr96@gmail.com>
1 parent d2266d7 commit a9479ba

12 files changed

Lines changed: 901 additions & 0 deletions

File tree

spring-grpc-client-spring-boot-autoconfigure/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@
135135
<artifactId>micrometer-core</artifactId>
136136
<optional>true</optional>
137137
</dependency>
138+
<dependency>
139+
<groupId>io.micrometer</groupId>
140+
<artifactId>micrometer-tracing</artifactId>
141+
<optional>true</optional>
142+
</dependency>
138143
<dependency>
139144
<groupId>io.netty</groupId>
140145
<artifactId>netty-transport-native-epoll</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.grpc.client.autoconfigure;
18+
19+
import java.util.List;
20+
21+
import org.springframework.boot.autoconfigure.AutoConfiguration;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.boot.context.properties.bind.Bindable;
26+
import org.springframework.boot.context.properties.bind.Binder;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.core.env.Environment;
29+
import org.springframework.grpc.client.GlobalClientInterceptor;
30+
31+
import io.micrometer.tracing.Tracer;
32+
33+
/**
34+
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
35+
* Auto-configuration} for gRPC client-side baggage propagation to metadata headers.
36+
* <p>
37+
* This configuration automatically propagates OpenTelemetry baggage values (based on
38+
* {@code management.tracing.baggage.remote-fields}) as gRPC metadata headers in outbound
39+
* calls to downstream services.
40+
*
41+
* @author Oleksandr Shevchenko
42+
* @since 1.2.0
43+
*/
44+
@AutoConfiguration(
45+
afterName = { "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration" },
46+
before = GrpcClientObservationAutoConfiguration.class)
47+
@ConditionalOnGrpcClientEnabled
48+
@ConditionalOnClass(Tracer.class)
49+
@ConditionalOnBean(Tracer.class)
50+
@ConditionalOnProperty(name = "management.tracing.baggage.enabled", havingValue = "true", matchIfMissing = false)
51+
public final class GrpcClientHeaderAutoConfiguration {
52+
53+
@Bean
54+
@GlobalClientInterceptor
55+
GrpcHeaderClientInterceptor grpcHeaderClientInterceptor(final Tracer tracer, final Environment environment) {
56+
List<String> remoteFields = Binder.get(environment)
57+
.bind("management.tracing.baggage.remote-fields", Bindable.listOf(String.class))
58+
.orElse(List.of());
59+
return new GrpcHeaderClientInterceptor(tracer, remoteFields);
60+
}
61+
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.grpc.client.autoconfigure;
18+
19+
import java.util.List;
20+
21+
import io.grpc.CallOptions;
22+
import io.grpc.Channel;
23+
import io.grpc.ClientCall;
24+
import io.grpc.ClientInterceptor;
25+
import io.grpc.ForwardingClientCall;
26+
import io.grpc.Metadata;
27+
import io.grpc.MethodDescriptor;
28+
import io.micrometer.tracing.Baggage;
29+
import io.micrometer.tracing.Tracer;
30+
31+
/**
32+
* A gRPC {@link ClientInterceptor} that propagates OpenTelemetry baggage to outbound gRPC
33+
* calls by adding them as metadata headers.
34+
* <p>
35+
* This interceptor ensures that baggage values are automatically forwarded to downstream
36+
* services in gRPC metadata headers.
37+
* <p>
38+
* The baggage fields to propagate are configured via
39+
* {@code management.tracing.baggage.remote-fields} in Spring Boot configuration.
40+
*
41+
* @author Oleksandr Shevchenko
42+
* @since 1.2.0
43+
*/
44+
public class GrpcHeaderClientInterceptor implements ClientInterceptor {
45+
46+
private final Tracer tracer;
47+
48+
private final List<String> remoteFields;
49+
50+
/**
51+
* Creates a new {@code GrpcHeaderClientInterceptor}.
52+
* @param tracer the tracer to use for accessing baggage
53+
* @param remoteFields the list of baggage field names to propagate as gRPC metadata
54+
* headers
55+
*/
56+
public GrpcHeaderClientInterceptor(final Tracer tracer, final List<String> remoteFields) {
57+
this.tracer = tracer;
58+
this.remoteFields = remoteFields;
59+
}
60+
61+
@Override
62+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(final MethodDescriptor<ReqT, RespT> method,
63+
final CallOptions callOptions, final Channel next) {
64+
65+
return new ForwardingClientCall.SimpleForwardingClientCall<>(next.newCall(method, callOptions)) {
66+
@Override
67+
public void start(final Listener<RespT> responseListener, final Metadata headers) {
68+
for (String fieldName : GrpcHeaderClientInterceptor.this.remoteFields) {
69+
Baggage baggage = GrpcHeaderClientInterceptor.this.tracer.getBaggage(fieldName);
70+
if (baggage != null) {
71+
String value = baggage.get();
72+
if (value != null) {
73+
Metadata.Key<String> key = Metadata.Key.of(fieldName, Metadata.ASCII_STRING_MARSHALLER);
74+
headers.put(key, value);
75+
}
76+
}
77+
}
78+
super.start(responseListener, headers);
79+
}
80+
};
81+
}
82+
83+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration
22
org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration
33
org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration
4+
org.springframework.boot.grpc.client.autoconfigure.GrpcClientHeaderAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.grpc.client.autoconfigure;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.boot.autoconfigure.AutoConfigurations;
25+
import org.springframework.boot.test.context.FilteredClassLoader;
26+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
27+
28+
import io.micrometer.tracing.Tracer;
29+
30+
class GrpcClientHeaderAutoConfigurationTests {
31+
32+
private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner()
33+
.withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class));
34+
35+
private ApplicationContextRunner validContextRunner() {
36+
return new ApplicationContextRunner()
37+
.withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class))
38+
.withBean("tracer", Tracer.class, () -> mock(Tracer.class))
39+
.withPropertyValues("management.tracing.baggage.enabled=true",
40+
"management.tracing.baggage.remote-fields=x-request-id,x-user-id");
41+
}
42+
43+
@Test
44+
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
45+
this.validContextRunner()
46+
.withClassLoader(new FilteredClassLoader(io.grpc.stub.AbstractStub.class))
47+
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
48+
}
49+
50+
@Test
51+
void whenTracerNotOnClasspathAutoConfigSkipped() {
52+
this.validContextRunner()
53+
.withClassLoader(new FilteredClassLoader(Tracer.class))
54+
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
55+
}
56+
57+
@Test
58+
void whenTracerNotProvidedThenAutoConfigSkipped() {
59+
this.baseContextRunner.withPropertyValues("management.tracing.baggage.enabled=true")
60+
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
61+
}
62+
63+
@Test
64+
void whenBaggagePropertyDisabledThenAutoConfigIsSkipped() {
65+
this.validContextRunner()
66+
.withPropertyValues("management.tracing.baggage.enabled=false")
67+
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
68+
}
69+
70+
@Test
71+
void whenBaggagePropertyNotSetThenAutoConfigIsSkipped() {
72+
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class))
73+
.withBean("tracer", Tracer.class, () -> mock(Tracer.class))
74+
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
75+
}
76+
77+
@Test
78+
void whenBaggagePropertyEnabledAndClientNotDisabledThenAutoConfigNotSkipped() {
79+
this.validContextRunner()
80+
.run((context) -> assertThat(context).hasSingleBean(GrpcClientHeaderAutoConfiguration.class));
81+
}
82+
83+
@Test
84+
void whenBaggagePropertyEnabledThenInterceptorIsCreated() {
85+
this.validContextRunner()
86+
.run((context) -> assertThat(context).hasSingleBean(GrpcHeaderClientInterceptor.class));
87+
}
88+
89+
}

0 commit comments

Comments
 (0)