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);
+ }
+}