Add HTTPClientMetrics (#3598)
* Add HTTPClientMetrics * Update instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com> Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
parent
256e8f2a64
commit
04c070ccc1
|
@ -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 <a
|
||||
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-client">HTTP
|
||||
* client metrics</a>.
|
||||
*
|
||||
* <p>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<State> 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();
|
||||
}
|
||||
}
|
|
@ -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<MetricData> 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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue