This is an automated email from the ASF dual-hosted git repository.
wy471x pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new b534088619 feat: propagate the gateway trace context to the upstream
service (#7616) (#7682)
b534088619 is described below
commit b534088619b70f58a1eab8b58a11bd09959c2c7f
Author: wy471x <[email protected]>
AuthorDate: Sat Oct 17 12:10:33 2026 +0800
feat: propagate the gateway trace context to the upstream service (#7616)
(#7682)
The upstream call of an http proxy now continues the trace of the gateway
span, so that one trace covers the client, the gateway and the upstream service.
- Inject the trace context into the outgoing request of both the spring
WebClient path and the netty path, from a single place in the shared http
client plugin.
- Drop the traceparent and the trace state of the client before injecting:
the headers of the client are copied to the outgoing request, and a request
which carries two trace contexts would make the upstream pick the wrong parent.
The context of the client is otherwise untouched, so a deployment without
tracing keeps forwarding it as it is.
- Define the trace context propagator as an extension point in the plugin
contract module, implemented by the tracing module, so that the http client
plugin does not depend on the tracing implementation and tracing stays optional.
- Register the W3C propagator for the http protocol, which continues the
incoming trace and its sampling decision and forwards the baggage, while the
upstream call becomes a child of the gateway span.
- Never let a propagator which cannot be loaded break forwarding: a tracing
module which is not installed is only logged at debug level, while an
installation which cannot be loaded, for example because one of its
dependencies is missing, is logged at warn level and leaves the trace context
of the client alone.
Part of #7566.
---
shenyu-plugin/shenyu-plugin-api/pom.xml | 7 +
.../plugin/api/tracing/TraceContextCarrier.java | 44 +++++
.../plugin/api/tracing/TraceContextPropagator.java | 53 ++++++
.../httpclient/AbstractHttpClientPlugin.java | 62 +++++++
.../httpclient/HttpHeadersTraceContextCarrier.java | 49 ++++++
.../plugin/httpclient/NettyHttpClientPlugin.java | 4 +
.../NettyHttpHeadersTraceContextCarrier.java | 49 ++++++
.../shenyu/plugin/httpclient/WebClientPlugin.java | 3 +
...ractHttpClientPluginTraceContextLoaderTest.java | 56 ++++++
.../NettyHttpClientPluginTraceContextTest.java | 124 +++++++++++++
.../httpclient/TestTraceContextPropagator.java | 64 +++++++
.../WebClientPluginTraceContextTest.java | 151 ++++++++++++++++
...henyu.plugin.api.tracing.TraceContextPropagator | 18 ++
shenyu-plugin/shenyu-plugin-tracing/pom.xml | 5 +
.../shenyu/plugin/tracing/TracingConstants.java | 5 +
.../tracing/spi/W3cTraceContextPropagator.java | 65 +++++++
...henyu.plugin.api.tracing.TraceContextPropagator | 18 ++
.../tracing/spi/W3cTraceContextPropagatorTest.java | 192 +++++++++++++++++++++
18 files changed, 969 insertions(+)
diff --git a/shenyu-plugin/shenyu-plugin-api/pom.xml
b/shenyu-plugin/shenyu-plugin-api/pom.xml
index 04c6b3f213..bfd3717461 100644
--- a/shenyu-plugin/shenyu-plugin-api/pom.xml
+++ b/shenyu-plugin/shenyu-plugin-api/pom.xml
@@ -31,6 +31,13 @@
<artifactId>shenyu-sync-data-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- the plugin contract exposes the extension point which a protocol
plugin and its
+ propagator use to pass the trace context to each other -->
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-spi</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
diff --git
a/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextCarrier.java
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextCarrier.java
new file mode 100644
index 0000000000..9638dc49f6
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextCarrier.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.api.tracing;
+
+/**
+ * The carrier of an outgoing call, which a {@link TraceContextPropagator}
writes the trace context
+ * into.
+ *
+ * <p>Every protocol provides its own carrier, for example the http client
plugin hands the headers
+ * of the outgoing request over, so that a propagator does not have to know
which protocol it is
+ * called for.</p>
+ */
+public interface TraceContextCarrier {
+
+ /**
+ * Sets a carrier entry, replacing the values which the entry already has.
+ *
+ * @param name the name of the entry
+ * @param value the value of the entry
+ */
+ void set(String name, String value);
+
+ /**
+ * Removes a carrier entry.
+ *
+ * @param name the name of the entry
+ */
+ void remove(String name);
+}
diff --git
a/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextPropagator.java
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextPropagator.java
new file mode 100644
index 0000000000..aacaa5f364
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/tracing/TraceContextPropagator.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.api.tracing;
+
+import org.apache.shenyu.spi.SPI;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Injects the trace context of a request into the outgoing call to the
upstream service, so that the
+ * upstream continues the same trace instead of starting an unrelated one.
+ *
+ * <p>A protocol plugin injects the context of its own protocol: the http
client plugin hands an http
+ * carrier to the propagator which is registered under {@link #HTTP}, a grpc
or dubbo plugin would
+ * hand its own carrier to the propagator of that protocol. The plugin of a
protocol which does not
+ * have a propagator simply injects nothing, and tracing stays optional for
the deployment.</p>
+ */
+@SPI
+public interface TraceContextPropagator {
+
+ /**
+ * The extension name of the propagator which injects into the http
headers.
+ */
+ String HTTP = "http";
+
+ /**
+ * Injects the trace context of the given exchange into the carrier of the
outgoing call.
+ *
+ * <p>The implementation decides whether the exchange has a trace context
at all, and it is
+ * responsible for not leaving a stale trace context of the client in the
carrier. The trace
+ * context itself, the traceparent and the trace state, belongs to the
trace and is therefore
+ * rebuilt from the context of the gateway, while the entries which are
not trace scoped, such as
+ * the W3C baggage, are forwarded as the client sent them.</p>
+ *
+ * @param exchange the current exchange
+ * @param carrier the carrier of the outgoing call
+ */
+ void inject(ServerWebExchange exchange, TraceContextCarrier carrier);
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPlugin.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPlugin.java
index 0e8a084a25..0f73f57d59 100644
---
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPlugin.java
@@ -27,6 +27,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
+import java.util.function.Supplier;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.constant.Constants;
@@ -41,7 +42,10 @@ import org.apache.shenyu.plugin.api.ShenyuPluginChain;
import org.apache.shenyu.plugin.api.context.ShenyuContext;
import org.apache.shenyu.plugin.api.result.ShenyuResultEnum;
import org.apache.shenyu.plugin.api.result.ShenyuResultWrap;
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+import org.apache.shenyu.plugin.api.tracing.TraceContextPropagator;
import org.apache.shenyu.plugin.api.utils.WebFluxResultUtils;
+import org.apache.shenyu.spi.ExtensionLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
@@ -62,6 +66,11 @@ public abstract class AbstractHttpClientPlugin<R> implements
ShenyuPlugin {
protected static final Logger LOG =
LoggerFactory.getLogger(AbstractHttpClientPlugin.class);
+ /**
+ * The propagator of the http protocol, null while tracing is not
available.
+ */
+ private static final TraceContextPropagator TRACE_CONTEXT_PROPAGATOR =
loadTraceContextPropagator();
+
private final long maxInMemorySize;
protected AbstractHttpClientPlugin(final long maxInMemorySize) {
@@ -113,6 +122,59 @@ public abstract class AbstractHttpClientPlugin<R>
implements ShenyuPlugin {
.flatMap((Function<Object, Mono<? extends Void>>) o ->
chain.execute(exchange));
}
+ /**
+ * Injects the trace context of the request into the carrier of the
outgoing call, so that the
+ * upstream service continues the trace of the client.
+ *
+ * @param exchange the current exchange
+ * @param carrier the carrier of the outgoing call
+ */
+ protected void injectTraceContext(final ServerWebExchange exchange, final
TraceContextCarrier carrier) {
+ if (Objects.isNull(TRACE_CONTEXT_PROPAGATOR)) {
+ return;
+ }
+ TRACE_CONTEXT_PROPAGATOR.inject(exchange, carrier);
+ }
+
+ /**
+ * Loads the propagator of the http protocol.
+ *
+ * <p>Tracing is optional for a deployment, so a propagator which cannot
be loaded must never break
+ * the http client plugin. Either the tracing module is not on the
classpath at all, which is the
+ * normal case, or it is there while one of its dependencies is missing:
the second case fails
+ * while the implementation class is initialized, which raises an error
rather than an exception,
+ * and it would otherwise fail the initialization of this plugin and take
every forwarded request
+ * with it.</p>
+ *
+ * @return the trace context propagator, null while tracing is not
available and the trace context
+ * of the client keeps being forwarded as it is
+ */
+ private static TraceContextPropagator loadTraceContextPropagator() {
+ return loadTraceContextPropagator(() ->
ExtensionLoader.getExtensionLoader(TraceContextPropagator.class)
+ .getJoin(TraceContextPropagator.HTTP));
+ }
+
+ /**
+ * Loads the propagator of the http protocol from the given lookup, which
keeps the failure paths
+ * of the call above testable.
+ *
+ * @param lookup the lookup of the propagator
+ * @return the trace context propagator, null while tracing is not
available
+ */
+ static TraceContextPropagator loadTraceContextPropagator(final
Supplier<TraceContextPropagator> lookup) {
+ try {
+ return lookup.get();
+ } catch (IllegalArgumentException ex) {
+ // no propagator is registered for the http protocol, tracing is
not installed
+ LOG.debug("shenyu tracing is not available, the trace context of
the client is forwarded as it is");
+ return null;
+ } catch (RuntimeException | LinkageError ex) {
+ // tracing is installed but could not be loaded, forwarding is not
affected by that
+ LOG.warn("shenyu tracing could not load its trace context
propagator, the trace context of the client is forwarded as it is", ex);
+ return null;
+ }
+ }
+
/**
* Cache the request body as byte[] so it can be replayed during retry.
*
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/HttpHeadersTraceContextCarrier.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/HttpHeadersTraceContextCarrier.java
new file mode 100644
index 0000000000..6b6b703e35
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/HttpHeadersTraceContextCarrier.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+import org.springframework.http.HttpHeaders;
+
+/**
+ * The {@link TraceContextCarrier} of the http client plugin, which writes
into the headers of the
+ * outgoing request.
+ */
+final class HttpHeadersTraceContextCarrier implements TraceContextCarrier {
+
+ private final HttpHeaders headers;
+
+ /**
+ * Instantiates a new http headers trace context carrier.
+ *
+ * @param headers the headers of the outgoing request
+ */
+ HttpHeadersTraceContextCarrier(final HttpHeaders headers) {
+ this.headers = headers;
+ }
+
+ @Override
+ public void set(final String name, final String value) {
+ headers.set(name, value);
+ }
+
+ @Override
+ public void remove(final String name) {
+ headers.remove(name);
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPlugin.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPlugin.java
index 6beabcfa6b..f35c02a2b3 100644
---
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPlugin.java
@@ -89,6 +89,10 @@ public class NettyHttpClientPlugin extends
AbstractHttpClientPlugin<HttpClientRe
if (preserveHost) {
headers.add(HttpHeaders.HOST,
request.getHeaders().getFirst(HttpHeaders.HOST));
}
+ // the headers of the client have been copied by now, so the trace
context of the gateway
+ // wins and the upstream continues the trace of the gateway span.
The headers handed to a
+ // propagator are the ones of the outgoing request, they are
always writable.
+ this.injectTraceContext(exchange, new
NettyHttpHeadersTraceContextCarrier(headers));
}).request(HttpMethod.valueOf(httpMethod)).uri(uri.toASCIIString())
.send((req, nettyOutbound) -> {
// Do not send a request body for GET/HEAD. Otherwise
Reactor Netty may add
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpHeadersTraceContextCarrier.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpHeadersTraceContextCarrier.java
new file mode 100644
index 0000000000..d9d8f23bf0
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/NettyHttpHeadersTraceContextCarrier.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+
+/**
+ * The {@link TraceContextCarrier} of the outgoing request which the netty
http client builds, whose
+ * headers are the headers of netty rather than the ones of spring.
+ */
+final class NettyHttpHeadersTraceContextCarrier implements TraceContextCarrier
{
+
+ private final HttpHeaders headers;
+
+ /**
+ * Instantiates a new netty http headers trace context carrier.
+ *
+ * @param headers the headers of the outgoing request
+ */
+ NettyHttpHeadersTraceContextCarrier(final HttpHeaders headers) {
+ this.headers = headers;
+ }
+
+ @Override
+ public void set(final String name, final String value) {
+ headers.set(name, value);
+ }
+
+ @Override
+ public void remove(final String name) {
+ headers.remove(name);
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/WebClientPlugin.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/WebClientPlugin.java
index 949623b455..7aafa5e22e 100644
---
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/WebClientPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/WebClientPlugin.java
@@ -86,6 +86,9 @@ public class WebClientPlugin extends
AbstractHttpClientPlugin<ResponseEntity<Flu
if (preserveHost) {
headers.add(HttpHeaders.HOST,
request.getHeaders().getFirst(HttpHeaders.HOST));
}
+ // the headers of the client have been copied by now, so
the trace context of the
+ // gateway wins and the upstream continues the trace of
the gateway span
+ this.injectTraceContext(exchange, new
HttpHeadersTraceContextCarrier(headers));
});
WebClient.RequestHeadersSpec<?> requestHeadersSpec = requestBodySpec;
// Do not attach a request body for GET/HEAD to avoid unexpected
transfer encoding.
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPluginTraceContextLoaderTest.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPluginTraceContextLoaderTest.java
new file mode 100644
index 0000000000..ee084e48b8
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/AbstractHttpClientPluginTraceContextLoaderTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The test case for loading the trace context propagator of the http
protocol, which has to fail
+ * softly so that a broken tracing installation cannot stop the http client
plugin.
+ */
+public class AbstractHttpClientPluginTraceContextLoaderTest {
+
+ @Test
+ public void testLoadsTheRegisteredPropagator() {
+ Assertions.assertInstanceOf(TestTraceContextPropagator.class,
+
AbstractHttpClientPlugin.loadTraceContextPropagator(TestTraceContextPropagator::new));
+ }
+
+ @Test
+ public void testNoPropagatorWhileTracingIsNotInstalled() {
+ // no propagator is registered for the http protocol, which is the
normal case while the
+ // tracing module is not on the classpath of the gateway
+
Assertions.assertNull(AbstractHttpClientPlugin.loadTraceContextPropagator(() ->
{
+ throw new IllegalArgumentException("http name is error");
+ }));
+ }
+
+ @Test
+ public void testNoPropagatorWhileTheImplementationCannotBeLoaded() {
+ // the tracing module is on the classpath while one of its
dependencies is missing: the
+ // implementation class fails while it is initialized, which raises an
error rather than an
+ // exception, and it must not reach the plugin
+
Assertions.assertNull(AbstractHttpClientPlugin.loadTraceContextPropagator(() ->
{
+ throw new
NoClassDefFoundError("io/opentelemetry/api/trace/Tracer");
+ }));
+
Assertions.assertNull(AbstractHttpClientPlugin.loadTraceContextPropagator(() ->
{
+ throw new IllegalStateException("load extension resources error");
+ }));
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPluginTraceContextTest.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPluginTraceContextTest.java
new file mode 100644
index 0000000000..3905f205d3
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/NettyHttpClientPluginTraceContextTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.enums.RetryEnum;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
+import
org.apache.shenyu.plugin.httpclient.exception.ShenyuUpstreamStatusException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.netty.DisposableServer;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.http.server.HttpServer;
+import reactor.test.StepVerifier;
+
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * The test case for the trace context which the netty http client plugin
injects into the outgoing
+ * request.
+ */
+public class NettyHttpClientPluginTraceContextTest {
+
+ private static final String CLIENT_TRACEPARENT =
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
+
+ private NettyHttpClientPlugin nettyHttpClientPlugin;
+
+ @BeforeEach
+ public void setUp() {
+ TestTraceContextPropagator.reset();
+ nettyHttpClientPlugin = new NettyHttpClientPlugin(HttpClient.create(),
Constants.BYTES_PER_MB);
+ }
+
+ @Test
+ public void testTheUpstreamReceivesTheInjectedContextOnly() {
+ List<List<String>> received = new CopyOnWriteArrayList<>();
+ DisposableServer server = startServer(received);
+ try {
+ ServerWebExchange exchange = createExchange(server.port());
+ StepVerifier.create(nettyHttpClientPlugin.doRequest(exchange,
"GET", uriOf(server.port()), Flux.empty()))
+ .expectError(ShenyuUpstreamStatusException.class)
+ .verify();
+ } finally {
+ server.disposeNow();
+ }
+ // the upstream receives exactly one traceparent, the injected one,
and the client one was
+ // replaced even though the headers of the request are read only
+ Assertions.assertEquals(1, received.size());
+
Assertions.assertEquals(List.of(TestTraceContextPropagator.INJECTED_TRACEPARENT),
received.get(0));
+ Assertions.assertEquals(1, TestTraceContextPropagator.injections());
+ }
+
+ @Test
+ public void testTheContextIsNotAccumulatedByAResend() {
+ List<List<String>> received = new CopyOnWriteArrayList<>();
+ DisposableServer server = startServer(received);
+ try {
+ // a resend reuses the exchange of the request, the way the retry
strategy resends it
+ ServerWebExchange exchange = createExchange(server.port());
+ for (int attempt = 0; attempt < 2; attempt++) {
+ StepVerifier.create(nettyHttpClientPlugin.doRequest(exchange,
"GET", uriOf(server.port()), Flux.empty()))
+ .expectError(ShenyuUpstreamStatusException.class)
+ .verify();
+ }
+ } finally {
+ server.disposeNow();
+ }
+ // a resend builds its own headers, so every attempt carries exactly
one traceparent and the
+ // context of the previous attempt is not repeated
+ Assertions.assertEquals(2, received.size());
+ received.forEach(headers -> Assertions.assertEquals(
+ List.of(TestTraceContextPropagator.INJECTED_TRACEPARENT),
headers));
+ Assertions.assertEquals(2, TestTraceContextPropagator.injections());
+ }
+
+ private DisposableServer startServer(final List<List<String>> received) {
+ return HttpServer.create()
+ .port(0)
+ .handle((request, response) -> {
+
received.add(request.requestHeaders().getAll("traceparent"));
+ return response.status(500).sendString(Mono.just("server
error"));
+ })
+ .bindNow();
+ }
+
+ private ServerWebExchange createExchange(final int port) {
+ ServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.get("/test")
+ .header("traceparent", CLIENT_TRACEPARENT)
+ .build());
+ exchange.getAttributes().put(Constants.CONTEXT,
mock(ShenyuContext.class));
+ exchange.getAttributes().put(Constants.HTTP_URI, uriOf(port));
+ exchange.getAttributes().put(Constants.RETRY_STRATEGY,
RetryEnum.FAILOVER.getName());
+ return exchange;
+ }
+
+ private URI uriOf(final int port) {
+ return URI.create("http://127.0.0.1:" + port + "/test");
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/TestTraceContextPropagator.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/TestTraceContextPropagator.java
new file mode 100644
index 0000000000..b3544bdb3e
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/TestTraceContextPropagator.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+import org.apache.shenyu.plugin.api.tracing.TraceContextPropagator;
+import org.apache.shenyu.spi.Join;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A propagator which stands in for the tracing implementation, so that the
http client plugin can be
+ * verified without depending on the tracing module.
+ */
+@Join
+public class TestTraceContextPropagator implements TraceContextPropagator {
+
+ /**
+ * The traceparent which the propagator writes into the carrier.
+ */
+ static final String INJECTED_TRACEPARENT =
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
+
+ private static final AtomicInteger INJECTIONS = new AtomicInteger();
+
+ @Override
+ public void inject(final ServerWebExchange exchange, final
TraceContextCarrier carrier) {
+ INJECTIONS.incrementAndGet();
+ // the same contract as the tracing propagator: the context of the
client is replaced
+ carrier.remove("traceparent");
+ carrier.set("traceparent", INJECTED_TRACEPARENT);
+ }
+
+ /**
+ * Forgets the injections of the previous test case.
+ */
+ static void reset() {
+ INJECTIONS.set(0);
+ }
+
+ /**
+ * Acquires the number of injections.
+ *
+ * @return the number of injections
+ */
+ static int injections() {
+ return INJECTIONS.get();
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTraceContextTest.java
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTraceContextTest.java
new file mode 100644
index 0000000000..21814b4312
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTraceContextTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.httpclient;
+
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.enums.RpcTypeEnum;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
+import org.apache.shenyu.plugin.api.result.ShenyuResult;
+import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ClientResponse;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The test case for the trace context which the http client plugin injects
into the outgoing request.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class WebClientPluginTraceContextTest {
+
+ private static final String CLIENT_TRACEPARENT =
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
+
+ @Mock
+ private ExchangeFunction exchangeFunction;
+
+ @Captor
+ private ArgumentCaptor<ClientRequest> captor;
+
+ @BeforeEach
+ public void setUp() {
+ TestTraceContextPropagator.reset();
+ ConfigurableApplicationContext context =
mock(ConfigurableApplicationContext.class);
+ SpringBeanUtils.getInstance().setApplicationContext(context);
+
when(context.getBean(ShenyuResult.class)).thenReturn(mock(ShenyuResult.class));
+ }
+
+ @Test
+ public void testTheUpstreamReceivesTheInjectedContextOnly() {
+ mockOkResponse();
+ ServerWebExchange exchange = createExchange();
+ ShenyuPluginChain chain = mock(ShenyuPluginChain.class);
+ when(chain.execute(any())).thenReturn(Mono.empty());
+ WebClientPlugin plugin = new
WebClientPlugin(WebClient.builder().exchangeFunction(exchangeFunction).build(),
+ Constants.BYTES_PER_MB);
+ StepVerifier.create(plugin.execute(exchange,
chain)).expectSubscription().verifyComplete();
+ HttpHeaders outboundHeaders = captor.getValue().headers();
+ // the upstream has to receive exactly one traceparent, the injected
one: the traceparent of
+ // the client was copied to the outgoing request first and has to be
replaced, not appended.
+ assertEquals(List.of(TestTraceContextPropagator.INJECTED_TRACEPARENT),
+ outboundHeaders.get("traceparent"));
+ // the headers of the client are still forwarded, the injection only
owns its own header
+ assertEquals("keep-me", outboundHeaders.getFirst("x-client-header"));
+ assertEquals(1, TestTraceContextPropagator.injections());
+ }
+
+ @Test
+ public void testInjectsWhileTheClientHasNoTraceContext() {
+ mockOkResponse();
+ ServerWebExchange exchange = createExchange(null);
+ ShenyuPluginChain chain = mock(ShenyuPluginChain.class);
+ when(chain.execute(any())).thenReturn(Mono.empty());
+ WebClientPlugin plugin = new
WebClientPlugin(WebClient.builder().exchangeFunction(exchangeFunction).build(),
+ Constants.BYTES_PER_MB);
+ StepVerifier.create(plugin.execute(exchange,
chain)).expectSubscription().verifyComplete();
+ // a request which does not carry a trace context still starts one for
the upstream call
+ assertEquals(List.of(TestTraceContextPropagator.INJECTED_TRACEPARENT),
+ captor.getValue().headers().get("traceparent"));
+ }
+
+ private ServerWebExchange createExchange() {
+ return createExchange(CLIENT_TRACEPARENT);
+ }
+
+ /**
+ * Creates an exchange, with the trace context of a client when one is
given.
+ *
+ * @param clientTraceparent the traceparent of the client, null for a
request without one
+ * @return the exchange
+ */
+ private ServerWebExchange createExchange(final String clientTraceparent) {
+ MockServerHttpRequest.BaseBuilder<?> request =
MockServerHttpRequest.get("/http/order")
+ .header("x-client-header", "keep-me");
+ if (Objects.nonNull(clientTraceparent)) {
+ request.header("traceparent", clientTraceparent);
+ }
+ ServerWebExchange exchange =
MockServerWebExchange.from(request.build());
+ ShenyuContext shenyuContext = new ShenyuContext();
+ shenyuContext.setRpcType(RpcTypeEnum.HTTP.getName());
+ exchange.getAttributes().put(Constants.CONTEXT, shenyuContext);
+ exchange.getAttributes().put(Constants.HTTP_URI,
URI.create("http://upstream:9195/http/order"));
+ return exchange;
+ }
+
+ private void mockOkResponse() {
+ ClientResponse.Headers headers = mock(ClientResponse.Headers.class);
+ when(headers.asHttpHeaders()).thenReturn(new HttpHeaders());
+ ClientResponse response = mock(ClientResponse.class);
+ when(response.statusCode()).thenReturn(HttpStatus.OK);
+ when(response.headers()).thenReturn(headers);
+ when(response.bodyToFlux(DataBuffer.class)).thenReturn(Flux.empty());
+ when(response.bodyToMono(byte[].class)).thenReturn(Mono.just(new
byte[0]));
+ when(response.releaseBody()).thenReturn(Mono.empty());
+
given(exchangeFunction.exchange(captor.capture())).willReturn(Mono.just(response));
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
new file mode 100644
index 0000000000..40c85142e9
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+http=org.apache.shenyu.plugin.httpclient.TestTraceContextPropagator
diff --git a/shenyu-plugin/shenyu-plugin-tracing/pom.xml
b/shenyu-plugin/shenyu-plugin-tracing/pom.xml
index 85fea74fc2..6b5fd9bbb8 100644
--- a/shenyu-plugin/shenyu-plugin-tracing/pom.xml
+++ b/shenyu-plugin/shenyu-plugin-tracing/pom.xml
@@ -31,6 +31,11 @@
<artifactId>shenyu-plugin-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-spi</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
diff --git
a/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingConstants.java
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingConstants.java
index 22630d1340..5aa248d455 100644
---
a/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingConstants.java
+++
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingConstants.java
@@ -37,6 +37,11 @@ public final class TracingConstants {
*/
public static final String TRACEPARENT = "traceparent";
+ /**
+ * The W3C trace state header.
+ */
+ public static final String TRACESTATE = "tracestate";
+
/**
* The exchange attribute that carries the active tracing context of the
current request.
*/
diff --git
a/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagator.java
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagator.java
new file mode 100644
index 0000000000..036a7e5d46
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagator.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.tracing.spi;
+
+import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import io.opentelemetry.context.propagation.TextMapSetter;
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+import org.apache.shenyu.plugin.api.tracing.TraceContextPropagator;
+import org.apache.shenyu.plugin.tracing.TracingConstants;
+import org.apache.shenyu.plugin.tracing.context.ReactorTracingContext;
+import org.apache.shenyu.spi.Join;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Injects the trace context of the gateway span with the W3C trace context
and the W3C baggage,
+ * delegating the encoding to the OpenTelemetry propagators.
+ *
+ * <p>The injected context is the context of the gateway span, so the upstream
call becomes a child of
+ * the gateway span, in the same trace the client started. When the request
has no tracing context,
+ * for example because tracing is disabled, nothing is injected and the
context of the client keeps
+ * being forwarded as it was.</p>
+ */
+@Join
+public class W3cTraceContextPropagator implements TraceContextPropagator {
+
+ private static final TextMapPropagator PROPAGATOR =
TextMapPropagator.composite(
+ W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance());
+
+ private static final TextMapSetter<TraceContextCarrier> SETTER =
TraceContextCarrier::set;
+
+ @Override
+ public void inject(final ServerWebExchange exchange, final
TraceContextCarrier carrier) {
+ final Context context = ReactorTracingContext.current(exchange);
+ if (!Span.fromContext(context).getSpanContext().isValid()) {
+ return;
+ }
+ // the headers of the client are copied to the outgoing request, so
the context of the client
+ // is dropped before the context of the gateway span is written: the
upstream must receive
+ // exactly one trace context and it has to belong to the gateway span.
The trace state is
+ // dropped as well, because a trace state of the client which does not
belong to the context
+ // we inject would otherwise stay on the outgoing request.
+ carrier.remove(TracingConstants.TRACEPARENT);
+ carrier.remove(TracingConstants.TRACESTATE);
+ PROPAGATOR.inject(context, carrier, SETTER);
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-tracing/src/main/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
b/shenyu-plugin/shenyu-plugin-tracing/src/main/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
new file mode 100644
index 0000000000..fb577a0c0f
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-tracing/src/main/resources/META-INF/shenyu/org.apache.shenyu.plugin.api.tracing.TraceContextPropagator
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+http=org.apache.shenyu.plugin.tracing.spi.W3cTraceContextPropagator
diff --git
a/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagatorTest.java
b/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagatorTest.java
new file mode 100644
index 0000000000..36253d653e
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/spi/W3cTraceContextPropagatorTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.tracing.spi;
+
+import io.opentelemetry.api.baggage.Baggage;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import org.apache.shenyu.plugin.api.tracing.TraceContextCarrier;
+import org.apache.shenyu.plugin.api.tracing.TraceContextPropagator;
+import org.apache.shenyu.plugin.tracing.TracingConstants;
+import org.apache.shenyu.plugin.tracing.context.ReactorTracingContext;
+import org.apache.shenyu.plugin.tracing.extract.TraceContextExtractor;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.util.List;
+
+/**
+ * The Test Case For the W3C propagator which injects the gateway context into
an outgoing call.
+ */
+public class W3cTraceContextPropagatorTest {
+
+ private static final String CLIENT_TRACE_ID =
"4bf92f3577b34da6a3ce929d0e0e4736";
+
+ private static final String CLIENT_SPAN_ID = "00f067aa0ba902b7";
+
+ private static final String STALE_TRACEPARENT = "00-" + CLIENT_TRACE_ID +
"-" + CLIENT_SPAN_ID + "-01";
+
+ private final TraceContextExtractor extractor = new
TraceContextExtractor();
+
+ private final TraceContextPropagator propagator = new
W3cTraceContextPropagator();
+
+ private SdkTracerProvider tracerProvider;
+
+ private Tracer tracer;
+
+ @BeforeEach
+ public void setUp() {
+ this.tracerProvider = SdkTracerProvider.builder().build();
+ this.tracer =
tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ tracerProvider.close();
+ }
+
+ @Test
+ public void testInjectsTheContextOfTheGatewaySpan() {
+ final ServerWebExchange exchange = createExchange();
+ Span gatewaySpan = tracer.spanBuilder("GET /http/order").startSpan();
+ ReactorTracingContext.store(exchange,
Context.root().with(gatewaySpan));
+ HeadersTraceContextCarrier carrier = new HeadersTraceContextCarrier();
+ // the headers of the client are copied to the outgoing request before
the propagator runs
+ carrier.set(TracingConstants.TRACEPARENT, STALE_TRACEPARENT);
+ propagator.inject(exchange, carrier);
+ // the upstream has to receive exactly one traceparent, and it has to
be the gateway span, in
+ // the trace the gateway continues from the client.
+ List<String> injected =
carrier.getHeaders().get(TracingConstants.TRACEPARENT);
+ Assertions.assertEquals(1, injected.size());
+ SpanContext upstreamParent =
Span.fromContext(extractor.extract(carrier.getHeaders())).getSpanContext();
+ Assertions.assertTrue(upstreamParent.isValid());
+ Assertions.assertTrue(upstreamParent.isRemote());
+ Assertions.assertEquals(gatewaySpan.getSpanContext().getSpanId(),
upstreamParent.getSpanId());
+ Assertions.assertEquals(gatewaySpan.getSpanContext().getTraceId(),
upstreamParent.getTraceId());
+ Assertions.assertTrue(upstreamParent.isSampled());
+ gatewaySpan.end();
+ }
+
+ @Test
+ public void testContinuesTheTraceAndTheBaggageOfTheClient() {
+ final ServerWebExchange exchange = createExchange();
+ HttpHeaders clientHeaders = new HttpHeaders();
+ clientHeaders.add(TracingConstants.TRACEPARENT, STALE_TRACEPARENT);
+ clientHeaders.add("tracestate", "vendor=value");
+ clientHeaders.add("baggage", "userId=alice");
+ Context clientContext = extractor.extract(clientHeaders);
+ // the gateway span continues the trace of the client, exactly like
the tracing plugin does
+ Span gatewaySpan = tracer.spanBuilder("GET
/http/order").setParent(clientContext).startSpan();
+ ReactorTracingContext.store(exchange, clientContext.with(gatewaySpan));
+ HeadersTraceContextCarrier carrier = new HeadersTraceContextCarrier();
+ propagator.inject(exchange, carrier);
+ SpanContext upstreamParent =
Span.fromContext(extractor.extract(carrier.getHeaders())).getSpanContext();
+ Assertions.assertEquals(CLIENT_TRACE_ID, upstreamParent.getTraceId());
+ Assertions.assertEquals(gatewaySpan.getSpanContext().getSpanId(),
upstreamParent.getSpanId());
+ Assertions.assertEquals("value",
upstreamParent.getTraceState().get("vendor"));
+ Context upstreamContext = extractor.extract(carrier.getHeaders());
+ Assertions.assertEquals("alice",
Baggage.fromContext(upstreamContext).getEntryValue("userId"));
+ gatewaySpan.end();
+ }
+
+ @Test
+ public void testKeepsTheContextOfTheClientWhileTracingIsNotActive() {
+ final ServerWebExchange exchange = createExchange();
+ HeadersTraceContextCarrier carrier = new HeadersTraceContextCarrier();
+ carrier.set(TracingConstants.TRACEPARENT, STALE_TRACEPARENT);
+ propagator.inject(exchange, carrier);
+ // tracing is not active for this request, so the client context is
forwarded untouched
+ Assertions.assertEquals(List.of(STALE_TRACEPARENT),
carrier.getHeaders().get(TracingConstants.TRACEPARENT));
+ }
+
+ @Test
+ public void testClearsTheTraceStateOfTheClientWhichIsNotContinued() {
+ final ServerWebExchange exchange = createExchange();
+ Span gatewaySpan = tracer.spanBuilder("GET /http/order").startSpan();
+ ReactorTracingContext.store(exchange,
Context.root().with(gatewaySpan));
+ HeadersTraceContextCarrier carrier = new HeadersTraceContextCarrier();
+ // the client sent a trace state which does not belong to the context
the gateway injects
+ carrier.set(TracingConstants.TRACESTATE, "stale=value");
+ propagator.inject(exchange, carrier);
+ List<String> injected =
carrier.getHeaders().get(TracingConstants.TRACEPARENT);
+ Assertions.assertEquals(1, injected.size());
+
Assertions.assertNull(carrier.getHeaders().getFirst(TracingConstants.TRACESTATE));
+ gatewaySpan.end();
+ }
+
+ @Test
+ public void testPropagatesATraceWhichTheClientDidNotSample() {
+ final ServerWebExchange exchange = createExchange();
+ HttpHeaders clientHeaders = new HttpHeaders();
+ clientHeaders.add(TracingConstants.TRACEPARENT, "00-" +
CLIENT_TRACE_ID + "-" + CLIENT_SPAN_ID + "-00");
+ Context clientContext = extractor.extract(clientHeaders);
+ Span gatewaySpan = tracer.spanBuilder("GET
/http/order").setParent(clientContext).startSpan();
+ ReactorTracingContext.store(exchange, clientContext.with(gatewaySpan));
+ HeadersTraceContextCarrier carrier = new HeadersTraceContextCarrier();
+ propagator.inject(exchange, carrier);
+ SpanContext upstreamParent =
Span.fromContext(extractor.extract(carrier.getHeaders())).getSpanContext();
+ // the decision of the client is kept: the upstream continues the
trace and does not record it
+ Assertions.assertEquals(CLIENT_TRACE_ID, upstreamParent.getTraceId());
+ Assertions.assertEquals(gatewaySpan.getSpanContext().getSpanId(),
upstreamParent.getSpanId());
+ Assertions.assertFalse(upstreamParent.isSampled());
+ gatewaySpan.end();
+ }
+
+ @Test
+ public void testPropagatorIsRegisteredForTheHttpProtocol() {
+ TraceContextPropagator registered =
ExtensionLoader.getExtensionLoader(TraceContextPropagator.class)
+ .getJoin(TraceContextPropagator.HTTP);
+ Assertions.assertInstanceOf(W3cTraceContextPropagator.class,
registered);
+ }
+
+ private ServerWebExchange createExchange() {
+ return
MockServerWebExchange.from(MockServerHttpRequest.get("/http/order").build());
+ }
+
+ /**
+ * The carrier of an outgoing http request, which is the headers of the
request in production.
+ */
+ private static final class HeadersTraceContextCarrier implements
TraceContextCarrier {
+
+ private final HttpHeaders headers = new HttpHeaders();
+
+ @Override
+ public void set(final String name, final String value) {
+ headers.set(name, value);
+ }
+
+ @Override
+ public void remove(final String name) {
+ headers.remove(name);
+ }
+
+ private HttpHeaders getHeaders() {
+ return headers;
+ }
+ }
+}