Skip to content
Open
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
5 changes: 5 additions & 0 deletions spring-grpc-client-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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 org.springframework.boot.grpc.client.autoconfigure;

import java.util.List;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.grpc.client.GlobalClientInterceptor;

import io.micrometer.tracing.Tracer;

/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} for gRPC client-side baggage propagation to metadata headers.
* <p>
* This configuration automatically propagates OpenTelemetry baggage values (based on
* {@code management.tracing.baggage.remote-fields}) as gRPC metadata headers in outbound
* calls to downstream services.
*
* @author Oleksandr Shevchenko
* @since 1.2.0
*/
@AutoConfiguration(
afterName = { "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration" },
before = GrpcClientObservationAutoConfiguration.class)
@ConditionalOnGrpcClientEnabled
@ConditionalOnClass(Tracer.class)
@ConditionalOnBean(Tracer.class)
@ConditionalOnProperty(name = "management.tracing.baggage.enabled", havingValue = "true", matchIfMissing = false)
public final class GrpcClientHeaderAutoConfiguration {

@Bean
@GlobalClientInterceptor
GrpcHeaderClientInterceptor grpcHeaderClientInterceptor(final Tracer tracer, final Environment environment) {
List<String> remoteFields = Binder.get(environment)
.bind("management.tracing.baggage.remote-fields", Bindable.listOf(String.class))
.orElse(List.of());
return new GrpcHeaderClientInterceptor(tracer, remoteFields);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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 org.springframework.boot.grpc.client.autoconfigure;

import java.util.List;

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 io.micrometer.tracing.Baggage;
import io.micrometer.tracing.Tracer;

/**
* A gRPC {@link ClientInterceptor} that propagates OpenTelemetry baggage to outbound gRPC
* calls by adding them as metadata headers.
* <p>
* This interceptor ensures that baggage values are automatically forwarded to downstream
* services in gRPC metadata headers.
* <p>
* The baggage fields to propagate are configured via
* {@code management.tracing.baggage.remote-fields} in Spring Boot configuration.
*
* @author Oleksandr Shevchenko
* @since 1.2.0
*/
public class GrpcHeaderClientInterceptor implements ClientInterceptor {

private final Tracer tracer;

private final List<String> remoteFields;

/**
* Creates a new {@code GrpcHeaderClientInterceptor}.
* @param tracer the tracer to use for accessing baggage
* @param remoteFields the list of baggage field names to propagate as gRPC metadata
* headers
*/
public GrpcHeaderClientInterceptor(final Tracer tracer, final List<String> remoteFields) {
this.tracer = tracer;
this.remoteFields = remoteFields;
}

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(final MethodDescriptor<ReqT, RespT> method,
final CallOptions callOptions, final Channel next) {

return new ForwardingClientCall.SimpleForwardingClientCall<>(next.newCall(method, callOptions)) {
@Override
public void start(final Listener<RespT> responseListener, final Metadata headers) {
for (String fieldName : GrpcHeaderClientInterceptor.this.remoteFields) {
Baggage baggage = GrpcHeaderClientInterceptor.this.tracer.getBaggage(fieldName);
if (baggage != null) {
String value = baggage.get();
if (value != null) {
Metadata.Key<String> key = Metadata.Key.of(fieldName, Metadata.ASCII_STRING_MARSHALLER);
headers.put(key, value);
}
}
}
super.start(responseListener, headers);
}
};
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration
org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration
org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration
org.springframework.boot.grpc.client.autoconfigure.GrpcClientHeaderAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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 org.springframework.boot.grpc.client.autoconfigure;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import io.micrometer.tracing.Tracer;

class GrpcClientHeaderAutoConfigurationTests {

private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class));

private ApplicationContextRunner validContextRunner() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class))
.withBean("tracer", Tracer.class, () -> mock(Tracer.class))
.withPropertyValues("management.tracing.baggage.enabled=true",
"management.tracing.baggage.remote-fields=x-request-id,x-user-id");
}

@Test
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.validContextRunner()
.withClassLoader(new FilteredClassLoader(io.grpc.stub.AbstractStub.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenTracerNotOnClasspathAutoConfigSkipped() {
this.validContextRunner()
.withClassLoader(new FilteredClassLoader(Tracer.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenTracerNotProvidedThenAutoConfigSkipped() {
this.baseContextRunner.withPropertyValues("management.tracing.baggage.enabled=true")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenBaggagePropertyDisabledThenAutoConfigIsSkipped() {
this.validContextRunner()
.withPropertyValues("management.tracing.baggage.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenBaggagePropertyNotSetThenAutoConfigIsSkipped() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcClientHeaderAutoConfiguration.class))
.withBean("tracer", Tracer.class, () -> mock(Tracer.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenBaggagePropertyEnabledAndClientNotDisabledThenAutoConfigNotSkipped() {
this.validContextRunner()
.run((context) -> assertThat(context).hasSingleBean(GrpcClientHeaderAutoConfiguration.class));
}

@Test
void whenBaggagePropertyEnabledThenInterceptorIsCreated() {
this.validContextRunner()
.run((context) -> assertThat(context).hasSingleBean(GrpcHeaderClientInterceptor.class));
}

}
Loading
Loading