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:
Anuraag Agrawal 2021-07-16 18:22:35 +09:00 committed by GitHub
parent 256e8f2a64
commit 04c070ccc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 228 additions and 0 deletions

View File

@ -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();
}
}

View File

@ -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();
});
}
}

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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(

View File

@ -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());
}

View File

@ -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;
}

View File

@ -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());
}