From f805076c5ccafdcd379e34e676f221c17f44704a Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Fri, 31 Jul 2026 11:06:13 +0800
Subject: [PATCH] feat(observability): add selectable tracing modes
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
server/skillhub-app/pom.xml | 13 ++
.../logging/CorrelationJsonProvider.java | 39 +++++-
.../logging/SkillHubEcsEncoder.java | 7 +-
.../SkillHubObservabilityProperties.java | 20 +++
.../tracing/SkillHubTracingConfiguration.java | 54 ++++++++
.../observability/tracing/TracingMode.java | 10 ++
...cingModeAutoConfigurationImportFilter.java | 51 +++++++
.../observability/tracing/package-info.java | 4 +
.../main/resources/META-INF/spring.factories | 2 +
.../src/main/resources/application.yml | 12 ++
.../src/main/resources/logback-spring.xml | 4 +
.../logging/SkillHubEcsEncoderTest.java | 26 ++++
.../SkillHubTracingConfigurationTest.java | 124 ++++++++++++++++++
...ModeAutoConfigurationImportFilterTest.java | 65 +++++++++
14 files changed, 426 insertions(+), 5 deletions(-)
create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubObservabilityProperties.java
create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfiguration.java
create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingMode.java
create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilter.java
create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/package-info.java
create mode 100644 server/skillhub-app/src/main/resources/META-INF/spring.factories
create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfigurationTest.java
create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilterTest.java
diff --git a/server/skillhub-app/pom.xml b/server/skillhub-app/pom.xml
index 8914e00e..413e7eaf 100644
--- a/server/skillhub-app/pom.xml
+++ b/server/skillhub-app/pom.xml
@@ -26,6 +26,19 @@
io.micrometer
micrometer-registry-prometheus
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+
+
+ org.apache.skywalking
+ apm-toolkit-logback-1.x
+ 9.6.0
+
net.logstash.logback
logstash-logback-encoder
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/CorrelationJsonProvider.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/CorrelationJsonProvider.java
index 814f33ce..82c87fd5 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/CorrelationJsonProvider.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/CorrelationJsonProvider.java
@@ -3,8 +3,10 @@ package com.iflytek.skillhub.observability.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.fasterxml.jackson.core.JsonGenerator;
import net.logstash.logback.composite.AbstractJsonProvider;
+import org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.LogbackMDCPatternConverter;
import java.io.IOException;
+import java.util.List;
import java.util.Map;
/**
@@ -17,22 +19,51 @@ final class CorrelationJsonProvider extends AbstractJsonProvider
private static final String SPAN_ID_KEY = "spanId";
private static final String EXTERNAL_TRACE_ID_KEY = "tid";
+ private final LogbackMDCPatternConverter externalTraceIdConverter;
+
+ CorrelationJsonProvider(boolean externalTraceIdEnabled) {
+ if (externalTraceIdEnabled) {
+ externalTraceIdConverter = new LogbackMDCPatternConverter();
+ externalTraceIdConverter.setOptionList(List.of(EXTERNAL_TRACE_ID_KEY));
+ externalTraceIdConverter.start();
+ } else {
+ externalTraceIdConverter = null;
+ }
+ }
+
@Override
public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
Map mdc = event.getMDCPropertyMap();
- if (mdc == null || mdc.isEmpty()) {
- return;
- }
+ mdc = mdc == null ? Map.of() : mdc;
writeIfPresent(generator, "request.id", mdc.get(REQUEST_ID_KEY));
writeIfPresent(
generator,
"trace.id",
- firstPresent(mdc.get(TRACE_ID_KEY), mdc.get(EXTERNAL_TRACE_ID_KEY))
+ firstPresent(mdc.get(TRACE_ID_KEY), externalTraceId(event, mdc))
);
writeIfPresent(generator, "span.id", mdc.get(SPAN_ID_KEY));
}
+ private String externalTraceId(ILoggingEvent event, Map mdc) {
+ String traceId = mdc.get(EXTERNAL_TRACE_ID_KEY);
+ if (!isPresent(traceId) && externalTraceIdConverter != null) {
+ traceId = externalTraceIdConverter.convert(event);
+ }
+ return normalizeExternalTraceId(traceId);
+ }
+
+ private String normalizeExternalTraceId(String traceId) {
+ if (!isPresent(traceId)) {
+ return null;
+ }
+ String normalized = traceId.trim();
+ if (normalized.regionMatches(true, 0, "TID:", 0, 4)) {
+ normalized = normalized.substring(4).trim();
+ }
+ return "N/A".equalsIgnoreCase(normalized) ? null : normalized;
+ }
+
private String firstPresent(String preferred, String fallback) {
return isPresent(preferred) ? preferred : fallback;
}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoder.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoder.java
index 7761b49d..8dc728bb 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoder.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoder.java
@@ -25,6 +25,7 @@ public class SkillHubEcsEncoder extends LoggingEventCompositeJsonEncoder {
private String serviceName = "skillhub";
private String serviceVersion = "unknown";
private String serviceEnvironment = "local";
+ private boolean externalTraceIdEnabled;
@Override
public void start() {
@@ -48,6 +49,10 @@ public class SkillHubEcsEncoder extends LoggingEventCompositeJsonEncoder {
this.serviceEnvironment = serviceEnvironment;
}
+ public void setTracingMode(String tracingMode) {
+ this.externalTraceIdEnabled = "external-agent".equalsIgnoreCase(tracingMode);
+ }
+
private LoggingEventJsonProviders createProviders() {
LoggingEventJsonProviders providers = new LoggingEventJsonProviders();
@@ -74,7 +79,7 @@ public class SkillHubEcsEncoder extends LoggingEventCompositeJsonEncoder {
providers.addThreadName(thread);
providers.addGlobalCustomFields(serviceFields());
- providers.addProvider(new CorrelationJsonProvider());
+ providers.addProvider(new CorrelationJsonProvider(externalTraceIdEnabled));
ThrowableClassNameJsonProvider errorType = new ThrowableClassNameJsonProvider();
errorType.setFieldName("error.type");
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubObservabilityProperties.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubObservabilityProperties.java
new file mode 100644
index 00000000..c27a3bfc
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubObservabilityProperties.java
@@ -0,0 +1,20 @@
+package com.iflytek.skillhub.observability.tracing;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Startup-time observability choices owned by SkillHub.
+ */
+@ConfigurationProperties(prefix = "skillhub.observability")
+public class SkillHubObservabilityProperties {
+
+ private TracingMode tracingMode = TracingMode.NONE;
+
+ public TracingMode getTracingMode() {
+ return tracingMode;
+ }
+
+ public void setTracingMode(TracingMode tracingMode) {
+ this.tracingMode = tracingMode;
+ }
+}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfiguration.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfiguration.java
new file mode 100644
index 00000000..ea3872b2
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfiguration.java
@@ -0,0 +1,54 @@
+package com.iflytek.skillhub.observability.tracing;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.util.StringUtils;
+
+/**
+ * Validates tracing mode combinations that SkillHub can determine at startup.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(SkillHubObservabilityProperties.class)
+public class SkillHubTracingConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(SkillHubTracingConfiguration.class);
+
+ @Bean
+ TracingModeGuard tracingModeGuard(
+ SkillHubObservabilityProperties properties,
+ Environment environment
+ ) {
+ TracingMode mode = properties.getTracingMode();
+ String otlpEndpoint = environment.getProperty("management.otlp.tracing.endpoint");
+ if (mode != TracingMode.OTEL_SDK && StringUtils.hasText(otlpEndpoint)) {
+ throw new IllegalStateException(
+ "management.otlp.tracing.endpoint requires "
+ + "skillhub.observability.tracing-mode=otel-sdk"
+ );
+ }
+ if (mode == TracingMode.OTEL_SDK
+ && Boolean.FALSE.equals(environment.getProperty(
+ "management.tracing.enabled",
+ Boolean.class
+ ))) {
+ throw new IllegalStateException(
+ "management.tracing.enabled=false conflicts with "
+ + "skillhub.observability.tracing-mode=otel-sdk"
+ );
+ }
+ if (mode == TracingMode.EXTERNAL_AGENT) {
+ log.warn(
+ "External tracing agent mode selected. SkillHub cannot verify the agent "
+ + "identity; deployment must provide exactly one tracing agent"
+ );
+ }
+ return new TracingModeGuard(mode);
+ }
+
+ record TracingModeGuard(TracingMode mode) {
+ }
+}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingMode.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingMode.java
new file mode 100644
index 00000000..feaf1171
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingMode.java
@@ -0,0 +1,10 @@
+package com.iflytek.skillhub.observability.tracing;
+
+/**
+ * Selects the single tracing implementation that may be active in the application process.
+ */
+public enum TracingMode {
+ NONE,
+ OTEL_SDK,
+ EXTERNAL_AGENT
+}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilter.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilter.java
new file mode 100644
index 00000000..55a33fda
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilter.java
@@ -0,0 +1,51 @@
+package com.iflytek.skillhub.observability.tracing;
+
+import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter;
+import org.springframework.boot.autoconfigure.AutoConfigurationMetadata;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.core.env.Environment;
+
+import java.util.Set;
+
+/**
+ * Keeps the OpenTelemetry SDK outside the application context unless the deployment explicitly
+ * selects {@code otel-sdk}. The normal Spring Boot NOOP tracer remains available in the other
+ * modes.
+ */
+public final class TracingModeAutoConfigurationImportFilter
+ implements AutoConfigurationImportFilter, EnvironmentAware {
+
+ static final String TRACING_MODE_PROPERTY = "skillhub.observability.tracing-mode";
+
+ private static final Set OTEL_AUTO_CONFIGURATIONS = Set.of(
+ "org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration",
+ "org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration",
+ "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration"
+ );
+
+ private Environment environment;
+
+ @Override
+ public boolean[] match(
+ String[] autoConfigurationClasses,
+ AutoConfigurationMetadata autoConfigurationMetadata
+ ) {
+ boolean otelSdkEnabled = environment != null
+ && "otel-sdk".equalsIgnoreCase(
+ environment.getProperty(TRACING_MODE_PROPERTY, "none")
+ );
+ boolean[] matches = new boolean[autoConfigurationClasses.length];
+ for (int index = 0; index < autoConfigurationClasses.length; index++) {
+ String autoConfigurationClass = autoConfigurationClasses[index];
+ matches[index] = autoConfigurationClass != null
+ && (otelSdkEnabled
+ || !OTEL_AUTO_CONFIGURATIONS.contains(autoConfigurationClass));
+ }
+ return matches;
+ }
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ this.environment = environment;
+ }
+}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/package-info.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/package-info.java
new file mode 100644
index 00000000..a6043895
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/tracing/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Startup tracing mode selection and auto-configuration boundaries.
+ */
+package com.iflytek.skillhub.observability.tracing;
diff --git a/server/skillhub-app/src/main/resources/META-INF/spring.factories b/server/skillhub-app/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..e8a70f68
--- /dev/null
+++ b/server/skillhub-app/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
+com.iflytek.skillhub.observability.tracing.TracingModeAutoConfigurationImportFilter
diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml
index 9983d5e8..10e9638e 100644
--- a/server/skillhub-app/src/main/resources/application.yml
+++ b/server/skillhub-app/src/main/resources/application.yml
@@ -95,6 +95,7 @@ spring:
skillhub:
observability:
+ tracing-mode: ${SKILLHUB_TRACING_MODE:none}
log-format: ${SKILLHUB_LOG_FORMAT:text}
log-async-queue-size: ${SKILLHUB_LOG_ASYNC_QUEUE_SIZE:1024}
service-version: ${SKILLHUB_SERVICE_VERSION:unknown}
@@ -214,6 +215,17 @@ skillhub:
email: ${BOOTSTRAP_ADMIN_EMAIL:admin@skillhub.local}
management:
+ tracing:
+ sampling:
+ probability: ${SKILLHUB_TRACING_SAMPLING_PROBABILITY:0.1}
+ baggage:
+ enabled: false
+ propagation:
+ type: W3C
+ otlp:
+ tracing:
+ timeout: ${SKILLHUB_OTLP_TIMEOUT:5s}
+ compression: ${SKILLHUB_OTLP_COMPRESSION:gzip}
health:
mail:
enabled: ${MANAGEMENT_HEALTH_MAIL_ENABLED:false}
diff --git a/server/skillhub-app/src/main/resources/logback-spring.xml b/server/skillhub-app/src/main/resources/logback-spring.xml
index 30152e8e..c9372168 100644
--- a/server/skillhub-app/src/main/resources/logback-spring.xml
+++ b/server/skillhub-app/src/main/resources/logback-spring.xml
@@ -5,6 +5,9 @@
+
@@ -30,6 +33,7 @@
${serviceName}
${serviceVersion}
${serviceEnvironment}
+ ${tracingMode}
diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoderTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoderTest.java
index 9d0df479..e417d97e 100644
--- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoderTest.java
+++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/logging/SkillHubEcsEncoderTest.java
@@ -81,6 +81,32 @@ class SkillHubEcsEncoderTest {
.hasSize(1);
}
+ @Test
+ void shouldNormalizeExternalAgentTraceId() throws Exception {
+ encoder.stop();
+ encoder.setTracingMode("external-agent");
+ encoder.start();
+ LoggingEvent event = event("external trace");
+ event.setMDCPropertyMap(Map.of("tid", "TID: external-agent-trace"));
+
+ JsonNode json = encode(event);
+
+ assertThat(json.path("trace.id").asText()).isEqualTo("external-agent-trace");
+ }
+
+ @Test
+ void shouldNotWriteToolkitSentinelAsTraceId() throws Exception {
+ encoder.stop();
+ encoder.setTracingMode("external-agent");
+ encoder.start();
+ LoggingEvent event = event("no external agent");
+ event.setMDCPropertyMap(Map.of("tid", "TID: N/A"));
+
+ JsonNode json = encode(event);
+
+ assertThat(json.has("trace.id")).isFalse();
+ }
+
@Test
void shouldWriteStructuredExceptionFields() throws Exception {
LoggingEvent event = event("failed");
diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfigurationTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfigurationTest.java
new file mode 100644
index 00000000..1c5913ce
--- /dev/null
+++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/SkillHubTracingConfigurationTest.java
@@ -0,0 +1,124 @@
+package com.iflytek.skillhub.observability.tracing;
+
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import org.junit.jupiter.api.Test;
+import org.slf4j.MDC;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SkillHubTracingConfigurationTest {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(TestApplication.class)
+ .withPropertyValues(
+ "spring.flyway.enabled=false",
+ "spring.jpa.hibernate.ddl-auto=none"
+ );
+
+ @Test
+ void noneModeShouldUseNoopTracerAndNoOtelSdk() {
+ contextRunner
+ .withPropertyValues("skillhub.observability.tracing-mode=none")
+ .run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean(Tracer.class)).isSameAs(Tracer.NOOP);
+ assertThat(context).doesNotHaveBean(OpenTelemetry.class);
+ assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class);
+ });
+ }
+
+ @Test
+ void externalAgentModeShouldUseNoopTracerAndNoOtelSdk() {
+ contextRunner
+ .withPropertyValues("skillhub.observability.tracing-mode=external-agent")
+ .run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean(Tracer.class)).isSameAs(Tracer.NOOP);
+ assertThat(context).doesNotHaveBean(OpenTelemetry.class);
+ assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class);
+ });
+ }
+
+ @Test
+ void otelSdkModeWithoutEndpointShouldCreateInProcessTracerOnly() {
+ contextRunner
+ .withPropertyValues(
+ "skillhub.observability.tracing-mode=otel-sdk",
+ "management.tracing.sampling.probability=1.0",
+ "management.tracing.baggage.enabled=false",
+ "management.tracing.propagation.type=W3C"
+ )
+ .run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean(Tracer.class)).isInstanceOf(OtelTracer.class);
+ assertThat(context).hasSingleBean(OpenTelemetry.class);
+ assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class);
+ });
+ }
+
+ @Test
+ void otelSdkModeShouldCreateExporterOnlyWhenEndpointIsConfigured() {
+ contextRunner
+ .withPropertyValues(
+ "skillhub.observability.tracing-mode=otel-sdk",
+ "management.otlp.tracing.endpoint=http://127.0.0.1:4318/v1/traces"
+ )
+ .run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class);
+ });
+ }
+
+ @Test
+ void nonOtelModeShouldRejectConfiguredOtlpEndpoint() {
+ contextRunner
+ .withPropertyValues(
+ "skillhub.observability.tracing-mode=none",
+ "management.otlp.tracing.endpoint=http://127.0.0.1:4318/v1/traces"
+ )
+ .run(context -> assertThat(context).hasFailed());
+ }
+
+ @Test
+ void otelSdkModeShouldRejectDisabledTracing() {
+ contextRunner
+ .withPropertyValues(
+ "skillhub.observability.tracing-mode=otel-sdk",
+ "management.tracing.enabled=false"
+ )
+ .run(context -> assertThat(context).hasFailed());
+ }
+
+ @Test
+ void otelSdkScopeShouldPublishTraceCorrelationToMdc() {
+ contextRunner
+ .withPropertyValues(
+ "skillhub.observability.tracing-mode=otel-sdk",
+ "management.tracing.sampling.probability=1.0"
+ )
+ .run(context -> {
+ Tracer tracer = context.getBean(Tracer.class);
+ io.micrometer.tracing.Span span = tracer.nextSpan().name("test-span").start();
+ try (Tracer.SpanInScope ignored = tracer.withSpan(span)) {
+ assertThat(MDC.get("traceId")).hasSize(32);
+ assertThat(MDC.get("spanId")).hasSize(16);
+ } finally {
+ span.end();
+ MDC.clear();
+ }
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableAutoConfiguration
+ @Import(SkillHubTracingConfiguration.class)
+ static class TestApplication {
+ }
+}
diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilterTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilterTest.java
new file mode 100644
index 00000000..6cfbb7d3
--- /dev/null
+++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/TracingModeAutoConfigurationImportFilterTest.java
@@ -0,0 +1,65 @@
+package com.iflytek.skillhub.observability.tracing;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurationMetadata;
+import org.springframework.mock.env.MockEnvironment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+class TracingModeAutoConfigurationImportFilterTest {
+
+ private static final String CORE_OTEL_AUTO_CONFIGURATION =
+ "org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration";
+ private static final String TRACING_OTEL_AUTO_CONFIGURATION =
+ "org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration";
+ private static final String OTLP_AUTO_CONFIGURATION =
+ "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration";
+ private static final String NOOP_AUTO_CONFIGURATION =
+ "org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfiguration";
+
+ private final TracingModeAutoConfigurationImportFilter filter =
+ new TracingModeAutoConfigurationImportFilter();
+
+ @Test
+ void shouldExcludeApplicationOtelForDefaultNoneMode() {
+ filter.setEnvironment(new MockEnvironment());
+
+ assertThat(matches()).containsExactly(false, false, false, true, false);
+ }
+
+ @Test
+ void shouldExcludeApplicationOtelForExternalAgentMode() {
+ filter.setEnvironment(new MockEnvironment()
+ .withProperty(
+ TracingModeAutoConfigurationImportFilter.TRACING_MODE_PROPERTY,
+ "external-agent"
+ ));
+
+ assertThat(matches()).containsExactly(false, false, false, true, false);
+ }
+
+ @Test
+ void shouldEnableApplicationOtelOnlyForOtelSdkMode() {
+ filter.setEnvironment(new MockEnvironment()
+ .withProperty(
+ TracingModeAutoConfigurationImportFilter.TRACING_MODE_PROPERTY,
+ "otel-sdk"
+ ));
+
+ assertThat(matches()).containsExactly(true, true, true, true, false);
+ }
+
+ private boolean[] matches() {
+ return filter.match(
+ new String[]{
+ CORE_OTEL_AUTO_CONFIGURATION,
+ TRACING_OTEL_AUTO_CONFIGURATION,
+ OTLP_AUTO_CONFIGURATION,
+ NOOP_AUTO_CONFIGURATION,
+ null
+ },
+ mock(AutoConfigurationMetadata.class)
+ );
+ }
+}