diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java
new file mode 100644
index 0000000000..df83f717d3
--- /dev/null
+++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.http;
+
+import com.google.auto.value.AutoValue;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleValueRecorder;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.metrics.common.Labels;
+import io.opentelemetry.api.metrics.common.LabelsBuilder;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.annotations.UnstableApi;
+import io.opentelemetry.instrumentation.api.instrumenter.RequestListener;
+import io.opentelemetry.instrumentation.api.instrumenter.RequestMetrics;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link RequestListener} which keeps track of HTTP
+ * client metrics.
+ *
+ *
To use this class, you may need to add the {@code opentelemetry-api-metrics} artifact to your
+ * dependencies.
+ */
+@UnstableApi
+public final class HttpClientMetrics implements RequestListener {
+
+ private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1);
+
+ private static final ContextKey HTTP_CLIENT_REQUEST_METRICS_STATE =
+ ContextKey.named("http-client-request-metrics-state");
+
+ private static final Logger logger = LoggerFactory.getLogger(HttpClientMetrics.class);
+
+ /**
+ * Returns a {@link RequestMetrics} which can be used to enable recording of {@link
+ * HttpClientMetrics} on an {@link
+ * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}.
+ */
+ @UnstableApi
+ public static RequestMetrics get() {
+ return HttpClientMetrics::new;
+ }
+
+ private final DoubleValueRecorder duration;
+
+ private HttpClientMetrics(Meter meter) {
+ duration =
+ meter
+ .doubleValueRecorderBuilder("http.client.duration")
+ .setUnit("milliseconds")
+ .setDescription("The duration of the outbound HTTP request")
+ .build();
+ }
+
+ @Override
+ public Context start(Context context, Attributes requestAttributes) {
+ long startTimeNanos = System.nanoTime();
+ Labels durationLabels = durationLabels(requestAttributes);
+
+ return context.with(
+ HTTP_CLIENT_REQUEST_METRICS_STATE,
+ new AutoValue_HttpClientMetrics_State(durationLabels, startTimeNanos));
+ }
+
+ @Override
+ public void end(Context context, Attributes responseAttributes) {
+ State state = context.get(HTTP_CLIENT_REQUEST_METRICS_STATE);
+ if (state == null) {
+ logger.debug(
+ "No state present when ending context {}. Cannot record HTTP request metrics.", context);
+ return;
+ }
+ duration.record(
+ (System.nanoTime() - state.startTimeNanos()) / NANOS_PER_MS, state.durationLabels());
+ }
+
+ private static Labels durationLabels(Attributes attributes) {
+ LabelsBuilder labels = Labels.builder();
+ attributes.forEach(
+ (key, value) -> {
+ switch (key.getKey()) {
+ case "http.method":
+ case "http.host":
+ case "http.scheme":
+ case "http.flavor":
+ case "http.server_name":
+ case "net.host.name":
+ if (value instanceof String) {
+ labels.put(key.getKey(), (String) value);
+ }
+ break;
+ case "http.status_code":
+ case "net.host.port":
+ if (value instanceof Long) {
+ labels.put(key.getKey(), Long.toString((long) value));
+ }
+ break;
+ default:
+ // fall through
+ }
+ });
+ return labels.build();
+ }
+
+ @AutoValue
+ abstract static class State {
+
+ abstract Labels durationLabels();
+
+ abstract long startTimeNanos();
+ }
+}
diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java
new file mode 100644
index 0000000000..db01c19e97
--- /dev/null
+++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.http;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.RequestListener;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import java.util.Collection;
+import org.junit.jupiter.api.Test;
+
+class HttpClientMetricsTest {
+
+ @Test
+ void collectsMetrics() {
+ SdkMeterProvider meterProvider = SdkMeterProvider.builder().build();
+
+ RequestListener listener = HttpClientMetrics.get().create(meterProvider.get("test"));
+
+ Attributes requestAttributes =
+ Attributes.builder()
+ .put("http.method", "GET")
+ .put("http.host", "host")
+ .put("http.scheme", "https")
+ .put("net.host.name", "localhost")
+ .put("net.host.port", 1234)
+ .put("rpc.service", "unused")
+ .put("rpc.method", "unused")
+ .build();
+
+ // Currently ignored.
+ Attributes responseAttributes =
+ Attributes.builder()
+ .put("http.flavor", "2.0")
+ .put("http.server_name", "server")
+ .put("http.status_code", 200)
+ .build();
+
+ Context context1 = listener.start(Context.current(), requestAttributes);
+
+ Collection metrics = meterProvider.collectAllMetrics();
+ assertThat(metrics).isEmpty();
+
+ Context context2 = listener.start(Context.current(), requestAttributes);
+
+ metrics = meterProvider.collectAllMetrics();
+ assertThat(metrics).isEmpty();
+
+ listener.end(context1, responseAttributes);
+
+ metrics = meterProvider.collectAllMetrics();
+ assertThat(metrics).hasSize(1);
+ assertThat(metrics)
+ .anySatisfy(
+ metric -> {
+ assertThat(metric.getName()).isEqualTo("http.client.duration");
+ assertThat(metric.getDoubleSummaryData().getPoints()).hasSize(1);
+ DoubleSummaryPointData data =
+ metric.getDoubleSummaryData().getPoints().stream().findFirst().get();
+ assertThat(data.getAttributes().asMap())
+ .containsOnly(
+ entry(stringKey("http.host"), "host"),
+ entry(stringKey("http.method"), "GET"),
+ entry(stringKey("http.scheme"), "https"),
+ entry(stringKey("net.host.name"), "localhost"),
+ entry(stringKey("net.host.port"), "1234"));
+ assertThat(data.getPercentileValues()).isNotEmpty();
+ });
+
+ listener.end(context2, responseAttributes);
+
+ metrics = meterProvider.collectAllMetrics();
+ assertThat(metrics).hasSize(1);
+ assertThat(metrics)
+ .anySatisfy(
+ metric -> {
+ assertThat(metric.getName()).isEqualTo("http.client.duration");
+ assertThat(metric.getDoubleSummaryData().getPoints()).hasSize(1);
+ DoubleSummaryPointData data =
+ metric.getDoubleSummaryData().getPoints().stream().findFirst().get();
+ assertThat(data.getPercentileValues()).isNotEmpty();
+ });
+ }
+}
diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java
index 99153044ec..a2f74bdd40 100644
--- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java
+++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java
@@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
@@ -37,6 +38,7 @@ public final class ApacheHttpAsyncClientSingletons {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
}
diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java
index 847abf2726..b206be6795 100644
--- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java
+++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java
@@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
@@ -37,6 +38,7 @@ public final class ApacheHttpClientSingletons {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
}
diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java
index 95fca2cc9f..9b784a70c5 100644
--- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java
+++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java
@@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
@@ -37,6 +38,7 @@ public final class ApacheHttpClientSingletons {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
}
diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java
index 5ac88456c3..b7012b3aaa 100644
--- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java
+++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java
@@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
@@ -38,6 +39,7 @@ public final class ApacheHttpClientSingletons {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
}
diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java
index df1efc4ba5..881f9a0441 100644
--- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java
+++ b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java
@@ -15,6 +15,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
@@ -84,6 +85,7 @@ public final class ArmeriaTracingBuilder {
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractors(additionalExtractors));
+ clientInstrumenterBuilder.addRequestMetrics(HttpClientMetrics.get());
serverInstrumenterBuilder.addRequestMetrics(HttpServerMetrics.get());
return new ArmeriaTracing(
diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java
index ef5737b531..3a62bdb530 100644
--- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java
+++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java
@@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
@@ -38,6 +39,7 @@ public final class AsyncHttpClientSingletons {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
}
diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java
index 5ca1c4e953..e23ea7d266 100644
--- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java
+++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java
@@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+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.util.ArrayList;
@@ -54,6 +55,7 @@ public final class JettyClientInstrumenterBuilder {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractors(additionalExtractors)
+ .addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpHeaderSetter());
return instrumenter;
}
diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracingBuilder.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracingBuilder.java
index 35b54b7465..4a88d511cb 100644
--- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracingBuilder.java
+++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracingBuilder.java
@@ -10,6 +10,7 @@ import static io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtracto
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.http.HttpClientMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpNetAttributesExtractor;
@@ -54,6 +55,7 @@ public final class OkHttpTracingBuilder {
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractors(additionalExtractors)
+ .addRequestMetrics(HttpClientMetrics.get())
.newInstrumenter(alwaysClient());
return new OkHttpTracing(instrumenter, openTelemetry.getPropagators());
}