Browse Source

Add library instrumentation for java http client (#8138)

Resolves
https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/8069
The javaagent instrumentation also supports propagating context into
[BodyHandler](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpResponse.BodyHandler.html)
implemented in
https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java
I think the initial idea behind it was that this allowed propagating
context into callbacks. Because this didn't work for
`connectionErrorUnopenedPortWithCallback` test later we also added
wrapping completable future to take care of propagating context into
callbacks. Should I also implement context propagation for `BodyHandler`
in library instrumentation or should I just delete it? I guess it could
come handy if someone builds a custom `BodyHandler` and wants to emit
spans from there, though this doesn't feel too likely. I'd like deleting
it more.

---------

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
Lauri Tulmin 1 year ago
parent
commit
08236a710f
27 changed files with 673 additions and 256 deletions
  1. 1 1
      docs/supported-libraries.md
  2. 5 0
      instrumentation/java-http-client/javaagent/build.gradle.kts
  3. 0 80
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java
  4. 5 8
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java
  5. 1 4
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentationModule.java
  6. 1 1
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java
  7. 48 0
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java
  8. 0 57
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientSingletons.java
  9. 0 28
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java
  10. 0 45
      instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/TrustedSubscriberInstrumentation.java
  11. 23 0
      instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java
  12. 54 0
      instrumentation/java-http-client/library/README.md
  13. 12 0
      instrumentation/java-http-client/library/build.gradle.kts
  14. 49 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetry.java
  15. 76 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java
  16. 5 1
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/CompletableFutureWrapper.java
  17. 11 12
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/HttpHeadersSetter.java
  18. 62 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/HttpRequestWrapper.java
  19. 8 2
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java
  20. 53 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java
  21. 6 2
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java
  22. 152 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/OpenTelemetryHttpClient.java
  23. 36 0
      instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/ResponseConsumer.java
  24. 30 0
      instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java
  25. 11 0
      instrumentation/java-http-client/testing/build.gradle.kts
  26. 22 15
      instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java
  27. 2 0
      settings.gradle.kts

+ 1 - 1
docs/supported-libraries.md

@@ -68,7 +68,7 @@ These are the supported libraries and frameworks:
 | [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html)                           | Java 8+                       | N/A                                                                                                                                                                                                                                                                                                                                                                                     | [HTTP Client Spans], [HTTP Client Metrics]                                             |
 | [Hystrix](https://github.com/Netflix/Hystrix)                                                                                               | 1.4+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | none                                                                                   |
 | [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html)                                              | Java 8+                       | N/A                                                                                                                                                                                                                                                                                                                                                                                     | Context propagation                                                                    |
-| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html)                     | Java 11+                      | N/A                                                                                                                                                                                                                                                                                                                                                                                     | [HTTP Client Spans], [HTTP Client Metrics]                                             |
+| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html)                     | Java 11+                      | [opentelemetry-java-http-client](../instrumentation/java-http-client/library)                                                                                                                                                                                                                                                                                                           | [HTTP Client Spans], [HTTP Client Metrics]                                             |
 | [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html)                                       | Java 8+                       | N/A                                                                                                                                                                                                                                                                                                                                                                                     | none                                                                                   |
 | [Java Platform](https://docs.oracle.com/javase/8/docs/api/java/lang/management/ManagementFactory.html)                                      | Java 8+                       | [opentelemetry-runtime-metrics](../instrumentation/runtime-metrics/library),<br>[opentelemetry-resources](../instrumentation/resources/library)                                                                                                                                                                                                                                         | [JVM Runtime Metrics]                                                                  |
 | [JAX-RS](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/package-summary.html)                                                    | 0.5+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | Provides `http.route` [2], Controller Spans [3]                                        |

+ 5 - 0
instrumentation/java-http-client/javaagent/build.gradle.kts

@@ -11,3 +11,8 @@ muzzle {
 otelJava {
   minJavaVersionSupported.set(JavaVersion.VERSION_11)
 }
+
+dependencies {
+  implementation(project(":instrumentation:java-http-client:library"))
+  testImplementation(project(":instrumentation:java-http-client:testing"))
+}

+ 0 - 80
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java

@@ -1,80 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.httpclient;
-
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import java.net.http.HttpResponse.BodyHandler;
-import java.net.http.HttpResponse.BodySubscriber;
-import java.net.http.HttpResponse.ResponseInfo;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.Flow;
-
-public class BodyHandlerWrapper<T> implements BodyHandler<T> {
-  private final BodyHandler<T> delegate;
-  private final Context context;
-
-  public BodyHandlerWrapper(BodyHandler<T> delegate, Context context) {
-    this.delegate = delegate;
-    this.context = context;
-  }
-
-  @Override
-  public BodySubscriber<T> apply(ResponseInfo responseInfo) {
-    BodySubscriber<T> subscriber = delegate.apply(responseInfo);
-    if (subscriber instanceof BodySubscriberWrapper) {
-      return subscriber;
-    }
-    return new BodySubscriberWrapper<>(subscriber, context);
-  }
-
-  public static class BodySubscriberWrapper<T> implements BodySubscriber<T> {
-    private final BodySubscriber<T> delegate;
-    private final Context context;
-
-    public BodySubscriberWrapper(BodySubscriber<T> delegate, Context context) {
-      this.delegate = delegate;
-      this.context = context;
-    }
-
-    public BodySubscriber<T> getDelegate() {
-      return delegate;
-    }
-
-    @Override
-    public CompletionStage<T> getBody() {
-      return delegate.getBody();
-    }
-
-    @Override
-    public void onSubscribe(Flow.Subscription subscription) {
-      delegate.onSubscribe(subscription);
-    }
-
-    @Override
-    public void onNext(List<ByteBuffer> item) {
-      try (Scope ignore = context.makeCurrent()) {
-        delegate.onNext(item);
-      }
-    }
-
-    @Override
-    public void onError(Throwable throwable) {
-      try (Scope ignore = context.makeCurrent()) {
-        delegate.onError(throwable);
-      }
-    }
-
-    @Override
-    public void onComplete() {
-      try (Scope ignore = context.makeCurrent()) {
-        delegate.onComplete();
-      }
-    }
-  }
-}

+ 5 - 8
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java

@@ -8,7 +8,7 @@ package io.opentelemetry.javaagent.instrumentation.httpclient;
 import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
 import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
 import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
-import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.instrumenter;
+import static io.opentelemetry.javaagent.instrumentation.httpclient.JavaHttpClientSingletons.instrumenter;
 import static net.bytebuddy.matcher.ElementMatchers.isMethod;
 import static net.bytebuddy.matcher.ElementMatchers.isPublic;
 import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
@@ -19,6 +19,8 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
 
 import io.opentelemetry.context.Context;
 import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.httpclient.internal.CompletableFutureWrapper;
+import io.opentelemetry.instrumentation.httpclient.internal.ResponseConsumer;
 import io.opentelemetry.javaagent.bootstrap.CallDepth;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@@ -59,8 +61,7 @@ public class HttpClientInstrumentation implements TypeInstrumentation {
         isMethod()
             .and(named("sendAsync"))
             .and(isPublic())
-            .and(takesArgument(0, named("java.net.http.HttpRequest")))
-            .and(takesArgument(1, named("java.net.http.HttpResponse$BodyHandler"))),
+            .and(takesArgument(0, named("java.net.http.HttpRequest"))),
         HttpClientInstrumentation.class.getName() + "$SendAsyncAdvice");
   }
 
@@ -103,7 +104,6 @@ public class HttpClientInstrumentation implements TypeInstrumentation {
     @Advice.OnMethodEnter(suppress = Throwable.class)
     public static void methodEnter(
         @Advice.Argument(value = 0) HttpRequest httpRequest,
-        @Advice.Argument(value = 1, readOnly = false) HttpResponse.BodyHandler<?> bodyHandler,
         @Advice.Local("otelCallDepth") CallDepth callDepth,
         @Advice.Local("otelContext") Context context,
         @Advice.Local("otelParentContext") Context parentContext,
@@ -114,9 +114,6 @@ public class HttpClientInstrumentation implements TypeInstrumentation {
       }
 
       parentContext = currentContext();
-      if (bodyHandler != null) {
-        bodyHandler = new BodyHandlerWrapper<>(bodyHandler, parentContext);
-      }
       if (!instrumenter().shouldStart(parentContext, httpRequest)) {
         return;
       }
@@ -146,7 +143,7 @@ public class HttpClientInstrumentation implements TypeInstrumentation {
       if (throwable != null) {
         instrumenter().end(context, httpRequest, null, throwable);
       } else {
-        future = future.whenComplete(new ResponseConsumer(context, httpRequest));
+        future = future.whenComplete(new ResponseConsumer(instrumenter(), context, httpRequest));
         future = CompletableFutureWrapper.wrap(future, parentContext);
       }
     }

+ 1 - 4
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentationModule.java

@@ -20,9 +20,6 @@ public class HttpClientInstrumentationModule extends InstrumentationModule {
 
   @Override
   public List<TypeInstrumentation> typeInstrumentations() {
-    return asList(
-        new HttpClientInstrumentation(),
-        new HttpHeadersInstrumentation(),
-        new TrustedSubscriberInstrumentation());
+    return asList(new HttpClientInstrumentation(), new HttpHeadersInstrumentation());
   }
 }

+ 1 - 1
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java

@@ -6,7 +6,7 @@
 package io.opentelemetry.javaagent.instrumentation.httpclient;
 
 import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
-import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.setter;
+import static io.opentelemetry.javaagent.instrumentation.httpclient.JavaHttpClientSingletons.setter;
 import static net.bytebuddy.matcher.ElementMatchers.isMethod;
 import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
 import static net.bytebuddy.matcher.ElementMatchers.named;

+ 48 - 0
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpclient;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor;
+import io.opentelemetry.instrumentation.httpclient.internal.HttpHeadersSetter;
+import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterFactory;
+import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientNetAttributesGetter;
+import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Arrays;
+
+public class JavaHttpClientSingletons {
+
+  private static final HttpHeadersSetter SETTER;
+  private static final Instrumenter<HttpRequest, HttpResponse<?>> INSTRUMENTER;
+
+  static {
+    SETTER = new HttpHeadersSetter(GlobalOpenTelemetry.getPropagators());
+
+    JavaHttpClientNetAttributesGetter netAttributesGetter = new JavaHttpClientNetAttributesGetter();
+
+    INSTRUMENTER =
+        JavaHttpClientInstrumenterFactory.createInstrumenter(
+            GlobalOpenTelemetry.get(),
+            CommonConfig.get().getClientRequestHeaders(),
+            CommonConfig.get().getClientResponseHeaders(),
+            Arrays.asList(
+                PeerServiceAttributesExtractor.create(
+                    netAttributesGetter, CommonConfig.get().getPeerServiceMapping())));
+  }
+
+  public static Instrumenter<HttpRequest, HttpResponse<?>> instrumenter() {
+    return INSTRUMENTER;
+  }
+
+  public static HttpHeadersSetter setter() {
+    return SETTER;
+  }
+
+  private JavaHttpClientSingletons() {}
+}

+ 0 - 57
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientSingletons.java

@@ -1,57 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.httpclient;
-
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor;
-import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class JdkHttpClientSingletons {
-
-  private static final HttpHeadersSetter SETTER;
-  private static final Instrumenter<HttpRequest, HttpResponse<?>> INSTRUMENTER;
-
-  static {
-    SETTER = new HttpHeadersSetter(GlobalOpenTelemetry.getPropagators());
-
-    JdkHttpAttributesGetter httpAttributesGetter = new JdkHttpAttributesGetter();
-    JdkHttpNetAttributesGetter netAttributesGetter = new JdkHttpNetAttributesGetter();
-
-    INSTRUMENTER =
-        Instrumenter.<HttpRequest, HttpResponse<?>>builder(
-                GlobalOpenTelemetry.get(),
-                "io.opentelemetry.java-http-client",
-                HttpSpanNameExtractor.create(httpAttributesGetter))
-            .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))
-            .addAttributesExtractor(
-                HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter)
-                    .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders())
-                    .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders())
-                    .build())
-            .addAttributesExtractor(
-                PeerServiceAttributesExtractor.create(
-                    netAttributesGetter, CommonConfig.get().getPeerServiceMapping()))
-            .addOperationMetrics(HttpClientMetrics.get())
-            .buildClientInstrumenter(SETTER);
-  }
-
-  public static Instrumenter<HttpRequest, HttpResponse<?>> instrumenter() {
-    return INSTRUMENTER;
-  }
-
-  public static HttpHeadersSetter setter() {
-    return SETTER;
-  }
-
-  private JdkHttpClientSingletons() {}
-}

