From fa641df3664a4ed2f08961755cd904f123b32071 Mon Sep 17 00:00:00 2001 From: yangtaoran Date: Sat, 11 Dec 2021 08:29:57 +0800 Subject: [PATCH] add metrics rpc conventions implement (#4838) * add metrics rpc conventions * handle format violations * handle format violations * resolve the comments suggestion * update RpcClientMetrics format * update RpcServerMetrics format * resolve time precision and cardinality issue * invoke buildServerFallbackView method * add RpcServerMetricsTest and RpcClientMetricsTest * server metrics attibutes remove net.perr* and add net.host * Update instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java * Update instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java Co-authored-by: Trask Stalnaker --- .../api/instrumenter/rpc/MetricsView.java | 115 +++++++++++++ .../instrumenter/rpc/RpcClientMetrics.java | 90 ++++++++++ .../instrumenter/rpc/RpcServerMetrics.java | 90 ++++++++++ .../rpc/RpcClientMetricsTest.java | 156 ++++++++++++++++++ .../rpc/RpcServerMetricsTest.java | 154 +++++++++++++++++ 5 files changed, 605 insertions(+) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java new file mode 100644 index 000000000000..dbf5bd7b5aed --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; + +// this is temporary, see +// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/3962#issuecomment-906606325 +@SuppressWarnings("rawtypes") +final class MetricsView { + + private static final Set alwaysInclude = buildAlwaysInclude(); + private static final Set clientView = buildClientView(); + private static final Set clientFallbackView = buildClientFallbackView(); + private static final Set serverView = buildServerView(); + private static final Set serverFallbackView = buildServerFallbackView(); + + private static Set buildAlwaysInclude() { + // the list of recommended metrics attributes is from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + Set view = new HashSet<>(); + view.add(SemanticAttributes.RPC_SYSTEM); + view.add(SemanticAttributes.RPC_SERVICE); + view.add(SemanticAttributes.RPC_METHOD); + return view; + } + + private static Set buildClientView() { + // the list of rpc client metrics attributes is from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + Set view = new HashSet<>(alwaysInclude); + view.add(SemanticAttributes.NET_PEER_NAME); + view.add(SemanticAttributes.NET_PEER_PORT); + view.add(SemanticAttributes.NET_TRANSPORT); + return view; + } + + private static Set buildClientFallbackView() { + // the list of rpc client metrics attributes is from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + Set view = new HashSet<>(alwaysInclude); + view.add(SemanticAttributes.NET_PEER_IP); + view.add(SemanticAttributes.NET_PEER_PORT); + view.add(SemanticAttributes.NET_TRANSPORT); + return view; + } + + private static Set buildServerView() { + // the list of rpc server metrics attributes is from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + Set view = new HashSet<>(alwaysInclude); + view.add(SemanticAttributes.NET_HOST_NAME); + view.add(SemanticAttributes.NET_TRANSPORT); + return view; + } + + private static Set buildServerFallbackView() { + // the list of rpc server metrics attributes is from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + Set view = new HashSet<>(alwaysInclude); + view.add(SemanticAttributes.NET_HOST_IP); + view.add(SemanticAttributes.NET_TRANSPORT); + return view; + } + + private static boolean containsAttribute( + AttributeKey key, Attributes startAttributes, Attributes endAttributes) { + return startAttributes.get(key) != null || endAttributes.get(key) != null; + } + + static Attributes applyClientView(Attributes startAttributes, Attributes endAttributes) { + Set fullSet = clientView; + if (!containsAttribute(SemanticAttributes.NET_PEER_NAME, startAttributes, endAttributes)) { + fullSet = clientFallbackView; + } + return applyView(fullSet, startAttributes, endAttributes); + } + + static Attributes applyServerView(Attributes startAttributes, Attributes endAttributes) { + Set fullSet = serverView; + if (!containsAttribute(SemanticAttributes.NET_HOST_NAME, startAttributes, endAttributes)) { + fullSet = serverFallbackView; + } + return applyView(fullSet, startAttributes, endAttributes); + } + + static Attributes applyView( + Set view, Attributes startAttributes, Attributes endAttributes) { + AttributesBuilder filtered = Attributes.builder(); + applyView(filtered, startAttributes, view); + applyView(filtered, endAttributes, view); + return filtered.build(); + } + + @SuppressWarnings("unchecked") + private static void applyView( + AttributesBuilder filtered, Attributes attributes, Set view) { + attributes.forEach( + (BiConsumer) + (key, value) -> { + if (view.contains(key)) { + filtered.put(key, value); + } + }); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java new file mode 100644 index 000000000000..ff3c1e1a8a96 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static io.opentelemetry.instrumentation.api.instrumenter.rpc.MetricsView.applyClientView; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +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 RPC + * client metrics. + * + *

To use this class, you may need to add the {@code opentelemetry-api-metrics} artifact to your + * dependencies. + */ +@UnstableApi +public final class RpcClientMetrics implements RequestListener { + + private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); + + private static final ContextKey RPC_CLIENT_REQUEST_METRICS_STATE = + ContextKey.named("rpc-client-request-metrics-state"); + + private static final Logger logger = LoggerFactory.getLogger(RpcClientMetrics.class); + + private final DoubleHistogram clientDurationHistogram; + + private RpcClientMetrics(Meter meter) { + clientDurationHistogram = + meter + .histogramBuilder("rpc.client.duration") + .setDescription("The duration of an outbound RPC invocation") + .setUnit("milliseconds") + .build(); + } + + /** + * Returns a {@link RequestMetrics} which can be used to enable recording of {@link + * RpcClientMetrics} on an {@link + * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. + */ + @UnstableApi + public static RequestMetrics get() { + return RpcClientMetrics::new; + } + + @Override + public Context start(Context context, Attributes startAttributes, long startNanos) { + return context.with( + RPC_CLIENT_REQUEST_METRICS_STATE, + new AutoValue_RpcClientMetrics_State(startAttributes, startNanos)); + } + + @Override + public void end(Context context, Attributes endAttributes, long endNanos) { + State state = context.get(RPC_CLIENT_REQUEST_METRICS_STATE); + if (state == null) { + logger.debug( + "No state present when ending context {}. Cannot record RPC request metrics.", context); + return; + } + clientDurationHistogram.record( + (endNanos - state.startTimeNanos()) / NANOS_PER_MS, + applyClientView(state.startAttributes(), endAttributes), + context); + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java new file mode 100644 index 000000000000..5572a15dff27 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static io.opentelemetry.instrumentation.api.instrumenter.rpc.MetricsView.applyServerView; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +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 RPC + * server metrics. + * + *

To use this class, you may need to add the {@code opentelemetry-api-metrics} artifact to your + * dependencies. + */ +@UnstableApi +public final class RpcServerMetrics implements RequestListener { + + private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); + + private static final ContextKey RPC_SERVER_REQUEST_METRICS_STATE = + ContextKey.named("rpc-server-request-metrics-state"); + + private static final Logger logger = LoggerFactory.getLogger(RpcServerMetrics.class); + + private final DoubleHistogram serverDurationHistogram; + + private RpcServerMetrics(Meter meter) { + serverDurationHistogram = + meter + .histogramBuilder("rpc.server.duration") + .setDescription("The duration of an inbound RPC invocation") + .setUnit("milliseconds") + .build(); + } + + /** + * Returns a {@link RequestMetrics} which can be used to enable recording of {@link + * RpcServerMetrics} on an {@link + * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. + */ + @UnstableApi + public static RequestMetrics get() { + return RpcServerMetrics::new; + } + + @Override + public Context start(Context context, Attributes startAttributes, long startNanos) { + return context.with( + RPC_SERVER_REQUEST_METRICS_STATE, + new AutoValue_RpcServerMetrics_State(startAttributes, startNanos)); + } + + @Override + public void end(Context context, Attributes endAttributes, long endNanos) { + State state = context.get(RPC_SERVER_REQUEST_METRICS_STATE); + if (state == null) { + logger.debug( + "No state present when ending context {}. Cannot record RPC request metrics.", context); + return; + } + serverDurationHistogram.record( + (endNanos - state.startTimeNanos()) / NANOS_PER_MS, + applyServerView(state.startAttributes(), endAttributes), + context); + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java new file mode 100644 index 000000000000..3edb0a0312b0 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.RequestListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class RpcClientMetricsTest { + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = new InMemoryMetricReader(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + RequestListener listener = RpcClientMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(SemanticAttributes.RPC_SYSTEM, "grpc") + .put(SemanticAttributes.RPC_SERVICE, "myservice.EchoService") + .put(SemanticAttributes.RPC_METHOD, "exampleMethod") + .build(); + + Attributes responseAttributes1 = + Attributes.builder() + .put(SemanticAttributes.NET_PEER_NAME, "example.com") + .put(SemanticAttributes.NET_PEER_IP, "127.0.0.1") + .put(SemanticAttributes.NET_PEER_PORT, 8080) + .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .build(); + + Attributes responseAttributes2 = + Attributes.builder() + .put(SemanticAttributes.NET_PEER_IP, "127.0.0.1") + .put(SemanticAttributes.NET_PEER_PORT, 8080) + .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .build(); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Context context1 = listener.start(parent, requestAttributes, nanos(100)); + + // TODO(anuraaga): Remove await from this file after 1.8.0 hopefully fixes + // https://github.com/open-telemetry/opentelemetry-java/issues/3725 + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isEmpty(); + }); + + Context context2 = listener.start(Context.root(), requestAttributes, nanos(150)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isEmpty(); + }); + + listener.end(context1, responseAttributes1, nanos(250)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> + MetricAssertions.assertThat(metric) + .hasName("rpc.client.duration") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> { + MetricAssertions.assertThat(point) + .hasSum(150 /* millis */) + .attributes() + .containsOnly( + attributeEntry("rpc.system", "grpc"), + attributeEntry("rpc.service", "myservice.EchoService"), + attributeEntry("rpc.method", "exampleMethod"), + attributeEntry("net.peer.name", "example.com"), + attributeEntry("net.peer.port", 8080), + attributeEntry("net.transport", "ip_tcp")); + MetricAssertions.assertThat(point).exemplars().hasSize(1); + MetricAssertions.assertThat(point.getExemplars().get(0)) + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00"); + })); + }); + + listener.end(context2, responseAttributes2, nanos(300)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> + MetricAssertions.assertThat(metric) + .hasName("rpc.client.duration") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> { + MetricAssertions.assertThat(point) + .hasSum(150 /* millis */) + .attributes() + .containsOnly( + attributeEntry("rpc.system", "grpc"), + attributeEntry("rpc.service", "myservice.EchoService"), + attributeEntry("rpc.method", "exampleMethod"), + attributeEntry("net.peer.ip", "127.0.0.1"), + attributeEntry("net.peer.port", 8080), + attributeEntry("net.transport", "ip_tcp")); + })); + }); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java new file mode 100644 index 000000000000..9dc415c8cab4 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.RequestListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class RpcServerMetricsTest { + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = new InMemoryMetricReader(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + RequestListener listener = RpcServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(SemanticAttributes.RPC_SYSTEM, "grpc") + .put(SemanticAttributes.RPC_SERVICE, "myservice.EchoService") + .put(SemanticAttributes.RPC_METHOD, "exampleMethod") + .build(); + + Attributes responseAttributes1 = + Attributes.builder() + .put(SemanticAttributes.NET_HOST_NAME, "example.com") + .put(SemanticAttributes.NET_HOST_IP, "127.0.0.1") + .put(SemanticAttributes.NET_HOST_PORT, 8080) + .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .build(); + + Attributes responseAttributes2 = + Attributes.builder() + .put(SemanticAttributes.NET_HOST_IP, "127.0.0.1") + .put(SemanticAttributes.NET_HOST_PORT, 8080) + .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .build(); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Context context1 = listener.start(parent, requestAttributes, nanos(100)); + + // TODO(anuraaga): Remove await from this file after 1.8.0 hopefully fixes + // https://github.com/open-telemetry/opentelemetry-java/issues/3725 + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isEmpty(); + }); + + Context context2 = listener.start(Context.root(), requestAttributes, nanos(150)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isEmpty(); + }); + + listener.end(context1, responseAttributes1, nanos(250)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> + MetricAssertions.assertThat(metric) + .hasName("rpc.server.duration") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> { + MetricAssertions.assertThat(point) + .hasSum(150 /* millis */) + .attributes() + .containsOnly( + attributeEntry("rpc.system", "grpc"), + attributeEntry("rpc.service", "myservice.EchoService"), + attributeEntry("rpc.method", "exampleMethod"), + attributeEntry("net.host.name", "example.com"), + attributeEntry("net.transport", "ip_tcp")); + MetricAssertions.assertThat(point).exemplars().hasSize(1); + MetricAssertions.assertThat(point.getExemplars().get(0)) + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00"); + })); + }); + + listener.end(context2, responseAttributes2, nanos(300)); + + await() + .untilAsserted( + () -> { + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> + MetricAssertions.assertThat(metric) + .hasName("rpc.server.duration") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> { + MetricAssertions.assertThat(point) + .hasSum(150 /* millis */) + .attributes() + .containsOnly( + attributeEntry("rpc.system", "grpc"), + attributeEntry("rpc.service", "myservice.EchoService"), + attributeEntry("rpc.method", "exampleMethod"), + attributeEntry("net.host.ip", "127.0.0.1"), + attributeEntry("net.transport", "ip_tcp")); + })); + }); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +}