From 91fb155ef17ec27dd7e9abb7c6e205da8f3c305a Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:22:12 +0800 Subject: [PATCH] feat(observability): propagate async trace context Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../iflytek/skillhub/config/AsyncConfig.java | 6 +- .../skillhub/config/SkillScannerConfig.java | 7 +- .../RequestIdThreadLocalAccessor.java | 37 ++ ...illHubContextPropagationConfiguration.java | 51 +++ .../ContextPropagationConfigurationTest.java | 320 ++++++++++++++++++ .../tracing/HttpTracePropagationTest.java | 178 ++++++++++ .../auth/oauth/GitLabClaimsExtractor.java | 8 + 7 files changed, 604 insertions(+), 3 deletions(-) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/RequestIdThreadLocalAccessor.java create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/SkillHubContextPropagationConfiguration.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/ContextPropagationConfigurationTest.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/HttpTracePropagationTest.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java index 7feb8955..10a6f5a5 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java @@ -2,6 +2,7 @@ package com.iflytek.skillhub.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -19,12 +20,15 @@ import java.util.concurrent.ThreadPoolExecutor; public class AsyncConfig { @Bean(name = "skillhubEventExecutor") - public Executor skillhubEventExecutor() { + public Executor skillhubEventExecutor( + ContextPropagatingTaskDecorator contextPropagatingTaskDecorator + ) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(100); executor.setThreadNamePrefix("skillhub-event-"); + executor.setTaskDecorator(contextPropagatingTaskDecorator); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java index d6b7a5ec..9753e982 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java @@ -26,7 +26,10 @@ public class SkillScannerConfig { @Bean @ConditionalOnProperty(prefix = "skillhub.security.scanner", name = "enabled", havingValue = "true") - public HttpClient scannerHttpClient(SkillScannerProperties properties) { + public HttpClient scannerHttpClient( + WebClient.Builder webClientBuilder, + SkillScannerProperties properties + ) { int readTimeoutMs = properties.getReadTimeoutMs(); int connectTimeoutMs = properties.getConnectTimeoutMs(); @@ -52,7 +55,7 @@ public class SkillScannerConfig { .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(100 * 1024 * 1024)) .build(); - WebClient webClient = WebClient.builder() + WebClient webClient = webClientBuilder.clone() .clientConnector(new ReactorClientHttpConnector(reactorClient)) .exchangeStrategies(strategies) .build(); diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/RequestIdThreadLocalAccessor.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/RequestIdThreadLocalAccessor.java new file mode 100644 index 00000000..9d1deffb --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/RequestIdThreadLocalAccessor.java @@ -0,0 +1,37 @@ +package com.iflytek.skillhub.observability; + +import io.micrometer.context.ThreadLocalAccessor; + +/** + * Captures and restores the authoritative Request ID scope for asynchronous execution. + */ +public final class RequestIdThreadLocalAccessor implements ThreadLocalAccessor { + + public static final String KEY = "skillhub.request-id"; + + private final RequestIdAccessor requestIdAccessor; + + public RequestIdThreadLocalAccessor(RequestIdAccessor requestIdAccessor) { + this.requestIdAccessor = requestIdAccessor; + } + + @Override + public Object key() { + return KEY; + } + + @Override + public String getValue() { + return requestIdAccessor.current(); + } + + @Override + public void setValue(String value) { + requestIdAccessor.replace(value); + } + + @Override + public void setValue() { + requestIdAccessor.replace(null); + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/SkillHubContextPropagationConfiguration.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/SkillHubContextPropagationConfiguration.java new file mode 100644 index 00000000..e0eaa710 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/observability/SkillHubContextPropagationConfiguration.java @@ -0,0 +1,51 @@ +package com.iflytek.skillhub.observability; + +import com.iflytek.skillhub.observability.tracing.SkillHubObservabilityProperties; +import com.iflytek.skillhub.observability.tracing.TracingMode; +import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshotFactory; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.contextpropagation.ObservationAwareSpanThreadLocalAccessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; + +/** + * Defines the context captured by SkillHub-managed asynchronous executors. + */ +@Configuration(proxyBeanMethods = false) +public class SkillHubContextPropagationConfiguration { + + @Bean + ContextRegistry skillHubContextRegistry( + RequestIdAccessor requestIdAccessor, + SkillHubObservabilityProperties observabilityProperties, + ObservationRegistry observationRegistry, + Tracer tracer + ) { + ContextRegistry registry = new ContextRegistry() + .loadContextAccessors() + .loadThreadLocalAccessors(); + registry.registerThreadLocalAccessor( + new RequestIdThreadLocalAccessor(requestIdAccessor) + ); + if (observabilityProperties.getTracingMode() == TracingMode.OTEL_SDK) { + registry.registerThreadLocalAccessor( + new ObservationAwareSpanThreadLocalAccessor(observationRegistry, tracer) + ); + } + return registry; + } + + @Bean + ContextPropagatingTaskDecorator skillHubContextPropagatingTaskDecorator( + ContextRegistry skillHubContextRegistry + ) { + ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() + .contextRegistry(skillHubContextRegistry) + .clearMissing(true) + .build(); + return new ContextPropagatingTaskDecorator(snapshotFactory); + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/ContextPropagationConfigurationTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/ContextPropagationConfigurationTest.java new file mode 100644 index 00000000..ceb1b351 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/ContextPropagationConfigurationTest.java @@ -0,0 +1,320 @@ +package com.iflytek.skillhub.observability; + +import com.iflytek.skillhub.config.AsyncConfig; +import com.iflytek.skillhub.observability.tracing.SkillHubTracingConfiguration; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +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 org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ContextPropagationConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestApplication.class) + .withPropertyValues( + "spring.flyway.enabled=false", + "spring.jpa.hibernate.ddl-auto=none" + ); + + @Test + void requestIdShouldPropagateRestoreNestedScopeAndNotLeakOnThreadReuse() { + contextRunner + .withPropertyValues("skillhub.observability.tracing-mode=none") + .run(context -> { + RequestIdAccessor requestIdAccessor = + context.getBean(RequestIdAccessor.class); + TaskDecorator taskDecorator = context.getBean(TaskDecorator.class); + ExecutorService worker = Executors.newSingleThreadExecutor(); + try { + ContextValues propagated; + try (RequestIdAccessor.Scope ignored = + requestIdAccessor.open("request-one")) { + propagated = execute(worker, taskDecorator, () -> { + assertThat(requestIdAccessor.current()) + .isEqualTo("request-one"); + try (RequestIdAccessor.Scope nested = + requestIdAccessor.open("nested")) { + assertThat(requestIdAccessor.current()) + .isEqualTo("nested"); + } + return currentValues(requestIdAccessor, null); + }); + } + + assertThat(propagated.requestId()).isEqualTo("request-one"); + assertThat(propagated.mdcRequestId()).isEqualTo("request-one"); + + try (RequestIdAccessor.Scope ignored = + requestIdAccessor.open("request-failure")) { + assertThatThrownBy(() -> execute( + worker, + taskDecorator, + () -> { + throw new IllegalStateException("expected failure"); + } + )).hasCauseInstanceOf(IllegalStateException.class); + } + + ContextValues clean = execute( + worker, + taskDecorator, + () -> currentValues(requestIdAccessor, null) + ); + assertThat(clean.requestId()).isNull(); + assertThat(clean.mdcRequestId()).isNull(); + } finally { + worker.shutdownNow(); + MDC.clear(); + } + }); + } + + @Test + void configuredEventExecutorShouldPropagateRequestId() { + contextRunner + .withPropertyValues("skillhub.observability.tracing-mode=none") + .run(context -> { + RequestIdAccessor requestIdAccessor = + context.getBean(RequestIdAccessor.class); + Executor executor = context.getBean("skillhubEventExecutor", Executor.class); + try (RequestIdAccessor.Scope ignored = + requestIdAccessor.open("configured-executor")) { + FutureTask task = new FutureTask<>( + () -> currentValues(requestIdAccessor, null) + ); + executor.execute(task); + + assertThat(task.get(5, TimeUnit.SECONDS).requestId()) + .isEqualTo("configured-executor"); + } finally { + MDC.clear(); + } + }); + } + + @Test + void callerRunsPolicyShouldRestoreCallerScopeAndLeaveWorkerClean() { + contextRunner + .withPropertyValues("skillhub.observability.tracing-mode=none") + .run(context -> { + RequestIdAccessor requestIdAccessor = + context.getBean(RequestIdAccessor.class); + TaskDecorator taskDecorator = context.getBean(TaskDecorator.class); + ThreadPoolTaskExecutor executor = callerRunsExecutor(taskDecorator); + CountDownLatch workerStarted = new CountDownLatch(1); + CountDownLatch releaseWorker = new CountDownLatch(1); + FutureTask blockingTask = new FutureTask<>(() -> { + workerStarted.countDown(); + releaseWorker.await(5, TimeUnit.SECONDS); + return null; + }); + try { + executor.execute(blockingTask); + assertThat(workerStarted.await(5, TimeUnit.SECONDS)).isTrue(); + + AtomicReference callerRunValues = + new AtomicReference<>(); + String callerThread = Thread.currentThread().getName(); + try (RequestIdAccessor.Scope ignored = + requestIdAccessor.open("caller-request")) { + executor.execute(() -> { + assertThat(Thread.currentThread().getName()) + .isEqualTo(callerThread); + callerRunValues.set(currentValues( + requestIdAccessor, + null + )); + }); + assertThat(requestIdAccessor.current()) + .isEqualTo("caller-request"); + assertThat(MDC.get(RequestIdAccessor.MDC_KEY)) + .isEqualTo("caller-request"); + } + + assertThat(callerRunValues.get().requestId()) + .isEqualTo("caller-request"); + releaseWorker.countDown(); + blockingTask.get(5, TimeUnit.SECONDS); + + FutureTask cleanTask = new FutureTask<>( + () -> currentValues(requestIdAccessor, null) + ); + executor.execute(cleanTask); + ContextValues clean = cleanTask.get(5, TimeUnit.SECONDS); + assertThat(clean.requestId()).isNull(); + assertThat(clean.mdcRequestId()).isNull(); + } finally { + releaseWorker.countDown(); + executor.shutdown(); + MDC.clear(); + } + }); + } + + @Test + void otelSpanShouldPropagateAndBeClearedAfterTask() { + contextRunner + .withPropertyValues( + "skillhub.observability.tracing-mode=otel-sdk", + "management.tracing.sampling.probability=1.0" + ) + .run(context -> { + RequestIdAccessor requestIdAccessor = + context.getBean(RequestIdAccessor.class); + TaskDecorator taskDecorator = context.getBean(TaskDecorator.class); + Tracer tracer = context.getBean(Tracer.class); + ExecutorService worker = Executors.newSingleThreadExecutor(); + Span span = tracer.nextSpan().name("parent").start(); + try { + ContextValues propagated; + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + propagated = execute( + worker, + taskDecorator, + () -> currentValues(requestIdAccessor, tracer) + ); + } + + assertThat(propagated.traceId()) + .isEqualTo(span.context().traceId()); + assertThat(propagated.mdcTraceId()) + .isEqualTo(span.context().traceId()); + + ContextValues clean = execute( + worker, + taskDecorator, + () -> currentValues(requestIdAccessor, tracer) + ); + assertThat(clean.traceId()).isNull(); + assertThat(clean.mdcTraceId()).isNull(); + } finally { + span.end(); + worker.shutdownNow(); + MDC.clear(); + } + }); + } + + @Test + void otelObservationShouldPropagateItsTraceAndRestoreWorker() { + contextRunner + .withPropertyValues( + "skillhub.observability.tracing-mode=otel-sdk", + "management.tracing.sampling.probability=1.0" + ) + .run(context -> { + RequestIdAccessor requestIdAccessor = + context.getBean(RequestIdAccessor.class); + TaskDecorator taskDecorator = context.getBean(TaskDecorator.class); + Tracer tracer = context.getBean(Tracer.class); + ObservationRegistry observationRegistry = + context.getBean(ObservationRegistry.class); + ExecutorService worker = Executors.newSingleThreadExecutor(); + Observation observation = Observation + .createNotStarted("parent-observation", observationRegistry) + .start(); + try { + ContextValues propagated; + String parentTraceId; + try (Observation.Scope ignored = observation.openScope()) { + assertThat(tracer.currentSpan()).isNotNull(); + parentTraceId = tracer.currentSpan().context().traceId(); + propagated = execute( + worker, + taskDecorator, + () -> currentValues(requestIdAccessor, tracer) + ); + } + + assertThat(propagated.traceId()).isEqualTo(parentTraceId); + assertThat(propagated.mdcTraceId()).isEqualTo(parentTraceId); + + ContextValues clean = execute( + worker, + taskDecorator, + () -> currentValues(requestIdAccessor, tracer) + ); + assertThat(clean.traceId()).isNull(); + assertThat(clean.mdcTraceId()).isNull(); + } finally { + observation.stop(); + worker.shutdownNow(); + MDC.clear(); + } + }); + } + + private ContextValues currentValues( + RequestIdAccessor requestIdAccessor, + Tracer tracer + ) { + Span currentSpan = tracer == null ? null : tracer.currentSpan(); + return new ContextValues( + requestIdAccessor.current(), + MDC.get(RequestIdAccessor.MDC_KEY), + currentSpan == null ? null : currentSpan.context().traceId(), + MDC.get("traceId") + ); + } + + private T execute( + Executor executor, + TaskDecorator taskDecorator, + Callable action + ) throws Exception { + FutureTask task = new FutureTask<>(action); + executor.execute(taskDecorator.decorate(task)); + return task.get(5, TimeUnit.SECONDS); + } + + private ThreadPoolTaskExecutor callerRunsExecutor(TaskDecorator taskDecorator) { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(1); + executor.setMaxPoolSize(1); + executor.setQueueCapacity(0); + executor.setTaskDecorator(taskDecorator); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + private record ContextValues( + String requestId, + String mdcRequestId, + String traceId, + String mdcTraceId + ) { + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @Import({ + SkillHubTracingConfiguration.class, + SkillHubContextPropagationConfiguration.class, + RequestIdAccessor.class, + AsyncConfig.class + }) + static class TestApplication { + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/HttpTracePropagationTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/HttpTracePropagationTest.java new file mode 100644 index 00000000..0e0b4c18 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/observability/tracing/HttpTracePropagationTest.java @@ -0,0 +1,178 @@ +package com.iflytek.skillhub.observability.tracing; + +import com.iflytek.skillhub.auth.oauth.GitLabClaimsExtractor; +import com.iflytek.skillhub.auth.oauth.OAuthClaims; +import com.iflytek.skillhub.config.SkillScannerConfig; +import com.iflytek.skillhub.config.SkillScannerProperties; +import com.iflytek.skillhub.infra.http.HttpClient; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; +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 org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpTracePropagationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestApplication.class) + .withPropertyValues( + "spring.flyway.enabled=false", + "spring.jpa.hibernate.ddl-auto=none", + "skillhub.observability.tracing-mode=otel-sdk", + "management.tracing.sampling.probability=1.0", + "skillhub.security.scanner.enabled=true" + ); + + @Test + void shouldPropagateW3cContextToScannerButNotExternalGitLab() throws Exception { + try (HeaderCaptureServer scannerServer = + new HeaderCaptureServer("text/plain", "scanner-ok"); + HeaderCaptureServer gitLabServer = + new HeaderCaptureServer( + "application/json", + """ + [ + { + "email": "alice@gitlab.example", + "confirmed_at": "2026-04-16T08:00:00Z" + } + ] + """ + )) { + contextRunner.run(context -> { + Tracer tracer = context.getBean(Tracer.class); + HttpClient scannerClient = + context.getBean("scannerHttpClient", HttpClient.class); + GitLabClaimsExtractor gitLabClaimsExtractor = + context.getBean(GitLabClaimsExtractor.class); + Span span = tracer.nextSpan().name("outbound-boundary").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + assertThat(scannerClient.get( + scannerServer.url("/health"), + String.class + )).isEqualTo("scanner-ok"); + + OAuthClaims claims = gitLabClaimsExtractor.extract( + gitLabRequest(gitLabServer.url("/api/v4/user")), + new DefaultOAuth2User( + List.of(), + Map.of( + "id", 42, + "username", "alice", + "email", "alice+pending@gitlab.example" + ), + "username" + ) + ); + assertThat(claims.email()) + .isEqualTo("alice@gitlab.example"); + } finally { + span.end(); + } + + String scannerTraceparent = scannerServer.traceparent(); + assertThat(scannerTraceparent) + .matches("^00-[0-9a-f]{32}-[0-9a-f]{16}-0[01]$"); + assertThat(scannerTraceparent.substring(3, 35)) + .isEqualTo(span.context().traceId()); + assertThat(gitLabServer.traceparent()).isNull(); + }); + } + } + + private OAuth2UserRequest gitLabRequest(String userInfoUri) { + ClientRegistration registration = ClientRegistration + .withRegistrationId("gitlab") + .clientId("client-id") + .clientSecret("client-secret") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("read_user", "email") + .authorizationUri("https://gitlab.example/oauth/authorize") + .tokenUri("https://gitlab.example/oauth/token") + .userInfoUri(userInfoUri) + .userNameAttributeName("username") + .clientName("GitLab") + .build(); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "test-token", + Instant.now(), + Instant.now().plusSeconds(3600) + ); + return new OAuth2UserRequest(registration, accessToken); + } + + private static final class HeaderCaptureServer implements AutoCloseable { + + private final HttpServer server; + private final AtomicReference traceparent = new AtomicReference<>(); + + private HeaderCaptureServer(String contentType, String body) throws IOException { + byte[] response = body.getBytes(StandardCharsets.UTF_8); + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> respond( + exchange, + contentType, + response + )); + server.start(); + } + + private void respond( + HttpExchange exchange, + String contentType, + byte[] response + ) throws IOException { + traceparent.set(exchange.getRequestHeaders().getFirst("traceparent")); + exchange.getResponseHeaders().set("Content-Type", contentType); + exchange.sendResponseHeaders(200, response.length); + try (var responseBody = exchange.getResponseBody()) { + responseBody.write(response); + } + } + + private String url(String path) { + return "http://127.0.0.1:" + server.getAddress().getPort() + path; + } + + private String traceparent() { + return traceparent.get(); + } + + @Override + public void close() { + server.stop(0); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @Import({ + SkillHubTracingConfiguration.class, + SkillScannerConfig.class, + SkillScannerProperties.class, + GitLabClaimsExtractor.class + }) + static class TestApplication { + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java index d6c840a2..7a744a30 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java @@ -27,6 +27,14 @@ public class GitLabClaimsExtractor implements OAuthClaimsExtractor { private final RestClient restClient; + /** + * Uses an external-service client that is intentionally not customized with application + * tracing. Trace context must not be propagated to a user-configured GitLab host. + */ + public GitLabClaimsExtractor() { + this(RestClient.builder()); + } + public GitLabClaimsExtractor(RestClient.Builder restClientBuilder) { this.restClient = restClientBuilder .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)