+ 0 - 28
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java

@@ -1,28 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.httpclient;
-
-import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.instrumenter;
-
-import io.opentelemetry.context.Context;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.function.BiConsumer;
-
-public class ResponseConsumer implements BiConsumer<HttpResponse<?>, Throwable> {
-  private final Context context;
-  private final HttpRequest httpRequest;
-
-  public ResponseConsumer(Context context, HttpRequest httpRequest) {
-    this.context = context;
-    this.httpRequest = httpRequest;
-  }
-
-  @Override
-  public void accept(HttpResponse<?> httpResponse, Throwable throwable) {
-    instrumenter().end(context, httpRequest, httpResponse, throwable);
-  }
-}

+ 0 - 45
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/TrustedSubscriberInstrumentation.java

@@ -1,45 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.httpclient;
-
-import static net.bytebuddy.matcher.ElementMatchers.named;
-import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
-
-import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
-import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
-import io.opentelemetry.javaagent.instrumentation.httpclient.BodyHandlerWrapper.BodySubscriberWrapper;
-import java.net.http.HttpResponse;
-import net.bytebuddy.asm.Advice;
-import net.bytebuddy.description.type.TypeDescription;
-import net.bytebuddy.matcher.ElementMatcher;
-
-public class TrustedSubscriberInstrumentation implements TypeInstrumentation {
-  @Override
-  public ElementMatcher<TypeDescription> typeMatcher() {
-    return named("jdk.internal.net.http.ResponseSubscribers$TrustedSubscriber");
-  }
-
-  @Override
-  public void transform(TypeTransformer transformer) {
-    transformer.applyAdviceToMethod(
-        named("needsExecutor")
-            .and(takesArgument(0, named("java.net.http.HttpResponse$BodySubscriber"))),
-        TrustedSubscriberInstrumentation.class.getName() + "$NeedsExecutorAdvice");
-  }
-
-  @SuppressWarnings("unused")
-  public static class NeedsExecutorAdvice {
-
-    @Advice.OnMethodEnter(suppress = Throwable.class)
-    public static void methodEnter(
-        @Advice.Argument(value = 0, readOnly = false)
-            HttpResponse.BodySubscriber<?> bodySubscriber) {
-      if (bodySubscriber instanceof BodySubscriberWrapper) {
-        bodySubscriber = ((BodySubscriberWrapper<?>) bodySubscriber).getDelegate();
-      }
-    }
-  }
-}

