diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java index 215662e457..8386312ef0 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java @@ -5,11 +5,12 @@ package io.opentelemetry.instrumentation.api.instrumenter.http; +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyClientDurationAndSizeView; import static java.util.logging.Level.FINE; import com.google.auto.value.AutoValue; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; @@ -19,14 +20,12 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import javax.annotation.Nullable; /** * {@link OperationListener} which keeps track of HTTP + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-client">HTTP * client metrics. */ public final class HttpClientMetrics implements OperationListener { @@ -92,35 +91,21 @@ public final class HttpClientMetrics implements OperationListener { context); return; } + Attributes durationAndSizeAttributes = applyClientDurationAndSizeView(state.startAttributes(), endAttributes); duration.record( (endNanos - state.startTimeNanos()) / NANOS_PER_S, durationAndSizeAttributes, context); - Long requestLength = - getAttribute( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, endAttributes, state.startAttributes()); - if (requestLength != null) { - requestSize.record(requestLength, durationAndSizeAttributes, context); - } - Long responseLength = - getAttribute( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - endAttributes, - state.startAttributes()); - if (responseLength != null) { - responseSize.record(responseLength, durationAndSizeAttributes, context); - } - } - @Nullable - private static T getAttribute(AttributeKey key, Attributes... attributesList) { - for (Attributes attributes : attributesList) { - T value = attributes.get(key); - if (value != null) { - return value; - } + Long requestBodySize = getHttpRequestBodySize(endAttributes, state.startAttributes()); + if (requestBodySize != null) { + requestSize.record(requestBodySize, durationAndSizeAttributes, context); + } + + Long responseBodySize = getHttpResponseBodySize(endAttributes, state.startAttributes()); + if (responseBodySize != null) { + responseSize.record(responseBodySize, durationAndSizeAttributes, context); } - return null; } @AutoValue diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java new file mode 100644 index 0000000000..73b9c0d7e3 --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import javax.annotation.Nullable; + +final class HttpMessageBodySizeUtil { + + private static final AttributeKey HTTP_REQUEST_BODY_SIZE = + SemconvStability.emitOldHttpSemconv() + ? SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH + : HttpAttributes.HTTP_REQUEST_BODY_SIZE; + + private static final AttributeKey HTTP_RESPONSE_BODY_SIZE = + SemconvStability.emitOldHttpSemconv() + ? SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH + : HttpAttributes.HTTP_RESPONSE_BODY_SIZE; + + @Nullable + static Long getHttpRequestBodySize(Attributes... attributesList) { + return getAttribute(HTTP_REQUEST_BODY_SIZE, attributesList); + } + + @Nullable + static Long getHttpResponseBodySize(Attributes... attributesList) { + return getAttribute(HTTP_RESPONSE_BODY_SIZE, attributesList); + } + + @Nullable + private static T getAttribute(AttributeKey key, Attributes... attributesList) { + for (Attributes attributes : attributesList) { + T value = attributes.get(key); + if (value != null) { + return value; + } + } + return null; + } + + private HttpMessageBodySizeUtil() {} +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java index c99e4ac70e..e9494374c0 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java @@ -5,12 +5,13 @@ package io.opentelemetry.instrumentation.api.instrumenter.http; +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyActiveRequestsView; import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyServerDurationAndSizeView; import static java.util.logging.Level.FINE; import com.google.auto.value.AutoValue; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; @@ -21,14 +22,12 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import javax.annotation.Nullable; /** * {@link OperationListener} which keeps track of HTTP + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server">HTTP * server metrics. */ public final class HttpServerMetrics implements OperationListener { @@ -107,35 +106,21 @@ public final class HttpServerMetrics implements OperationListener { // it's important to use exactly the same attributes that were used when incrementing the active // request count (otherwise it will split the timeseries) activeRequests.add(-1, applyActiveRequestsView(state.startAttributes()), context); + Attributes durationAndSizeAttributes = applyServerDurationAndSizeView(state.startAttributes(), endAttributes); duration.record( (endNanos - state.startTimeNanos()) / NANOS_PER_S, durationAndSizeAttributes, context); - Long requestLength = - getAttribute( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, endAttributes, state.startAttributes()); - if (requestLength != null) { - requestSize.record(requestLength, durationAndSizeAttributes, context); - } - Long responseLength = - getAttribute( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - endAttributes, - state.startAttributes()); - if (responseLength != null) { - responseSize.record(responseLength, durationAndSizeAttributes, context); - } - } - @Nullable - private static T getAttribute(AttributeKey key, Attributes... attributesList) { - for (Attributes attributes : attributesList) { - T value = attributes.get(key); - if (value != null) { - return value; - } + Long requestBodySize = getHttpRequestBodySize(endAttributes, state.startAttributes()); + if (requestBodySize != null) { + requestSize.record(requestBodySize, durationAndSizeAttributes, context); + } + + Long responseBodySize = getHttpResponseBodySize(endAttributes, state.startAttributes()); + if (responseBodySize != null) { + responseSize.record(responseBodySize, durationAndSizeAttributes, context); } - return null; } @AutoValue diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java index bc89025ab2..b340e9871f 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java @@ -9,6 +9,8 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.HashSet; import java.util.Set; @@ -32,6 +34,13 @@ final class TemporaryMetricsView { view.add(SemanticAttributes.HTTP_STATUS_CODE); // Optional view.add(NetAttributes.NET_PROTOCOL_NAME); // Optional view.add(NetAttributes.NET_PROTOCOL_VERSION); // Optional + // stable semconv + view.add(HttpAttributes.HTTP_REQUEST_METHOD); + view.add(HttpAttributes.HTTP_RESPONSE_STATUS_CODE); + view.add(NetworkAttributes.NETWORK_PROTOCOL_NAME); + view.add(NetworkAttributes.NETWORK_PROTOCOL_VERSION); + view.add(NetworkAttributes.SERVER_ADDRESS); + view.add(NetworkAttributes.SERVER_PORT); return view; } @@ -43,6 +52,8 @@ final class TemporaryMetricsView { view.add(SemanticAttributes.NET_PEER_NAME); view.add(SemanticAttributes.NET_PEER_PORT); view.add(SemanticAttributes.NET_SOCK_PEER_ADDR); + // stable semconv + view.add(NetworkAttributes.SERVER_SOCKET_ADDRESS); return view; } @@ -57,6 +68,8 @@ final class TemporaryMetricsView { view.add(SemanticAttributes.NET_HOST_NAME); view.add(SemanticAttributes.NET_HOST_PORT); view.add(SemanticAttributes.HTTP_ROUTE); + // stable semconv + view.add(UrlAttributes.URL_SCHEME); return view; } @@ -68,6 +81,11 @@ final class TemporaryMetricsView { view.add(SemanticAttributes.HTTP_SCHEME); view.add(SemanticAttributes.NET_HOST_NAME); view.add(SemanticAttributes.NET_HOST_PORT); + // stable semconv + view.add(HttpAttributes.HTTP_REQUEST_METHOD); + view.add(NetworkAttributes.SERVER_ADDRESS); + view.add(NetworkAttributes.SERVER_PORT); + view.add(UrlAttributes.URL_SCHEME); return view; } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java index 46ea3df859..1d8c57a30c 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java @@ -40,5 +40,14 @@ public final class NetworkAttributes { public static final AttributeKey SERVER_SOCKET_PORT = longKey("server.socket.port"); + public static final AttributeKey CLIENT_ADDRESS = stringKey("client.address"); + + public static final AttributeKey CLIENT_PORT = longKey("client.port"); + + public static final AttributeKey CLIENT_SOCKET_ADDRESS = + stringKey("client.socket.address"); + + public static final AttributeKey CLIENT_SOCKET_PORT = longKey("client.socket.port"); + private NetworkAttributes() {} } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java index b1f4078d8a..a5968c9137 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java +++ b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java @@ -14,6 +14,8 @@ import static org.assertj.core.api.Assertions.entry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import org.junit.jupiter.api.Test; @@ -56,6 +58,43 @@ class TemporaryMetricsViewTest { entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")); } + @Test + void shouldApplyClientDurationAndSizeView_stableSemconv() { + Attributes startAttributes = + Attributes.builder() + .put( + UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") + .put(UrlAttributes.URL_QUERY, "jsessionId=121454") + .put(NetworkAttributes.SERVER_ADDRESS, "somehost2") + .put(NetworkAttributes.SERVER_PORT, 443) + .build(); + + Attributes endAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500) + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") + .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.SERVER_SOCKET_DOMAIN, "somehost20") + .put(NetworkAttributes.SERVER_SOCKET_PORT, 8080) + .build(); + + assertThat(applyClientDurationAndSizeView(startAttributes, endAttributes)) + .containsOnly( + entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500L), + entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + entry(NetworkAttributes.SERVER_ADDRESS, "somehost2"), + entry(NetworkAttributes.SERVER_PORT, 443L), + entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")); + } + @Test void shouldApplyServerDurationAndSizeView() { Attributes startAttributes = @@ -98,6 +137,48 @@ class TemporaryMetricsViewTest { entry(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}")); } + @Test + void shouldApplyServerDurationAndSizeView_stableSemconv() { + Attributes startAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put( + UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") + .put(UrlAttributes.URL_QUERY, "jsessionId=121454") + .put(NetworkAttributes.SERVER_ADDRESS, "somehost") + .put(NetworkAttributes.SERVER_PORT, 443) + .put(NetworkAttributes.CLIENT_ADDRESS, "somehost2") + .put(NetworkAttributes.CLIENT_PORT, 443) + .build(); + + Attributes endAttributes = + Attributes.builder() + .put(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}") + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500) + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") + .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") + .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) + .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) + .build(); + + assertThat(applyServerDurationAndSizeView(startAttributes, endAttributes)) + .containsOnly( + entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500L), + entry(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}"), + entry(UrlAttributes.URL_SCHEME, "https"), + entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + entry(NetworkAttributes.SERVER_ADDRESS, "somehost"), + entry(NetworkAttributes.SERVER_PORT, 443L)); + } + @Test void shouldApplyActiveRequestsView() { Attributes attributes = @@ -127,4 +208,34 @@ class TemporaryMetricsViewTest { entry(SemanticAttributes.NET_HOST_NAME, "somehost"), entry(SemanticAttributes.NET_HOST_PORT, 443L)); } + + @Test + void shouldApplyActiveRequestsView_stableSemconv() { + Attributes attributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put( + UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") + .put(UrlAttributes.URL_QUERY, "jsessionId=121454") + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") + .put(NetworkAttributes.SERVER_ADDRESS, "somehost") + .put(NetworkAttributes.SERVER_PORT, 443) + .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") + .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) + .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) + .build(); + + assertThat(applyActiveRequestsView(attributes)) + .containsOnly( + entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + entry(UrlAttributes.URL_SCHEME, "https"), + entry(NetworkAttributes.SERVER_ADDRESS, "somehost"), + entry(NetworkAttributes.SERVER_PORT, 443L)); + } } diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java new file mode 100644 index 0000000000..783dbe35f6 --- /dev/null +++ b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +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.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class HttpClientMetricsStableSemconvTest { + + static final double[] DURATION_BUCKETS = + HistogramAdviceUtil.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpClientMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put(UrlAttributes.URL_FULL, "https://localhost:1234/") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/") + .put(UrlAttributes.URL_QUERY, "q=a") + .put(NetworkAttributes.SERVER_ADDRESS, "localhost") + .put(NetworkAttributes.SERVER_PORT, 1234) + .build(); + + Attributes responseAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .put(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") + .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.SERVER_SOCKET_DOMAIN, "somehost20") + .put(NetworkAttributes.SERVER_SOCKET_PORT, 8080) + .build(); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Context context1 = listener.onStart(parent, requestAttributes, nanos(100)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + Context context2 = listener.onStart(Context.root(), requestAttributes, nanos(150)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + listener.onEnd(context1, responseAttributes, nanos(250)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.client.duration") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15 /* seconds */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234), + equalTo( + NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00")) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("http.client.request.size") + .hasUnit("By") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(100 /* bytes */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234), + equalTo( + NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00")))), + metric -> + assertThat(metric) + .hasName("http.client.response.size") + .hasUnit("By") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(200 /* bytes */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234), + equalTo( + NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00"))))); + + listener.onEnd(context2, responseAttributes, nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.client.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasSum(0.3 /* seconds */))), + metric -> + assertThat(metric) + .hasName("http.client.request.size") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying(point -> point.hasSum(200 /* bytes */))), + metric -> + assertThat(metric) + .hasName("http.client.response.size") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying(point -> point.hasSum(400 /* bytes */)))); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java new file mode 100644 index 0000000000..768ec1ead5 --- /dev/null +++ b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java @@ -0,0 +1,339 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +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.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class HttpServerMetricsStableSemconvTest { + + static final double[] DURATION_BUCKETS = + HistogramAdviceUtil.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/") + .put(UrlAttributes.URL_QUERY, "q=a") + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") + .put(NetworkAttributes.SERVER_ADDRESS, "localhost") + .put(NetworkAttributes.SERVER_PORT, 1234) + .build(); + + Attributes responseAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .put(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) + .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") + .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) + .build(); + + SpanContext spanContext1 = + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()); + SpanContext spanContext2 = + SpanContext.create( + "123456789abcdef00000000000999999", + "abcde00000054321", + TraceFlags.getSampled(), + TraceState.getDefault()); + + Context parent1 = Context.root().with(Span.wrap(spanContext1)); + Context context1 = listener.onStart(parent1, requestAttributes, nanos(100)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.active_requests") + .hasDescription( + "The number of concurrent HTTP requests that are currently in-flight") + .hasUnit("{requests}") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId()))))); + + Context parent2 = Context.root().with(Span.wrap(spanContext2)); + Context context2 = listener.onStart(parent2, requestAttributes, nanos(150)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.active_requests") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(2) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId()))))); + + listener.onEnd(context1, responseAttributes, nanos(250)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.active_requests") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId())))), + metric -> + assertThat(metric) + .hasName("http.server.duration") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15 /* seconds */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId())) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("http.server.request.size") + .hasUnit("By") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(100 /* bytes */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId())))), + metric -> + assertThat(metric) + .hasName("http.server.response.size") + .hasUnit("By") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(200 /* bytes */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), + equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId()))))); + + listener.onEnd(context2, responseAttributes, nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.active_requests") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(0) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId())))), + metric -> + assertThat(metric) + .hasName("http.server.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.3 /* seconds */) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId())))), + metric -> + assertThat(metric) + .hasName("http.server.request.size") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(200 /* bytes */) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId())))), + metric -> + assertThat(metric) + .hasName("http.server.response.size") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(400 /* bytes */) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId()))))); + } + + @Test + void collectsHttpRouteFromEndAttributes() { + // given + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(NetworkAttributes.SERVER_ADDRESS, "host") + .put(UrlAttributes.URL_SCHEME, "https") + .build(); + + Attributes responseAttributes = + Attributes.builder().put(SemanticAttributes.HTTP_ROUTE, "/test/{id}").build(); + + Context parentContext = Context.root(); + + // when + Context context = listener.onStart(parentContext, requestAttributes, nanos(100)); + listener.onEnd(context, responseAttributes, nanos(200)); + + // then + assertThat(metricReader.collectAllMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("http.server.duration") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.100 /* seconds */) + .hasAttributesSatisfying( + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(NetworkAttributes.SERVER_ADDRESS, "host"), + equalTo( + SemanticAttributes.HTTP_ROUTE, "/test/{id}"))))); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +}