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 c958888290 feat: export the gateway spans over OTLP and make the 
tracing configurable (#7612) (#7705)
c958888290 is described below

commit c958888290f89402e9dd863eb980fe187483aa03
Author: wy471x <[email protected]>
AuthorDate: Sat Oct 17 17:56:10 2026 +0800

    feat: export the gateway spans over OTLP and make the tracing configurable 
(#7612) (#7705)
    
    * feat: export the gateway spans over OTLP and make the tracing 
configurable (#7612)
    
    A deployment can now decide how the gateway traces: which requests are 
sampled, what the spans carry as resource, where they are exported, and whether 
the gateway creates a span at all.
    
    - Extend shenyu.tracing with the create-span switch, the sampler with its 
ratio, the resource attributes and the OTLP exporter configuration, and 
document the defaults in the bootstrap configuration, where the sample ratio is 
only shown for the sampler it belongs to.
    
    - Build the sampler from the configuration: parent based by default so the 
decision of the client is respected, always on, always off, or a ratio which is 
clamped to the valid range instead of failing the gateway. An unknown sampler 
and a ratio which is not a number are reported instead of changing the sampling 
silently.
    
    - Export the spans over OTLP over http in batches. The grpc transport is 
deliberately not bundled, because grpc netty is already on the classpath of the 
gateway through the grpc plugin. A base endpoint gets the traces path appended, 
and an exporter or a batch processor which cannot be created is logged while 
the spans stay in the process, so forwarding is never affected.
    
    - Log the effective configuration once, without the credentials an endpoint 
may carry in its user information or its query.
    
    - Own the exporter with a bean of its own, because the batch processor does 
not close the exporter it writes to while the exporter keeps a connection pool: 
the context closes the tracer provider first and the exporter afterwards.
    
    - Propagate only when the span creation is disabled, which is how a 
deployment that already instruments the gateway, for example with a java agent, 
avoids duplicate spans; the global OpenTelemetry is never replaced.
    
    - Register the exporter artifacts of the bootstrap distribution in its 
LICENSE, and verify the export against a collector stub in the starter tests.
    
    Fixes #7612
    
    Part of #7566.
    
    * fix: keep the credentials out of the tracing log and prefer the active 
context (#7612)
    
    Address the review of the tracing configuration and the propagate only mode.
    
    - Do not log the failure of an exporter which cannot be created, at any 
level: the message of the failure quotes the configured endpoint, which may 
carry a token in its user information or its query. The type of the failure and 
the redacted endpoint stay, so a broken configuration is still reported.
    
    - Prefer the active context of an instrumentation which runs in the 
process, for example a java agent, over the context of the headers of the 
client. The gateway span, or the request in propagate only mode, then continues 
the trace of the agent instead of skipping its span, while the headers of the 
client are used when no instrumentation is active.
    
    Part of #7566.
---
 .../src/main/resources/application.yml             |  13 +
 .../apache/shenyu/common/config/ShenyuConfig.java  | 173 ++++++++++++-
 .../src/main/release-docs/LICENSE                  |   5 +
 .../shenyu/plugin/tracing/TracingConstants.java    |  20 ++
 .../shenyu/plugin/tracing/TracingPlugin.java       |  49 +++-
 .../shenyu/plugin/tracing/TracingPluginTest.java   |  67 ++++++
 .../pom.xml                                        |   7 +
 .../plugin/tracing/TracingExporterHolder.java      |  61 +++++
 .../plugin/tracing/TracingPluginConfiguration.java | 268 ++++++++++++++++++++-
 .../tracing/TracingPluginConfigurationTest.java    | 190 +++++++++++++++
 10 files changed, 838 insertions(+), 15 deletions(-)

diff --git a/shenyu-bootstrap/src/main/resources/application.yml 
b/shenyu-bootstrap/src/main/resources/application.yml
index d1d399e0a3..7953a96628 100644
--- a/shenyu-bootstrap/src/main/resources/application.yml
+++ b/shenyu-bootstrap/src/main/resources/application.yml
@@ -312,6 +312,19 @@ shenyu:
   tracing:
     enabled: false
     serviceName: shenyu-gateway
+    createSpan: true
+    # parent_based continues the decision of the client and samples a request 
which has none,
+    # always_on, always_off and ratio are the alternatives
+    sampler: parent_based
+    # only used by the ratio sampler
+    # sampleRatio: 0.1
+#    resourceAttributes:
+#      deployment.environment: prod
+#    otlp:
+#      endpoint: http://127.0.0.1:4318/v1/traces
+#      timeout: 10s
+#      headers:
+#        authorization: Bearer example
 #  plugins:
 #    ai:
 #      proxy:
diff --git 
a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ShenyuConfig.java 
b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ShenyuConfig.java
index 9f05efe7fa..b194dcf9bd 100644
--- 
a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ShenyuConfig.java
+++ 
b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ShenyuConfig.java
@@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.shenyu.common.concurrent.MemoryLimitCalculator;
 import org.apache.shenyu.common.constant.Constants;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -2001,6 +2002,16 @@ public class ShenyuConfig {
 
         private String serviceName = "shenyu-gateway";
 
+        private boolean createSpan = true;
+
+        private String sampler = "parent_based";
+
+        private double sampleRatio = 0.1D;
+
+        private Properties resourceAttributes;
+
+        private OtlpConfig otlp = new OtlpConfig();
+
         /**
          * Gets enabled.
          *
@@ -2036,8 +2047,168 @@ public class ShenyuConfig {
         public void setServiceName(final String serviceName) {
             this.serviceName = serviceName;
         }
-    }
 
+        /**
+         * Gets create span.
+         *
+         * @return true while the gateway creates a span of its own
+         */
+        public boolean getCreateSpan() {
+            return createSpan;
+        }
+
+        /**
+         * Sets create span, it is disabled for a deployment which instruments 
the gateway already,
+         * for example with a java agent, so that the request is only 
propagated.
+         *
+         * @param createSpan the create span
+         */
+        public void setCreateSpan(final boolean createSpan) {
+            this.createSpan = createSpan;
+        }
+
+        /**
+         * Gets sampler.
+         *
+         * @return the sampler, one of parent_based, always_on, always_off and 
ratio
+         */
+        public String getSampler() {
+            return sampler;
+        }
+
+        /**
+         * Sets sampler, one of parent_based, always_on, always_off and ratio. 
The parent based
+         * sampler, which is the default, continues the decision of the client 
and samples a request
+         * which does not carry a decision, while the ratio sampler samples 
the given ratio of the
+         * requests which do not carry a decision.
+         *
+         * @param sampler the sampler
+         */
+        public void setSampler(final String sampler) {
+            this.sampler = sampler;
+        }
+
+        /**
+         * Gets sample ratio.
+         *
+         * @return the sample ratio, which is only used by the ratio sampler
+         */
+        public double getSampleRatio() {
+            return sampleRatio;
+        }
+
+        /**
+         * Sets sample ratio, which is only used by the ratio sampler.
+         *
+         * @param sampleRatio the sample ratio
+         */
+        public void setSampleRatio(final double sampleRatio) {
+            this.sampleRatio = sampleRatio;
+        }
+
+        /**
+         * Gets resource attributes.
+         *
+         * @return the resource attributes
+         */
+        public Properties getResourceAttributes() {
+            return resourceAttributes;
+        }
+
+        /**
+         * Sets resource attributes.
+         *
+         * @param resourceAttributes the resource attributes
+         */
+        public void setResourceAttributes(final Properties resourceAttributes) 
{
+            this.resourceAttributes = resourceAttributes;
+        }
+
+        /**
+         * Gets the otlp exporter config.
+         *
+         * @return the otlp config
+         */
+        public OtlpConfig getOtlp() {
+            return otlp;
+        }
+
+        /**
+         * Sets the otlp exporter config.
+         *
+         * @param otlp the otlp config
+         */
+        public void setOtlp(final OtlpConfig otlp) {
+            this.otlp = otlp;
+        }
+
+        /**
+         * The otlp exporter config of the tracing plugin.
+         */
+        public static class OtlpConfig {
+
+            private String endpoint;
+
+            private Duration timeout = Duration.ofSeconds(10);
+
+            private Properties headers;
+
+            /**
+             * Gets endpoint.
+             *
+             * @return the endpoint, empty while the spans are not exported
+             */
+            public String getEndpoint() {
+                return endpoint;
+            }
+
+            /**
+             * Sets endpoint, the traces endpoint of the collector, for example
+             * http://127.0.0.1:4318/v1/traces.
+             *
+             * @param endpoint the endpoint
+             */
+            public void setEndpoint(final String endpoint) {
+                this.endpoint = endpoint;
+            }
+
+            /**
+             * Gets timeout.
+             *
+             * @return the timeout
+             */
+            public Duration getTimeout() {
+                return timeout;
+            }
+
+            /**
+             * Sets timeout.
+             *
+             * @param timeout the timeout
+             */
+            public void setTimeout(final Duration timeout) {
+                this.timeout = timeout;
+            }
+
+            /**
+             * Gets headers.
+             *
+             * @return the headers of the export request
+             */
+            public Properties getHeaders() {
+                return headers;
+            }
+
+            /**
+             * Sets headers of the export request, for example an 
authorization header.
+             *
+             * @param headers the headers
+             */
+            public void setHeaders(final Properties headers) {
+                this.headers = headers;
+            }
+        }
+    }
 
     public static class SpringCloudCacheConfig {
         private Boolean enabled = Boolean.FALSE;
diff --git a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
index 6147bf4fc0..835c0d51f6 100644
--- a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
+++ b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
@@ -430,9 +430,14 @@ The text of each license is the standard Apache 2.0 
license.
     opentracing-util 0.22.0: https://github.com/opentracing/opentracing-java, 
Apache 2.0
     opentelemetry-api 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-api, Apache 
2.0
     opentelemetry-api-incubator 1.37.0-alpha: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-api-incubator,
 Apache 2.0
+    opentelemetry-exporter-common 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-exporter-common,
 Apache 2.0
+    opentelemetry-exporter-otlp 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-exporter-otlp,
 Apache 2.0
+    opentelemetry-exporter-otlp-common 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-exporter-otlp-common,
 Apache 2.0
+    opentelemetry-exporter-sender-okhttp 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-exporter-sender-okhttp,
 Apache 2.0
     opentelemetry-context 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-context, 
Apache 2.0
     opentelemetry-sdk 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk, Apache 
2.0
     opentelemetry-sdk-common 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk-common, 
Apache 2.0
+    opentelemetry-sdk-extension-autoconfigure-spi 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk-extension-autoconfigure-spi,
 Apache 2.0
     opentelemetry-sdk-logs 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk-logs, 
Apache 2.0
     opentelemetry-sdk-metrics 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk-metrics, 
Apache 2.0
     opentelemetry-sdk-trace 1.37.0: 
https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-sdk-trace, 
Apache 2.0
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 5aa248d455..1f95d67673 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
@@ -42,6 +42,26 @@ public final class TracingConstants {
      */
     public static final String TRACESTATE = "tracestate";
 
+    /**
+     * The sampler which continues the decision of the parent and samples the 
request otherwise.
+     */
+    public static final String SAMPLER_PARENT_BASED = "parent_based";
+
+    /**
+     * The sampler which samples every request.
+     */
+    public static final String SAMPLER_ALWAYS_ON = "always_on";
+
+    /**
+     * The sampler which samples no request.
+     */
+    public static final String SAMPLER_ALWAYS_OFF = "always_off";
+
+    /**
+     * The sampler which samples the given ratio of the requests.
+     */
+    public static final String SAMPLER_RATIO = "ratio";
+
     /**
      * 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/TracingPlugin.java
 
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingPlugin.java
index ab96c4380b..cb77c1d515 100644
--- 
a/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-tracing/src/main/java/org/apache/shenyu/plugin/tracing/TracingPlugin.java
@@ -47,6 +47,10 @@ import java.util.concurrent.atomic.AtomicReference;
  * <p>The span is created for every subscription, so the span of an attempt is 
never ended twice.
  * The gateway chain itself is subscribed once per request, the guarantee 
matters for callers which
  * subscribe the returned {@code Mono} more than once.</p>
+ *
+ * <p>A deployment which already instruments the gateway, for example with a 
java agent, can disable
+ * the span creation: the plugin then only propagates the incoming context, so 
that the request is not
+ * traced twice.</p>
  */
 public class TracingPlugin implements ShenyuPlugin {
 
@@ -54,13 +58,15 @@ public class TracingPlugin implements ShenyuPlugin {
 
     private final TraceContextExtractor extractor;
 
+    private final boolean createSpan;
+
     /**
      * Instantiates a new tracing plugin.
      *
      * @param tracer the tracer which creates the gateway spans
      */
     public TracingPlugin(final Tracer tracer) {
-        this(tracer, new TraceContextExtractor());
+        this(tracer, new TraceContextExtractor(), true);
     }
 
     /**
@@ -70,8 +76,22 @@ public class TracingPlugin implements ShenyuPlugin {
      * @param extractor the extractor of the inbound trace context
      */
     public TracingPlugin(final Tracer tracer, final TraceContextExtractor 
extractor) {
+        this(tracer, extractor, true);
+    }
+
+    /**
+     * Instantiates a new tracing plugin.
+     *
+     * @param tracer the tracer which creates the gateway spans
+     * @param extractor the extractor of the inbound trace context
+     * @param createSpan whether the plugin creates a span of its own, false 
for a deployment which
+     *                   instruments the gateway already, for example with a 
java agent, where the
+     *                   request is only propagated
+     */
+    public TracingPlugin(final Tracer tracer, final TraceContextExtractor 
extractor, final boolean createSpan) {
         this.tracer = tracer;
         this.extractor = extractor;
+        this.createSpan = createSpan;
     }
 
     @Override
@@ -102,7 +122,16 @@ public class TracingPlugin implements ShenyuPlugin {
      * @return the chain wrapped by the span lifecycle
      */
     private Mono<Void> withSpan(final ServerWebExchange exchange, final 
Mono<Void> chainResult) {
-        final Context parent = extractor.extract(exchange);
+        final Context parent = parentContext(exchange);
+        if (!createSpan) {
+            // the deployment instruments the gateway already, so the request 
is propagated without
+            // creating a second span for it: the active context of the 
instrumentation is the
+            // current context, which the access log and the outgoing call see 
as well.
+            ReactorTracingContext.store(exchange, parent);
+            exposeTraceId(exchange, Span.fromContext(parent));
+            return chainResult
+                    .contextWrite(context -> 
context.put(TracingConstants.REACTOR_CONTEXT_KEY, parent));
+        }
         final Span span = 
tracer.spanBuilder(SpanAttributesBuilder.spanName(exchange))
                 .setSpanKind(SpanKind.SERVER)
                 .setParent(parent)
@@ -128,6 +157,22 @@ public class TracingPlugin implements ShenyuPlugin {
                 });
     }
 
+    /**
+     * Acquires the context which the request continues.
+     *
+     * <p>A deployment which instruments the gateway itself, for example with 
a java agent, has an
+     * active context of its own for the request: continuing it keeps the 
gateway span of the agent in
+     * the trace instead of skipping it. The context of the headers of the 
client is used while no
+     * instrumentation is active, which is the normal case of the gateway.</p>
+     *
+     * @param exchange the current exchange
+     * @return the context of the request
+     */
+    private Context parentContext(final ServerWebExchange exchange) {
+        final Context active = Context.current();
+        return Span.fromContext(active).getSpanContext().isValid() ? active : 
extractor.extract(exchange);
+    }
+
     /**
      * Exposes the trace id of the span through the exchange attribute which 
the logging plugins read.
      *
diff --git 
a/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/TracingPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/TracingPluginTest.java
index f0a652a456..f731d87a76 100644
--- 
a/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/TracingPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-tracing/src/test/java/org/apache/shenyu/plugin/tracing/TracingPluginTest.java
@@ -21,6 +21,7 @@ import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.api.trace.SpanKind;
 import io.opentelemetry.api.trace.TracerProvider;
 import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
 import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
 import io.opentelemetry.sdk.trace.SdkTracerProvider;
 import io.opentelemetry.sdk.trace.data.SpanData;
@@ -31,6 +32,7 @@ import org.apache.shenyu.common.enums.PluginEnum;
 import org.apache.shenyu.plugin.api.ShenyuPluginChain;
 import org.apache.shenyu.plugin.api.context.ShenyuContext;
 import org.apache.shenyu.plugin.tracing.context.ReactorTracingContext;
+import org.apache.shenyu.plugin.tracing.extract.TraceContextExtractor;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -259,6 +261,71 @@ public class TracingPluginTest {
         Assertions.assertEquals(TracingConstants.DEFAULT_SPAN_NAME, 
spanExporter.getFinishedSpanItems().get(0).getName());
     }
 
+    @Test
+    public void testSpanCreationCanBeDisabled() {
+        TracingPlugin plugin = new 
TracingPlugin(tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME),
+                new TraceContextExtractor(), false);
+        
Mockito.when(chain.execute(ArgumentMatchers.any())).thenReturn(Mono.empty());
+        ServerWebExchange exchange = createExchange("00-" + TRACE_ID + "-" + 
PARENT_SPAN_ID + "-01");
+        StepVerifier.create(plugin.execute(exchange, 
chain)).expectSubscription().verifyComplete();
+        // the deployment instruments the gateway itself, so no span is 
created while the incoming
+        // trace is still propagated to the access log and to the upstream call
+        Assertions.assertTrue(spanExporter.getFinishedSpanItems().isEmpty());
+        Assertions.assertEquals(TRACE_ID, 
exchange.getAttribute(Constants.SHENYU_AGENT_TRACE_ID));
+    }
+
+    @Test
+    public void testSpanCreationDisabledWithoutAnIncomingTrace() {
+        TracingPlugin plugin = new 
TracingPlugin(tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME),
+                new TraceContextExtractor(), false);
+        
Mockito.when(chain.execute(ArgumentMatchers.any())).thenReturn(Mono.empty());
+        ServerWebExchange exchange = createExchange(null);
+        StepVerifier.create(plugin.execute(exchange, 
chain)).expectSubscription().verifyComplete();
+        // a request without an incoming trace has nothing to propagate either
+        Assertions.assertTrue(spanExporter.getFinishedSpanItems().isEmpty());
+        
Assertions.assertNull(exchange.getAttribute(Constants.SHENYU_AGENT_TRACE_ID));
+    }
+
+    @Test
+    public void testPropagatesTheActiveContextOfAnInstrumentation() {
+        TracingPlugin plugin = new 
TracingPlugin(tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME),
+                new TraceContextExtractor(), false);
+        
Mockito.when(chain.execute(ArgumentMatchers.any())).thenReturn(Mono.empty());
+        ServerWebExchange exchange = createExchange("00-" + TRACE_ID + "-" + 
PARENT_SPAN_ID + "-01");
+        Span agentSpan = 
tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME)
+                .spanBuilder("agent-server").startSpan();
+        try (Scope scope = Context.root().with(agentSpan).makeCurrent()) {
+            StepVerifier.create(plugin.execute(exchange, 
chain)).expectSubscription().verifyComplete();
+        }
+        // the context of the instrumentation which runs in this process wins 
over the headers of the
+        // client, so that the access log and the outgoing call carry the 
trace of the agent
+        Assertions.assertEquals(agentSpan.getSpanContext().getTraceId(),
+                exchange.getAttribute(Constants.SHENYU_AGENT_TRACE_ID));
+        Assertions.assertNotEquals(TRACE_ID, 
exchange.getAttribute(Constants.SHENYU_AGENT_TRACE_ID));
+        Assertions.assertTrue(spanExporter.getFinishedSpanItems().isEmpty());
+        agentSpan.end();
+    }
+
+    @Test
+    public void testTheGatewaySpanContinuesTheActiveContext() {
+        
Mockito.when(chain.execute(ArgumentMatchers.any())).thenReturn(Mono.empty());
+        ServerWebExchange exchange = createExchange("00-" + TRACE_ID + "-" + 
PARENT_SPAN_ID + "-01");
+        Span agentSpan = 
tracerProvider.get(TracingConstants.INSTRUMENTATION_NAME)
+                .spanBuilder("agent-server").startSpan();
+        try (Scope scope = Context.root().with(agentSpan).makeCurrent()) {
+            StepVerifier.create(tracingPlugin.execute(exchange, 
chain)).expectSubscription().verifyComplete();
+        }
+        // the gateway span is a child of the span of the instrumentation 
instead of a child of the
+        // remote parent, so the agent span does not drop out of the trace
+        SpanData gatewaySpan = spanExporter.getFinishedSpanItems().stream()
+                .filter(span -> "GET".equals(span.getName()))
+                .findFirst()
+                .orElseThrow();
+        Assertions.assertEquals(agentSpan.getSpanContext().getSpanId(), 
gatewaySpan.getParentSpanId());
+        Assertions.assertEquals(agentSpan.getSpanContext().getTraceId(), 
gatewaySpan.getTraceId());
+        agentSpan.end();
+    }
+
     @Test
     public void testOrderAndName() {
         Assertions.assertEquals(PluginEnum.TRACING.getCode(), 
tracingPlugin.getOrder());
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/pom.xml
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/pom.xml
index c41be4d240..c1d2a3a2cb 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/pom.xml
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/pom.xml
@@ -36,6 +36,13 @@
             <groupId>io.opentelemetry</groupId>
             <artifactId>opentelemetry-sdk</artifactId>
         </dependency>
+        <!-- the spans are exported over OTLP over http, the grpc transport is 
deliberately not
+             bundled because grpc netty is already on the classpath of the 
gateway. The sender of the
+             exporter is the one managed together with the OpenTelemetry 
version of the framework. -->
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-exporter-otlp</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingExporterHolder.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingExporterHolder.java
new file mode 100644
index 0000000000..68c77535af
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingExporterHolder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.springboot.starter.plugin.tracing;
+
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import org.springframework.beans.factory.DisposableBean;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Holds the OTLP exporter of the gateway spans and closes it together with 
the application context.
+ *
+ * <p>The batch processor of the tracer provider does not close the exporter 
it writes to, while the
+ * OTLP exporter keeps a connection pool. The exporter therefore is owned by a 
bean of its own, which
+ * the context closes after it has closed the tracer provider.</p>
+ */
+final class TracingExporterHolder implements DisposableBean {
+
+    private final SpanExporter exporter;
+
+    /**
+     * Instantiates a new tracing exporter holder.
+     *
+     * @param exporter the exporter, null while the spans are not exported
+     */
+    TracingExporterHolder(final SpanExporter exporter) {
+        this.exporter = exporter;
+    }
+
+    /**
+     * Acquires the exporter.
+     *
+     * @return the exporter, empty while the spans are not exported
+     */
+    Optional<SpanExporter> exporter() {
+        return Optional.ofNullable(exporter);
+    }
+
+    @Override
+    public void destroy() {
+        if (Objects.nonNull(exporter)) {
+            exporter.close();
+        }
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfiguration.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfiguration.java
index 58a1b24ae6..ca4ed63d37 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfiguration.java
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/main/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfiguration.java
@@ -18,20 +18,35 @@
 package org.apache.shenyu.springboot.starter.plugin.tracing;
 
 import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
 import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.ResourceBuilder;
 import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
+import io.opentelemetry.sdk.trace.SpanProcessor;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
 import io.opentelemetry.sdk.trace.samplers.Sampler;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shenyu.common.config.ShenyuConfig;
 import org.apache.shenyu.plugin.api.ShenyuPlugin;
 import org.apache.shenyu.plugin.tracing.TracingConstants;
 import org.apache.shenyu.plugin.tracing.TracingPlugin;
+import org.apache.shenyu.plugin.tracing.extract.TraceContextExtractor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.web.util.UriComponentsBuilder;
 
+import java.net.URI;
+import java.util.Locale;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.Properties;
 
 /**
  * The type Tracing plugin configuration.
@@ -41,15 +56,21 @@ import java.util.Optional;
  * agent keeps its own instrumentation.</p>
  *
  * <p>The beans are wired by their explicit names, so that an application 
which brings its own
- * OpenTelemetry beans keeps working. Span export is added by a follow-up 
change, this configuration
- * already produces spans with a usable trace id, which is what the access log 
correlation needs.</p>
+ * OpenTelemetry beans keeps working. Spans are exported over OTLP when an 
endpoint is configured;
+ * the grpc transport is deliberately not bundled, because grpc netty is 
already on the classpath of
+ * the gateway through the grpc plugin. Nothing of the export path may affect 
request forwarding, so
+ * an exporter which cannot be created is logged and the spans simply stay in 
the process.</p>
  */
 @Configuration
 @ConditionalOnProperty(value = {"shenyu.tracing.enabled"}, havingValue = 
"true")
 public class TracingPluginConfiguration {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(TracingPluginConfiguration.class);
+
     private static final String DEFAULT_SERVICE_NAME = "shenyu-gateway";
 
+    private static final String TRACES_PATH = "/v1/traces";
+
     /**
      * The resource of the gateway spans.
      *
@@ -58,24 +79,51 @@ public class TracingPluginConfiguration {
      */
     @Bean
     public Resource tracingResource(final ShenyuConfig shenyuConfig) {
-        return Resource.getDefault()
+        final ResourceBuilder builder = Resource.getDefault()
                 .toBuilder()
-                .put(TracingConstants.SERVICE_NAME, serviceName(shenyuConfig))
-                .build();
+                .put(TracingConstants.SERVICE_NAME, serviceName(shenyuConfig));
+        resourceAttributes(shenyuConfig).ifPresent(attributes -> 
attributes.stringPropertyNames()
+                .forEach(name -> builder.put(name, 
attributes.getProperty(name))));
+        return builder.build();
+    }
+
+    /**
+     * The exporter of the gateway spans, which the context closes after it 
has closed the tracer
+     * provider.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the exporter holder
+     */
+    @Bean
+    public TracingExporterHolder tracingExporterHolder(final ShenyuConfig 
shenyuConfig) {
+        return new 
TracingExporterHolder(createSpanExporter(shenyuConfig).orElse(null));
     }
 
     /**
      * The tracer provider of the gateway spans.
      *
      * @param resource the OpenTelemetry resource
+     * @param exporterHolder the exporter of the gateway spans
+     * @param shenyuConfig the shenyu config
      * @return the sdk tracer provider
      */
     @Bean(destroyMethod = "close")
-    public SdkTracerProvider 
tracingTracerProvider(@Qualifier("tracingResource") final Resource resource) {
-        return SdkTracerProvider.builder()
+    public SdkTracerProvider 
tracingTracerProvider(@Qualifier("tracingResource") final Resource resource,
+                                                   final TracingExporterHolder 
exporterHolder,
+                                                   final ShenyuConfig 
shenyuConfig) {
+        final Sampler sampler = sampler(shenyuConfig);
+        final SdkTracerProviderBuilder builder = SdkTracerProvider.builder()
                 .setResource(resource)
-                .setSampler(Sampler.parentBased(Sampler.alwaysOn()))
-                .build();
+                .setSampler(sampler);
+        final Optional<SpanProcessor> processor = 
exporterHolder.exporter().flatMap(this::spanProcessor);
+        processor.ifPresent(builder::addSpanProcessor);
+        // the processor of the exporter may fail to be created, so the log 
reports the export from
+        // the processor which is really attached instead of claiming that the 
spans are exported
+        LOG.info("shenyu tracing: service name {}, sampler {}, span creation 
{}, spans exported {}",
+                serviceName(shenyuConfig), sampler.getDescription(),
+                createSpan(shenyuConfig) ? "enabled" : "disabled",
+                processor.isPresent() ? endpointOf(shenyuConfig) : "disabled");
+        return builder.build();
     }
 
     /**
@@ -93,17 +141,213 @@ public class TracingPluginConfiguration {
      * Tracing plugin.
      *
      * @param tracer the tracer
+     * @param shenyuConfig the shenyu config
      * @return the shenyu plugin
      */
     @Bean
-    public ShenyuPlugin tracingPlugin(@Qualifier("tracingTracer") final Tracer 
tracer) {
-        return new TracingPlugin(tracer);
+    public ShenyuPlugin tracingPlugin(@Qualifier("tracingTracer") final Tracer 
tracer,
+                                      final ShenyuConfig shenyuConfig) {
+        return new TracingPlugin(tracer, new TraceContextExtractor(), 
createSpan(shenyuConfig));
     }
 
+    /**
+     * Acquires the tracing config.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the tracing config, empty while it is not configured
+     */
+    private Optional<ShenyuConfig.TracingConfig> tracingConfig(final 
ShenyuConfig shenyuConfig) {
+        return Optional.ofNullable(shenyuConfig.getTracing());
+    }
+
+    /**
+     * Acquires the service name of the gateway.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the service name
+     */
     private String serviceName(final ShenyuConfig shenyuConfig) {
-        return Optional.ofNullable(shenyuConfig.getTracing())
+        return tracingConfig(shenyuConfig)
                 .map(ShenyuConfig.TracingConfig::getServiceName)
                 .filter(StringUtils::isNotBlank)
                 .orElse(DEFAULT_SERVICE_NAME);
     }
+
+    /**
+     * Tells whether the gateway creates a span of its own.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return true while the plugin creates a span
+     */
+    private boolean createSpan(final ShenyuConfig shenyuConfig) {
+        return 
tracingConfig(shenyuConfig).map(ShenyuConfig.TracingConfig::getCreateSpan).orElse(Boolean.TRUE);
+    }
+
+    /**
+     * Acquires the resource attributes which are added to the resource of the 
gateway spans.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the configured resource attributes, empty while none is 
configured
+     */
+    private Optional<Properties> resourceAttributes(final ShenyuConfig 
shenyuConfig) {
+        return tracingConfig(shenyuConfig)
+                .map(ShenyuConfig.TracingConfig::getResourceAttributes)
+                .filter(attributes -> !attributes.isEmpty());
+    }
+
+    /**
+     * Builds the sampler which decides whether a request is traced.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the sampler
+     */
+    private Sampler sampler(final ShenyuConfig shenyuConfig) {
+        final ShenyuConfig.TracingConfig config = tracingConfig(shenyuConfig)
+                .orElseGet(ShenyuConfig.TracingConfig::new);
+        final String sampler = Optional.ofNullable(config.getSampler())
+                .map(String::trim)
+                .filter(StringUtils::isNotBlank)
+                .orElse(TracingConstants.SAMPLER_PARENT_BASED)
+                .toLowerCase(Locale.ROOT);
+        switch (sampler) {
+            case TracingConstants.SAMPLER_ALWAYS_ON:
+                return Sampler.alwaysOn();
+            case TracingConstants.SAMPLER_ALWAYS_OFF:
+                return Sampler.alwaysOff();
+            case TracingConstants.SAMPLER_RATIO:
+                return 
Sampler.parentBased(Sampler.traceIdRatioBased(ratio(config.getSampleRatio())));
+            case TracingConstants.SAMPLER_PARENT_BASED:
+                return Sampler.parentBased(Sampler.alwaysOn());
+            default:
+                LOG.warn("shenyu tracing does not know the sampler {}, the 
parent based sampler is used", sampler);
+                return Sampler.parentBased(Sampler.alwaysOn());
+        }
+    }
+
+    /**
+     * Normalizes the sample ratio.
+     *
+     * <p>A ratio which is not a number or which is out of the range would 
silently change the
+     * sampling behaviour, so it is reported and clamped instead of failing 
the gateway.</p>
+     *
+     * @param sampleRatio the configured sample ratio
+     * @return the ratio within the valid range
+     */
+    private double ratio(final double sampleRatio) {
+        if (Double.isNaN(sampleRatio)) {
+            LOG.warn("shenyu tracing could not read the sample ratio, no 
request is sampled");
+            return 0.0D;
+        }
+        final double bounded = Math.max(0.0D, Math.min(1.0D, sampleRatio));
+        if (bounded != sampleRatio) {
+            LOG.warn("shenyu tracing clamps the sample ratio {} to {}", 
sampleRatio, bounded);
+        }
+        return bounded;
+    }
+
+    /**
+     * Builds the batch processor of the exporter.
+     *
+     * @param exporter the exporter of the gateway spans
+     * @return the batch processor, empty while it cannot be created
+     */
+    private Optional<SpanProcessor> spanProcessor(final SpanExporter exporter) 
{
+        try {
+            return Optional.of(BatchSpanProcessor.builder(exporter).build());
+        } catch (RuntimeException | LinkageError ex) {
+            LOG.warn("shenyu tracing could not create the batch processor of 
the exporter, the spans are not exported", ex);
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Creates the OTLP exporter of the gateway spans.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the span exporter, empty while no endpoint is configured or the 
exporter cannot be created
+     */
+    private Optional<SpanExporter> createSpanExporter(final ShenyuConfig 
shenyuConfig) {
+        final Optional<ShenyuConfig.TracingConfig.OtlpConfig> configured = 
tracingConfig(shenyuConfig)
+                .map(ShenyuConfig.TracingConfig::getOtlp)
+                .filter(otlp -> StringUtils.isNotBlank(otlp.getEndpoint()));
+        if (!configured.isPresent()) {
+            return Optional.empty();
+        }
+        final ShenyuConfig.TracingConfig.OtlpConfig otlp = configured.get();
+        try {
+            final String endpoint = endpoint(otlp.getEndpoint());
+            final OtlpHttpSpanExporterBuilder builder = 
OtlpHttpSpanExporter.builder().setEndpoint(endpoint);
+            if (Objects.nonNull(otlp.getTimeout())) {
+                builder.setTimeout(otlp.getTimeout());
+            }
+            if (Objects.nonNull(otlp.getHeaders())) {
+                final Properties headers = otlp.getHeaders();
+                headers.stringPropertyNames().forEach(name -> 
builder.addHeader(name, headers.getProperty(name)));
+            }
+            return Optional.of(builder.build());
+        } catch (RuntimeException | LinkageError ex) {
+            // the message of the failure may quote the endpoint, and an 
endpoint may carry a token in
+            // its user information or its query, so neither the throwable nor 
its message is logged,
+            // at any level: only the redacted endpoint and the type of the 
failure.
+            LOG.warn("shenyu tracing could not create the OTLP exporter of {}, 
the spans are not exported: {}",
+                    redactedEndpoint(otlp.getEndpoint()), 
ex.getClass().getSimpleName());
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Resolves the traces endpoint: a base endpoint without a path gets the 
traces path appended, so
+     * that a collector is not called at its root and the spans do not get 
lost silently.
+     *
+     * @param configuredEndpoint the configured endpoint
+     * @return the endpoint of the traces of the collector
+     */
+    private String endpoint(final String configuredEndpoint) {
+        final String endpoint = configuredEndpoint.trim();
+        final String path = URI.create(endpoint).getPath();
+        if (StringUtils.isNotBlank(path) && !"/".equals(path)) {
+            return endpoint;
+        }
+        // the path is appended instead of concatenated, so that a query or a 
fragment of the
+        // configured endpoint keeps being valid
+        return 
UriComponentsBuilder.fromUriString(endpoint).path(TRACES_PATH).build().toUriString();
+    }
+
+    /**
+     * Acquires the endpoint the spans are exported to, without the 
credentials it may carry.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the endpoint, disabled while the spans are not exported
+     */
+    private String endpointOf(final ShenyuConfig shenyuConfig) {
+        return tracingConfig(shenyuConfig)
+                .map(ShenyuConfig.TracingConfig::getOtlp)
+                .map(ShenyuConfig.TracingConfig.OtlpConfig::getEndpoint)
+                .filter(StringUtils::isNotBlank)
+                .map(this::endpoint)
+                .map(TracingPluginConfiguration::redactedEndpoint)
+                .orElse("disabled");
+    }
+
+    /**
+     * Removes the user information, the query and the fragment from an 
endpoint.
+     *
+     * <p>A collector endpoint commonly carries a token in the query or in the 
user information, and
+     * the log must not expose it.</p>
+     *
+     * @param endpoint the endpoint
+     * @return the endpoint without its credentials
+     */
+    static String redactedEndpoint(final String endpoint) {
+        try {
+            return UriComponentsBuilder.fromUriString(endpoint)
+                    .userInfo(null)
+                    .query(null)
+                    .fragment(null)
+                    .build()
+                    .toUriString();
+        } catch (RuntimeException ex) {
+            return "the configured endpoint";
+        }
+    }
 }
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/test/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfigurationTest.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/test/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfigurationTest.java
index 90cd63ab1b..3377f465f0 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/test/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfigurationTest.java
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-tracing/src/test/java/org/apache/shenyu/springboot/starter/plugin/tracing/TracingPluginConfigurationTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.shenyu.springboot.starter.plugin.tracing;
 
+import io.opentelemetry.api.GlobalOpenTelemetry;
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.api.trace.Tracer;
@@ -24,12 +25,39 @@ import io.opentelemetry.api.trace.TracerProvider;
 import io.opentelemetry.sdk.resources.Resource;
 import io.opentelemetry.sdk.trace.SdkTracerProvider;
 import org.apache.shenyu.common.config.ShenyuConfig;
+import org.apache.shenyu.common.constant.Constants;
 import org.apache.shenyu.plugin.api.ShenyuPlugin;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
 import org.apache.shenyu.plugin.tracing.TracingConstants;
 import org.apache.shenyu.plugin.tracing.TracingPlugin;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import 
org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
 import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+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.Mono;
+
+import com.sun.net.httpserver.HttpServer;
+import org.awaitility.Awaitility;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * The Test Case For TracingPluginConfiguration.
@@ -123,4 +151,166 @@ public class TracingPluginConfigurationTest {
     private String serviceName(final Resource resource) {
         return 
resource.getAttribute(AttributeKey.stringKey(TracingConstants.SERVICE_NAME));
     }
+
+    @Test
+    public void testAlwaysOffSamplerDoesNotRecord() {
+        ShenyuConfig shenyuConfig = new ShenyuConfig();
+        
shenyuConfig.getTracing().setSampler(TracingConstants.SAMPLER_ALWAYS_OFF);
+        runner(shenyuConfig).run(context -> Assertions.assertFalse(
+                
context.getBean(Tracer.class).spanBuilder("test").startSpan().isRecording()));
+    }
+
+    @Test
+    public void testRatioSampler() {
+        ShenyuConfig withoutSamples = new ShenyuConfig();
+        withoutSamples.getTracing().setSampler(TracingConstants.SAMPLER_RATIO);
+        withoutSamples.getTracing().setSampleRatio(0.0D);
+        runner(withoutSamples).run(context -> Assertions.assertFalse(
+                
context.getBean(Tracer.class).spanBuilder("test").startSpan().isRecording()));
+        ShenyuConfig withSamples = new ShenyuConfig();
+        withSamples.getTracing().setSampler(TracingConstants.SAMPLER_RATIO);
+        withSamples.getTracing().setSampleRatio(1.0D);
+        runner(withSamples).run(context -> Assertions.assertTrue(
+                
context.getBean(Tracer.class).spanBuilder("test").startSpan().isRecording()));
+    }
+
+    @Test
+    public void testTheGlobalOpenTelemetryIsNotReplaced() {
+        // a deployment which instruments the gateway already, for example 
with a java agent, keeps
+        // its own global OpenTelemetry: the tracer of the global never 
records our spans
+        
contextRunner.withPropertyValues("shenyu.tracing.enabled=true").run(context ->
+                Assertions.assertFalse(GlobalOpenTelemetry.getTracer("test")
+                        .spanBuilder("test").startSpan().isRecording()));
+    }
+
+    @Test
+    public void testAnExporterWithAnInvalidEndpointDoesNotFailTheStartup() {
+        ShenyuConfig shenyuConfig = new ShenyuConfig();
+        shenyuConfig.getTracing().getOtlp().setEndpoint("not a valid 
endpoint");
+        runner(shenyuConfig).run(context -> {
+            Assertions.assertNull(context.getStartupFailure());
+            Assertions.assertTrue(context.containsBean("tracingPlugin"));
+        });
+    }
+
+    @Test
+    public void testAnUnreachableExporterDoesNotAffectTheSpan() {
+        ShenyuConfig shenyuConfig = new ShenyuConfig();
+        
shenyuConfig.getTracing().getOtlp().setEndpoint("http://127.0.0.1:1/v1/traces";);
+        runner(shenyuConfig).run(context -> {
+            Span span = 
context.getBean(Tracer.class).spanBuilder("test").startSpan();
+            Assertions.assertDoesNotThrow(() -> {
+                span.end();
+            });
+        });
+    }
+
+    @Test
+    public void testResourceAttributesAreRecorded() {
+        ShenyuConfig shenyuConfig = new ShenyuConfig();
+        Properties attributes = new Properties();
+        attributes.setProperty("deployment.environment", "test");
+        shenyuConfig.getTracing().setResourceAttributes(attributes);
+        new ApplicationContextRunner()
+                .withUserConfiguration(TracingPluginConfiguration.class)
+                .withBean(ShenyuConfig.class, () -> shenyuConfig)
+                .withPropertyValues("shenyu.tracing.enabled=true")
+                .run(context -> Assertions.assertEquals("test", 
context.getBean(Resource.class)
+                        
.getAttribute(AttributeKey.stringKey("deployment.environment"))));
+    }
+
+    @Test
+    public void testTheTracingConfigIsBoundFromProperties() {
+        MapConfigurationPropertySource source = new 
MapConfigurationPropertySource(Map.of(
+                "shenyu.tracing.enabled", "true",
+                "shenyu.tracing.create-span", "false",
+                "shenyu.tracing.sampler", "ratio",
+                "shenyu.tracing.sample-ratio", "0.25",
+                "shenyu.tracing.otlp.endpoint", 
"http://collector:4318/v1/traces";,
+                "shenyu.tracing.otlp.headers.authorization", "Bearer test"));
+        Binder binder = new Binder(source);
+        ShenyuConfig.TracingConfig config = binder.bind("shenyu.tracing", 
Bindable.of(ShenyuConfig.TracingConfig.class))
+                .orElseGet(ShenyuConfig.TracingConfig::new);
+        Assertions.assertFalse(config.getCreateSpan());
+        Assertions.assertEquals("ratio", config.getSampler());
+        Assertions.assertEquals(0.25D, config.getSampleRatio());
+        Assertions.assertEquals("http://collector:4318/v1/traces";, 
config.getOtlp().getEndpoint());
+        Assertions.assertEquals("Bearer test", 
config.getOtlp().getHeaders().getProperty("authorization"));
+    }
+
+    @Test
+    public void testSpansAreExportedToTheCollector() throws IOException {
+        AtomicReference<String> exported = new AtomicReference<>();
+        HttpServer server = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext("/v1/traces", exchange -> {
+            try (InputStream body = exchange.getRequestBody()) {
+                body.readAllBytes();
+            }
+            exported.set(exchange.getRequestURI().getPath());
+            exchange.sendResponseHeaders(200, -1);
+            exchange.close();
+        });
+        server.start();
+        try {
+            ShenyuConfig shenyuConfig = new ShenyuConfig();
+            // the configured endpoint is the base endpoint of the collector, 
the traces path is
+            // appended by the configuration
+            
shenyuConfig.getTracing().getOtlp().setEndpoint("http://127.0.0.1:"; + 
server.getAddress().getPort());
+            runner(shenyuConfig).run(context -> {
+                // the span is written to the collector when the context 
closes, which flushes the
+                // batch of the exporter
+                Span span = 
context.getBean(Tracer.class).spanBuilder("test").startSpan();
+                span.end();
+            });
+            Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> 
Objects.nonNull(exported.get()));
+        } finally {
+            server.stop(0);
+        }
+        Assertions.assertEquals("/v1/traces", exported.get());
+    }
+
+    @Test
+    public void testTheExporterIsOwnedByTheContext() {
+        ShenyuConfig exported = new ShenyuConfig();
+        
exported.getTracing().getOtlp().setEndpoint("http://127.0.0.1:4318/v1/traces";);
+        runner(exported).run(context -> Assertions.assertTrue(
+                
context.getBean(TracingExporterHolder.class).exporter().isPresent()));
+        runner(new ShenyuConfig()).run(context -> Assertions.assertFalse(
+                
context.getBean(TracingExporterHolder.class).exporter().isPresent()));
+    }
+
+    @Test
+    public void testTheChainCompletesWhileTheExporterIsUnreachable() {
+        ShenyuConfig shenyuConfig = new ShenyuConfig();
+        
shenyuConfig.getTracing().getOtlp().setEndpoint("http://127.0.0.1:1/v1/traces";);
+        runner(shenyuConfig).run(context -> {
+            ServerWebExchange exchange = MockServerWebExchange.from(
+                    MockServerHttpRequest.get("/http/order").build());
+            exchange.getAttributes().put(Constants.CONTEXT, new 
ShenyuContext());
+            ShenyuPluginChain chain = mock(ShenyuPluginChain.class);
+            when(chain.execute(any())).thenReturn(Mono.empty());
+            // the export of the span fails in the background, the request is 
forwarded anyway
+            Assertions.assertDoesNotThrow(() -> 
context.getBean(ShenyuPlugin.class)
+                    .execute(exchange, chain)
+                    .block(Duration.ofSeconds(5)));
+            verify(chain).execute(exchange);
+        });
+    }
+
+    @Test
+    public void testTheLoggedEndpointDoesNotCarryCredentials() {
+        Assertions.assertEquals("http://host:4318/v1/traces";, 
TracingPluginConfiguration
+                
.redactedEndpoint("http://user:pass@host:4318/v1/traces?token=secret";));
+        Assertions.assertEquals("http://host:4318/v1/traces";,
+                
TracingPluginConfiguration.redactedEndpoint("http://host:4318/v1/traces#fragment";));
+        Assertions.assertDoesNotThrow(() ->
+                TracingPluginConfiguration.redactedEndpoint("not a valid 
endpoint"));
+    }
+
+    private ApplicationContextRunner runner(final ShenyuConfig shenyuConfig) {
+        return new ApplicationContextRunner()
+                .withUserConfiguration(TracingPluginConfiguration.class)
+                .withBean(ShenyuConfig.class, () -> shenyuConfig)
+                .withPropertyValues("shenyu.tracing.enabled=true");
+    }
 }

Reply via email to