+ 23 - 0
instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpclient;
+
+import io.opentelemetry.instrumentation.httpclient.AbstractJavaHttpClientTest;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
+import java.net.http.HttpClient;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class JavaHttpClientTest extends AbstractJavaHttpClientTest {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
+
+  @Override
+  protected HttpClient configureHttpClient(HttpClient httpClient) {
+    return httpClient;
+  }
+}

+ 54 - 0
instrumentation/java-http-client/library/README.md

@@ -0,0 +1,54 @@
+# Library Instrumentation for Java HTTP Client
+
+Provides OpenTelemetry instrumentation for [Java HTTP Client](https://openjdk.org/groups/net/httpclient/intro.html).
+
+## Quickstart
+
+### Add these dependencies to your project
+
+Replace `OPENTELEMETRY_VERSION` with the [latest
+release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-java-http-client).
+
+For Maven, add to your `pom.xml` dependencies:
+
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opentelemetry.instrumentation</groupId>
+    <artifactId>opentelemetry-java-http-client</artifactId>
+    <version>OPENTELEMETRY_VERSION</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle, add to your dependencies:
+
+```groovy
+implementation("io.opentelemetry.instrumentation:opentelemetry-java-http-client:OPENTELEMETRY_VERSION")
+```
+
+### Usage
+
+The instrumentation library contains an `HttpClient` wrapper that provides OpenTelemetry-based spans
+and context propagation.
+
+```java
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.httpclient.JavaHttpClientTelemetry;
+import java.net.http.HttpClient;
+
+import java.util.concurrent.ExecutorService;
+
+public class JavaHttpClientConfiguration {
+
+  //Use this HttpClient implementation for making standard http client calls.
+  public HttpClient createTracedClient(OpenTelemetry openTelemetry) {
+    return JavaHttpClientTelemetry.builder(openTelemetry).build().newHttpClient(createClient());
+  }
+
+  //your configuration of the Java HTTP Client goes here:
+  private HttpClient createClient() {
+    return HttpClient.newBuilder().build();
+  }
+}
+```

+ 12 - 0
instrumentation/java-http-client/library/build.gradle.kts

@@ -0,0 +1,12 @@
+plugins {
+  id("otel.library-instrumentation")
+  id("otel.nullaway-conventions")
+}
+
+otelJava {
+  minJavaVersionSupported.set(JavaVersion.VERSION_11)
+}
+
+dependencies {
+  testImplementation(project(":instrumentation:java-http-client:testing"))
+}

+ 49 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetry.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.httpclient.internal.HttpHeadersSetter;
+import io.opentelemetry.instrumentation.httpclient.internal.OpenTelemetryHttpClient;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+/** Entrypoint for instrumenting Java HTTP Client. */
+public final class JavaHttpClientTelemetry {
+
+  /**
+   * Returns a new {@link JavaHttpClientTelemetry} configured with the given {@link OpenTelemetry}.
+   */
+  public static JavaHttpClientTelemetry create(OpenTelemetry openTelemetry) {
+    return builder(openTelemetry).build();
+  }
+
+  public static JavaHttpClientTelemetryBuilder builder(OpenTelemetry openTelemetry) {
+    return new JavaHttpClientTelemetryBuilder(openTelemetry);
+  }
+
+  private final Instrumenter<HttpRequest, HttpResponse<?>> instrumenter;
+  private final HttpHeadersSetter headersSetter;
+
+  JavaHttpClientTelemetry(
+      Instrumenter<HttpRequest, HttpResponse<?>> instrumenter, HttpHeadersSetter headersSetter) {
+    this.instrumenter = instrumenter;
+    this.headersSetter = headersSetter;
+  }
+
+  /**
+   * Construct a new OpenTelemetry tracing-enabled {@link HttpClient} using the provided {@link
+   * HttpClient} instance.
+   *
+   * @param client An instance of HttpClient configured as desired.
+   * @return a tracing-enabled {@link HttpClient}.
+   */
+  public HttpClient newHttpClient(HttpClient client) {
+    return new OpenTelemetryHttpClient(client, instrumenter, headersSetter);
+  }
+}

+ 76 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient;
+
+import static java.util.Collections.emptyList;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.httpclient.internal.HttpHeadersSetter;
+import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterFactory;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class JavaHttpClientTelemetryBuilder {
+
+  private final OpenTelemetry openTelemetry;
+
+  private final List<AttributesExtractor<? super HttpRequest, ? super HttpResponse<?>>>
+      additionalExtractors = new ArrayList<>();
+
+  private List<String> capturedRequestHeaders = emptyList();
+  private List<String> capturedResponseHeaders = emptyList();
+
+  JavaHttpClientTelemetryBuilder(OpenTelemetry openTelemetry) {
+    this.openTelemetry = openTelemetry;
+  }
+
+  /**
+   * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
+   * items. The {@link AttributesExtractor} will be executed after all default extractors.
+   */
+  @CanIgnoreReturnValue
+  public JavaHttpClientTelemetryBuilder addAttributeExtractor(
+      AttributesExtractor<? super HttpRequest, ? super HttpResponse<?>> attributesExtractor) {
+    additionalExtractors.add(attributesExtractor);
+    return this;
+  }
+
+  /**
+   * Configures the HTTP client request headers that will be captured as span attributes.
+   *
+   * @param requestHeaders A list of HTTP header names.
+   */
+  @CanIgnoreReturnValue
+  public JavaHttpClientTelemetryBuilder setCapturedRequestHeaders(List<String> requestHeaders) {
+    capturedRequestHeaders = requestHeaders;
+    return this;
+  }
+
+  /**
+   * Configures the HTTP client response headers that will be captured as span attributes.
+   *
+   * @param responseHeaders A list of HTTP header names.
+   */
+  @CanIgnoreReturnValue
+  public JavaHttpClientTelemetryBuilder setCapturedResponseHeaders(List<String> responseHeaders) {
+    capturedResponseHeaders = responseHeaders;
+    return this;
+  }
+
+  public JavaHttpClientTelemetry build() {
+    Instrumenter<HttpRequest, HttpResponse<?>> instrumenter =
+        JavaHttpClientInstrumenterFactory.createInstrumenter(
+            openTelemetry, capturedRequestHeaders, capturedResponseHeaders, additionalExtractors);
+
+    return new JavaHttpClientTelemetry(
+        instrumenter, new HttpHeadersSetter(openTelemetry.getPropagators()));
+  }
+}

+ 5 - 1
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/CompletableFutureWrapper.java → instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/CompletableFutureWrapper.java

@@ -3,12 +3,16 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-package io.opentelemetry.javaagent.instrumentation.httpclient;
+package io.opentelemetry.instrumentation.httpclient.internal;
 
 import io.opentelemetry.context.Context;
 import io.opentelemetry.context.Scope;
 import java.util.concurrent.CompletableFuture;
 
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
 public final class CompletableFutureWrapper {
 
   private CompletableFutureWrapper() {}

+ 11 - 12
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersSetter.java → instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/HttpHeadersSetter.java

@@ -3,21 +3,21 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-package io.opentelemetry.javaagent.instrumentation.httpclient;
+package io.opentelemetry.instrumentation.httpclient.internal;
 
 import io.opentelemetry.context.Context;
 import io.opentelemetry.context.propagation.ContextPropagators;
-import io.opentelemetry.context.propagation.TextMapSetter;
 import java.net.http.HttpHeaders;
-import java.net.http.HttpRequest;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-// TODO should this class implement TextMapSetter at all?
-/** Context propagation is initiated via {@link HttpHeadersInstrumentation}. */
-public class HttpHeadersSetter implements TextMapSetter<HttpRequest> {
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class HttpHeadersSetter {
 
   private final ContextPropagators contextPropagators;
 
@@ -25,11 +25,6 @@ public class HttpHeadersSetter implements TextMapSetter<HttpRequest> {
     this.contextPropagators = contextPropagators;
   }
 
-  @Override
-  public void set(HttpRequest carrier, String key, String value) {
-    // Don't do anything because headers are immutable
-  }
-
   public HttpHeaders inject(HttpHeaders original) {
     Map<String, List<String>> headerMap = new HashMap<>(original.map());
 
@@ -38,7 +33,11 @@ public class HttpHeadersSetter implements TextMapSetter<HttpRequest> {
         .inject(
             Context.current(),
             headerMap,
-            (carrier, key, value) -> carrier.put(key, Collections.singletonList(value)));
+            (carrier, key, value) -> {
+              if (carrier != null) {
+                carrier.put(key, Collections.singletonList(value));
+              }
+            });
 
     return HttpHeaders.of(headerMap, (s, s2) -> true);
   }

+ 62 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/HttpRequestWrapper.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient.internal;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+final class HttpRequestWrapper extends HttpRequest {
+  private final HttpRequest request;
+  private final HttpHeaders headers;
+
+  HttpRequestWrapper(HttpRequest request, HttpHeaders headers) {
+    this.request = request;
+    this.headers = headers;
+  }
+
+  @Override
+  public Optional<BodyPublisher> bodyPublisher() {
+    return request.bodyPublisher();
+  }
+
+  @Override
+  public String method() {
+    return request.method();
+  }
+
+  @Override
+  public Optional<Duration> timeout() {
+    return request.timeout();
+  }
+
+  @Override
+  public boolean expectContinue() {
+    return request.expectContinue();
+  }
+
+  @Override
+  public URI uri() {
+    return request.uri();
+  }
+
+  @Override
+  public Optional<HttpClient.Version> version() {
+    return request.version();
+  }
+
+  @Override
+  public HttpHeaders headers() {
+    return headers;
+  }
+}

+ 8 - 2
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpAttributesGetter.java → instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-package io.opentelemetry.javaagent.instrumentation.httpclient;
+package io.opentelemetry.instrumentation.httpclient.internal;
 
 import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter;
 import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
@@ -13,7 +13,13 @@ import java.net.http.HttpResponse;
 import java.util.List;
 import javax.annotation.Nullable;
 
-class JdkHttpAttributesGetter implements HttpClientAttributesGetter<HttpRequest, HttpResponse<?>> {
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+enum JavaHttpClientAttributesGetter
+    implements HttpClientAttributesGetter<HttpRequest, HttpResponse<?>> {
+  INSTANCE;
 
   @Override
   public String getMethod(HttpRequest httpRequest) {

+ 53 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient.internal;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class JavaHttpClientInstrumenterFactory {
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.java-http-client";
+
+  public static Instrumenter<HttpRequest, HttpResponse<?>> createInstrumenter(
+      OpenTelemetry openTelemetry,
+      List<String> capturedRequestHeaders,
+      List<String> capturedResponseHeaders,
+      List<AttributesExtractor<? super HttpRequest, ? super HttpResponse<?>>>
+          additionalExtractors) {
+    JavaHttpClientAttributesGetter httpAttributesGetter = JavaHttpClientAttributesGetter.INSTANCE;
+
+    HttpClientAttributesExtractorBuilder<HttpRequest, HttpResponse<?>>
+        httpAttributesExtractorBuilder =
+            HttpClientAttributesExtractor.builder(
+                httpAttributesGetter, new JavaHttpClientNetAttributesGetter());
+    httpAttributesExtractorBuilder.setCapturedRequestHeaders(capturedRequestHeaders);
+    httpAttributesExtractorBuilder.setCapturedResponseHeaders(capturedResponseHeaders);
+
+    return Instrumenter.<HttpRequest, HttpResponse<?>>builder(
+            openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributesGetter))
+        .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))
+        .addAttributesExtractor(httpAttributesExtractorBuilder.build())
+        .addAttributesExtractors(additionalExtractors)
+        .addOperationMetrics(HttpClientMetrics.get())
+        .buildInstrumenter(SpanKindExtractor.alwaysClient());
+  }
+
+  private JavaHttpClientInstrumenterFactory() {}
+}

+ 6 - 2
instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpNetAttributesGetter.java → instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-package io.opentelemetry.javaagent.instrumentation.httpclient;
+package io.opentelemetry.instrumentation.httpclient.internal;
 
 import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter;
 import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
@@ -11,7 +11,11 @@ import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import javax.annotation.Nullable;
 
-public class JdkHttpNetAttributesGetter
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public class JavaHttpClientNetAttributesGetter
     implements NetClientAttributesGetter<HttpRequest, HttpResponse<?>> {
 
   @Override

+ 152 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/OpenTelemetryHttpClient.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient.internal;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class OpenTelemetryHttpClient extends HttpClient {
+  private final HttpClient client;
+  private final Instrumenter<HttpRequest, HttpResponse<?>> instrumenter;
+  private final HttpHeadersSetter headersSetter;
+
+  public OpenTelemetryHttpClient(
+      HttpClient client,
+      Instrumenter<HttpRequest, HttpResponse<?>> instrumenter,
+      HttpHeadersSetter headersSetter) {
+    this.client = client;
+    this.instrumenter = instrumenter;
+    this.headersSetter = headersSetter;
+  }
+
+  @Override
+  public Optional<CookieHandler> cookieHandler() {
+    return client.cookieHandler();
+  }
+
+  @Override
+  public Optional<Duration> connectTimeout() {
+    return client.connectTimeout();
+  }
+
+  @Override
+  public Redirect followRedirects() {
+    return client.followRedirects();
+  }
+
+  @Override
+  public Optional<ProxySelector> proxy() {
+    return client.proxy();
+  }
+
+  @Override
+  public SSLContext sslContext() {
+    return client.sslContext();
+  }
+
+  @Override
+  public SSLParameters sslParameters() {
+    return client.sslParameters();
+  }
+
+  @Override
+  public Optional<Authenticator> authenticator() {
+    return client.authenticator();
+  }
+
+  @Override
+  public Version version() {
+    return client.version();
+  }
+
+  @Override
+  public Optional<Executor> executor() {
+    return client.executor();
+  }
+
+  @Override
+  public <T> HttpResponse<T> send(
+      HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
+      throws IOException, InterruptedException {
+    Context parentContext = Context.current();
+    if (request == null || !instrumenter.shouldStart(parentContext, request)) {
+      return client.send(request, responseBodyHandler);
+    }
+
+    HttpResponse<T> response = null;
+    Throwable error = null;
+    Context context = instrumenter.start(parentContext, request);
+    try (Scope ignore = context.makeCurrent()) {
+      HttpRequestWrapper requestWrapper =
+          new HttpRequestWrapper(request, headersSetter.inject(request.headers()));
+
+      response = client.send(requestWrapper, responseBodyHandler);
+    } catch (Throwable throwable) {
+      error = throwable;
+      throw throwable;
+    } finally {
+      instrumenter.end(context, request, response, error);
+    }
+
+    return response;
+  }
+
+  @Override
+  public <T> CompletableFuture<HttpResponse<T>> sendAsync(
+      HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
+    return traceAsync(request, req -> client.sendAsync(req, responseBodyHandler));
+  }
+
+  @Override
+  public <T> CompletableFuture<HttpResponse<T>> sendAsync(
+      HttpRequest request,
+      HttpResponse.BodyHandler<T> responseBodyHandler,
+      HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
+    return traceAsync(
+        request, req -> client.sendAsync(req, responseBodyHandler, pushPromiseHandler));
+  }
+
+  private <T> CompletableFuture<HttpResponse<T>> traceAsync(
+      HttpRequest request, Function<HttpRequest, CompletableFuture<HttpResponse<T>>> action) {
+    Context parentContext = Context.current();
+    if (request == null || !instrumenter.shouldStart(parentContext, request)) {
+      return action.apply(request);
+    }
+
+    Context context = instrumenter.start(parentContext, request);
+    try (Scope ignore = context.makeCurrent()) {
+      HttpRequestWrapper requestWrapper =
+          new HttpRequestWrapper(request, headersSetter.inject(request.headers()));
+
+      CompletableFuture<HttpResponse<T>> future = action.apply(requestWrapper);
+      future = future.whenComplete(new ResponseConsumer(instrumenter, context, request));
+      future = CompletableFutureWrapper.wrap(future, parentContext);
+      return future;
+    } catch (Throwable throwable) {
+      instrumenter.end(context, request, null, throwable);
+      throw throwable;
+    }
+  }
+}

+ 36 - 0
instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/ResponseConsumer.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient.internal;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.function.BiConsumer;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public class ResponseConsumer implements BiConsumer<HttpResponse<?>, Throwable> {
+  private final Instrumenter<HttpRequest, HttpResponse<?>> instrumenter;
+  private final Context context;
+  private final HttpRequest httpRequest;
+
+  public ResponseConsumer(
+      Instrumenter<HttpRequest, HttpResponse<?>> instrumenter,
+      Context context,
+      HttpRequest httpRequest) {
+    this.instrumenter = instrumenter;
+    this.context = context;
+    this.httpRequest = httpRequest;
+  }
+
+  @Override
+  public void accept(HttpResponse<?> httpResponse, Throwable throwable) {
+    instrumenter.end(context, httpRequest, httpResponse, throwable);
+  }
+}

+ 30 - 0
instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.httpclient;
+
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
+import java.net.http.HttpClient;
+import java.util.Collections;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class JavaHttpClientTest extends AbstractJavaHttpClientTest {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary();
+
+  @Override
+  protected HttpClient configureHttpClient(HttpClient httpClient) {
+    return JavaHttpClientTelemetry.builder(testing.getOpenTelemetry())
+        .setCapturedRequestHeaders(
+            Collections.singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER))
+        .setCapturedResponseHeaders(
+            Collections.singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER))
+        .build()
+        .newHttpClient(httpClient);
+  }
+}

+ 11 - 0
instrumentation/java-http-client/testing/build.gradle.kts

@@ -0,0 +1,11 @@
+plugins {
+  id("otel.java-conventions")
+}
+
+otelJava {
+  minJavaVersionSupported.set(JavaVersion.VERSION_11)
+}
+
+dependencies {
+  api(project(":testing-common"))
+}

+ 22 - 15
instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTest.java → instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java

@@ -3,31 +3,35 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-package io.opentelemetry.javaagent.instrumentation.httpclient;
+package io.opentelemetry.instrumentation.httpclient;
 
-import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
 import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
-import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
 import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
 import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
+import java.io.IOException;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.util.Map;
-import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.BeforeAll;
 
-public class JdkHttpClientTest extends AbstractHttpClientTest<HttpRequest> {
+public abstract class AbstractJavaHttpClientTest extends AbstractHttpClientTest<HttpRequest> {
 
-  @RegisterExtension
-  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
+  private HttpClient client;
 
-  private static final HttpClient client =
-      HttpClient.newBuilder()
-          .version(HttpClient.Version.HTTP_1_1)
-          .connectTimeout(CONNECTION_TIMEOUT)
-          .followRedirects(HttpClient.Redirect.NORMAL)
-          .build();
+  @BeforeAll
+  void setUp() {
+    HttpClient httpClient =
+        HttpClient.newBuilder()
+            .version(HttpClient.Version.HTTP_1_1)
+            .connectTimeout(CONNECTION_TIMEOUT)
+            .followRedirects(HttpClient.Redirect.NORMAL)
+            .build();
+    client = configureHttpClient(httpClient);
+  }
+
+  protected abstract HttpClient configureHttpClient(HttpClient httpClient);
 
   @Override
   public HttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
@@ -42,7 +46,7 @@ public class JdkHttpClientTest extends AbstractHttpClientTest<HttpRequest> {
 
   @Override
   public int sendRequest(HttpRequest request, String method, URI uri, Map<String, String> headers)
-      throws Exception {
+      throws IOException, InterruptedException {
     return client.send(request, HttpResponse.BodyHandlers.ofString()).statusCode();
   }
 
@@ -59,8 +63,11 @@ public class JdkHttpClientTest extends AbstractHttpClientTest<HttpRequest> {
             (response, throwable) -> {
               if (throwable == null) {
                 httpClientResult.complete(response.statusCode());
-              } else {
+              } else if (throwable.getCause() != null) {
                 httpClientResult.complete(throwable.getCause());
+              } else {
+                httpClientResult.complete(
+                    new IllegalStateException("throwable.getCause() returned null", throwable));
               }
             });
   }

+ 2 - 0
settings.gradle.kts

@@ -248,6 +248,8 @@ hideFromDependabot(":instrumentation:hikaricp-3.0:testing")
 hideFromDependabot(":instrumentation:http-url-connection:javaagent")
 hideFromDependabot(":instrumentation:hystrix-1.4:javaagent")
 hideFromDependabot(":instrumentation:java-http-client:javaagent")
+hideFromDependabot(":instrumentation:java-http-client:library")
+hideFromDependabot(":instrumentation:java-http-client:testing")
 hideFromDependabot(":instrumentation:java-util-logging:javaagent")
 hideFromDependabot(":instrumentation:java-util-logging:shaded-stub-for-instrumenting")
 hideFromDependabot(":instrumentation:jaxrs:jaxrs-common:bootstrap")