From 0a8594d00c10eda38c2dd733b665fbac2b46eefd Mon Sep 17 00:00:00 2001 From: Eunbin Son Date: Fri, 28 Aug 2026 18:12:26 +0900 Subject: [PATCH] Suppress instrumentation of exporter requests in JdkHttpSender --- .../sender/jdk/internal/JdkHttpSender.java | 40 +++++++------- .../jdk/internal/JdkHttpSenderTest.java | 52 +++++++++++++++++++ 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java index e836766c781..1c9e7b5d669 100644 --- a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java +++ b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java @@ -5,6 +5,7 @@ package io.opentelemetry.exporter.sender.jdk.internal; +import io.opentelemetry.api.impl.InstrumentationUtil; import io.opentelemetry.exporter.internal.RetryUtil; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.Compressor; @@ -169,24 +170,27 @@ private static HttpClient configureClient( public void send( MessageWriter messageWriter, Consumer onResponse, Consumer onError) { try { - CompletableFuture unused = - CompletableFuture.supplyAsync( - () -> { - try { - return sendInternal(messageWriter); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }, - executorService) - .whenComplete( - (httpResponse, throwable) -> { - if (throwable != null) { - onError.accept(throwable); - return; - } - onResponse.accept(httpResponse); - }); + InstrumentationUtil.suppressInstrumentation( + () -> { + CompletableFuture unused = + CompletableFuture.supplyAsync( + () -> { + try { + return sendInternal(messageWriter); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, + executorService) + .whenComplete( + (httpResponse, throwable) -> { + if (throwable != null) { + onError.accept(throwable); + return; + } + onResponse.accept(httpResponse); + }); + }); } catch (RejectedExecutionException e) { onError.accept(e); } diff --git a/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java index 9e4d4258642..6b75403c06a 100644 --- a/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java +++ b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -16,6 +17,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.opentelemetry.api.impl.InstrumentationUtil; +import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.HttpResponse; import io.opentelemetry.sdk.common.export.MessageWriter; @@ -40,6 +43,7 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLException; import org.assertj.core.api.InstanceOfAssertFactories; @@ -391,6 +395,54 @@ void send_rejectedExecution_callsOnError() { assertThat(responseRef.get()).isNull(); } + @SuppressWarnings("unchecked") + @Test + void send_suppressesInstrumentation() throws Exception { + java.net.http.HttpResponse mockJdkResponse = + mock(java.net.http.HttpResponse.class); + when(mockJdkResponse.statusCode()).thenReturn(200); + when(mockJdkResponse.body()).thenReturn(new ByteArrayInputStream(new byte[0])); + when(mockJdkResponse.headers()) + .thenReturn(HttpHeaders.of(Collections.emptyMap(), (a, b) -> true)); + + AtomicBoolean suppressed = new AtomicBoolean(false); + doAnswer( + invocation -> { + suppressed.set(InstrumentationUtil.shouldSuppressInstrumentation(Context.current())); + return mockJdkResponse; + }) + .when(mockHttpClient) + .send(any(), any()); + + // Context.taskWrapping stands in for the java agent's executor instrumentation, which + // propagates the calling thread's context to the thread the request is sent on. + ExecutorService executor = Context.taskWrapping(Executors.newSingleThreadExecutor()); + JdkHttpSender testSender = + new JdkHttpSender( + mockHttpClient, + URI.create("http://localhost"), + "text/plain", + null, + Duration.ofSeconds(10), + Collections::emptyMap, + null, + executor, + Long.MAX_VALUE); + + try { + CountDownLatch latch = new CountDownLatch(1); + + testSender.send( + new NoOpRequestBodyWriter(), response -> latch.countDown(), error -> latch.countDown()); + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(suppressed.get()).isTrue(); + } finally { + testSender.shutdown(); + executor.shutdownNow(); + } + } + private static class NoOpRequestBodyWriter implements MessageWriter { @Override public void writeMessage(OutputStream output) {}