diff --git a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationExtension.java b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationExtension.java
index b55b0857..85ec89ee 100644
--- a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationExtension.java
+++ b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationExtension.java
@@ -20,15 +20,18 @@
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.util.Nonbinding;
-import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.annotation.Observed;
+import io.smallrye.opentelemetry.instrumentation.observation.ObservationRegistryProducer;
+import io.smallrye.opentelemetry.instrumentation.observation.handler.OpenTelemetryObservationHandler;
public class ObservationExtension implements Extension {
public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery beforeBeanDiscovery, BeanManager beanManager) {
beforeBeanDiscovery.addInterceptorBinding(
new ObservedAnnotatedType(beanManager.createAnnotatedType(Observed.class)));
- beforeBeanDiscovery.addAnnotatedType(ObservationRegistry.class, ObservationRegistry.class.getName());
+ beforeBeanDiscovery.addAnnotatedType(OpenTelemetryObservationHandler.class,
+ OpenTelemetryObservationHandler.class.getName());
+ beforeBeanDiscovery.addAnnotatedType(ObservationRegistryProducer.class, ObservationRegistryProducer.class.getName());
}
public void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) {
diff --git a/implementation/observation-otel-bridge/src/test/java/io/smallrye/opentelemetry/implementation/observation/ObservationOTelTest.java b/implementation/observation-otel-bridge/src/test/java/io/smallrye/opentelemetry/implementation/observation/ObservationOTelTest.java
index 941f28a6..4d0e5e57 100644
--- a/implementation/observation-otel-bridge/src/test/java/io/smallrye/opentelemetry/implementation/observation/ObservationOTelTest.java
+++ b/implementation/observation-otel-bridge/src/test/java/io/smallrye/opentelemetry/implementation/observation/ObservationOTelTest.java
@@ -38,7 +38,6 @@
import io.smallrye.opentelemetry.implementation.cdi.OpenTelemetryExtension;
import io.smallrye.opentelemetry.implementation.config.OpenTelemetryConfigProducer;
import io.smallrye.opentelemetry.implementation.micrometer.cdi.MicrometerExtension;
-import io.smallrye.opentelemetry.instrumentation.observation.ObservationRegistryProducer;
import io.smallrye.opentelemetry.instrumentation.observation.cdi.ObservationExtension;
import io.smallrye.opentelemetry.instrumentation.observation.handler.OpenTelemetryObservationHandler;
import io.smallrye.opentelemetry.test.InMemoryExporter;
diff --git a/implementation/rest-observation/pom.xml b/implementation/rest-observation/pom.xml
index 1168b55b..8fb47be6 100644
--- a/implementation/rest-observation/pom.xml
+++ b/implementation/rest-observation/pom.xml
@@ -16,7 +16,10 @@
io.smallrye.opentelemetry
smallrye-opentelemetry-api
-
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-micrometer-otel-bridge
+
io.smallrye.opentelemetry
smallrye-opentelemetry-observation-otel-bridge
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationClientFilter.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationClientFilter.java
deleted file mode 100644
index 12c2eecd..00000000
--- a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationClientFilter.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package io.smallrye.opentelemetry.implementation.rest.observation;
-
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE;
-import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_NAME;
-import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_VERSION;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
-
-import java.util.List;
-
-import jakarta.inject.Inject;
-import jakarta.ws.rs.client.ClientRequestContext;
-import jakarta.ws.rs.client.ClientRequestFilter;
-import jakarta.ws.rs.client.ClientResponseContext;
-import jakarta.ws.rs.client.ClientResponseFilter;
-import jakarta.ws.rs.ext.Provider;
-
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.common.AttributesBuilder;
-import io.opentelemetry.api.metrics.LongHistogram;
-import io.opentelemetry.api.metrics.Meter;
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.context.propagation.TextMapSetter;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
-import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientExperimentalMetrics;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
-import io.opentelemetry.instrumentation.api.internal.SemconvStability;
-
-@Provider
-public class ObservationClientFilter implements ClientRequestFilter, ClientResponseFilter {
- private Instrumenter instrumenter;
- private LongHistogram durationHistogram;
-
- // RESTEasy requires no-arg constructor for CDI injection: https://issues.redhat.com/browse/RESTEASY-1538
- public ObservationClientFilter() {
- }
-
- @Inject
- public ObservationClientFilter(final OpenTelemetry openTelemetry) {
- ClientAttributesExtractor clientAttributesExtractor = new ClientAttributesExtractor();
-
- // TODO - The Client Span name is only "HTTP {METHOD_NAME}": https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name
- final InstrumenterBuilder builder = Instrumenter.builder(
- openTelemetry,
- INSTRUMENTATION_NAME,
- HttpSpanNameExtractor.create(clientAttributesExtractor));
- builder.setInstrumentationVersion(INSTRUMENTATION_VERSION);
-
- this.instrumenter = builder
- .setSpanStatusExtractor(HttpSpanStatusExtractor.create(clientAttributesExtractor))
- .addAttributesExtractor(HttpClientAttributesExtractor.create(clientAttributesExtractor))
-// .addOperationMetrics(HttpClientMetrics.get()) // This will include the duration histogram
- .addOperationMetrics(HttpClientExperimentalMetrics.get())
- .buildClientInstrumenter(new ClientRequestContextTextMapSetter());
-
- final Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME);
- //fixme use new https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpclientrequestduration
- durationHistogram = meter.histogramBuilder("http.client.duration")
- .setDescription("The duration of an outbound HTTP request")
- .ofLongs()
- .setUnit("ms")
- .build();
- }
-
- @Override
- public void filter(final ClientRequestContext request) {
- // CDI is not available in some contexts even if this library is available on the CP
- if (instrumenter != null) {
- Context parentContext = Context.current();
- if (instrumenter.shouldStart(parentContext, request)) {
- Context spanContext = instrumenter.start(parentContext, request);
- Scope scope = spanContext.makeCurrent();
- request.setProperty("otel.span.client.context", spanContext);
- request.setProperty("otel.span.client.parentContext", parentContext);
- request.setProperty("otel.span.client.scope", scope);
- }
- }
- if (durationHistogram != null) {
- request.setProperty("otel.metrics.client.start", System.currentTimeMillis());
- }
- }
-
- @Override
- public void filter(final ClientRequestContext request, final ClientResponseContext response) {
- // CDI is not available in some contexts even if this library is available on the CP
- Context spanContext = (Context) request.getProperty("otel.span.client.context");
- if (instrumenter != null) {
- Scope scope = (Scope) request.getProperty("otel.span.client.scope");
- if (scope == null) {
- return;
- }
-
- try {
- instrumenter.end(spanContext, request, response, null);
- } finally {
- scope.close();
-
- request.removeProperty("otel.span.client.context");
- request.removeProperty("otel.span.client.parentContext");
- request.removeProperty("otel.span.client.scope");
- }
- }
- if (durationHistogram != null) {
- Long start = (Long) request.getProperty("otel.metrics.client.start");
- if (start != null) {
- try {
- durationHistogram.record(System.currentTimeMillis() - start,
- getHistogramAttributes(request, response),
- spanContext);
- } finally {
- request.removeProperty("otel.metrics.client.start");
- }
- }
- }
- }
-
- private Attributes getHistogramAttributes(ClientRequestContext request, ClientResponseContext response) {
- AttributesBuilder builder = Attributes.builder();
- builder.put(HTTP_ROUTE.getKey(), request.getUri().getPath().toString());// Fixme must contain a template /users/:userID?
- if (SemconvStability.emitOldHttpSemconv()) {
- builder.put(HTTP_METHOD, request.getMethod());// FIXME semantic conventions
- builder.put(HTTP_STATUS_CODE, response.getStatus());
- } else {
- builder.put(HTTP_REQUEST_METHOD, request.getMethod());// FIXME semantic conventions
- builder.put(HTTP_RESPONSE_STATUS_CODE, response.getStatus());
- }
- return builder.build();
- }
-
- private static class ClientRequestContextTextMapSetter implements TextMapSetter {
- @Override
- public void set(final ClientRequestContext carrier, final String key, final String value) {
- if (carrier != null) {
- carrier.getHeaders().put(key, singletonList(value));
- }
- }
- }
-
- private static class ClientAttributesExtractor
- implements HttpClientAttributesGetter {
-
- @Override
- public String getUrlFull(final ClientRequestContext request) {
- return request.getUri().toString();
- }
-
- @Override
- public String getServerAddress(final ClientRequestContext request) {
- return request.getUri().getHost();
- }
-
- @Override
- public Integer getServerPort(final ClientRequestContext request) {
- return request.getUri().getPort();
- }
-
- @Override
- public String getHttpRequestMethod(final ClientRequestContext request) {
- return request.getMethod();
- }
-
- @Override
- public List getHttpRequestHeader(final ClientRequestContext request, final String name) {
- return request.getStringHeaders().getOrDefault(name, emptyList());
- }
-
- @Override
- public Integer getHttpResponseStatusCode(final ClientRequestContext request, final ClientResponseContext response,
- final Throwable throwable) {
- return response.getStatus();
- }
-
- @Override
- public List getHttpResponseHeader(final ClientRequestContext request, final ClientResponseContext response,
- final String name) {
- return response.getHeaders().getOrDefault(name, emptyList());
- }
- }
-}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationServerFilter.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationServerFilter.java
deleted file mode 100644
index 66f1cbe6..00000000
--- a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest.observation/ObservationServerFilter.java
+++ /dev/null
@@ -1,247 +0,0 @@
-package io.smallrye.opentelemetry.implementation.rest.observation;
-
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
-import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE;
-import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_NAME;
-import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_VERSION;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
-
-import java.lang.reflect.Method;
-import java.util.List;
-
-import jakarta.inject.Inject;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.container.ContainerRequestFilter;
-import jakarta.ws.rs.container.ContainerResponseContext;
-import jakarta.ws.rs.container.ContainerResponseFilter;
-import jakarta.ws.rs.container.ResourceInfo;
-import jakarta.ws.rs.core.UriBuilder;
-import jakarta.ws.rs.ext.Provider;
-
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.common.AttributesBuilder;
-import io.opentelemetry.api.metrics.LongHistogram;
-import io.opentelemetry.api.metrics.Meter;
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.context.propagation.TextMapGetter;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
-import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerExperimentalMetrics;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesExtractor;
-import io.opentelemetry.instrumentation.api.internal.SemconvStability;
-import io.opentelemetry.semconv.SemanticAttributes;
-
-@Provider
-public class ObservationServerFilter implements ContainerRequestFilter, ContainerResponseFilter {
- private Instrumenter instrumenter;
- private LongHistogram durationHistogram;
-
- @jakarta.ws.rs.core.Context
- ResourceInfo resourceInfo;
-
- // RESTEasy requires no-arg constructor for CDI injection: https://issues.redhat.com/browse/RESTEASY-1538
- public ObservationServerFilter() {
- }
-
- @Inject
- public ObservationServerFilter(final OpenTelemetry openTelemetry) {
- HttpServerAttributesGetter serverAttributesGetter = new HttpServerAttributesGetter();
-
- InstrumenterBuilder builder = Instrumenter.builder(
- openTelemetry,
- INSTRUMENTATION_NAME,
- HttpSpanNameExtractor.create(serverAttributesGetter));
- builder.setInstrumentationVersion(INSTRUMENTATION_VERSION);
-
- this.instrumenter = builder
- .setSpanStatusExtractor(HttpSpanStatusExtractor.create(serverAttributesGetter))
- .addAttributesExtractor(NetworkAttributesExtractor.create(new NetworkAttributesGetter()))
- .addAttributesExtractor(HttpServerAttributesExtractor.create(serverAttributesGetter))
- .addOperationMetrics(HttpServerMetrics.get())// FIXME how to filter out excluded endpoints?
- .addOperationMetrics(HttpServerExperimentalMetrics.get())
- .buildServerInstrumenter(new ContainerRequestContextTextMapGetter());
-
- final Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME);
- // fixme Use new: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
- durationHistogram = meter.histogramBuilder("http.server.duration")
- .setDescription("The duration of the inbound HTTP request")
- .ofLongs()
- .setUnit("ms")
- .build();
- }
-
- @Override
- public void filter(final ContainerRequestContext request) {
- // CDI is not available in some contexts even if this library is available on the CP
- if (instrumenter != null) {
- Context parentContext = Context.current();
- if (instrumenter.shouldStart(parentContext, request)) {
- request.setProperty("rest.resource.class", resourceInfo.getResourceClass());
- request.setProperty("rest.resource.method", resourceInfo.getResourceMethod());
-
- Context spanContext = instrumenter.start(parentContext, request);
- Scope scope = spanContext.makeCurrent();
- request.setProperty("otel.span.server.context", spanContext);
- request.setProperty("otel.span.server.parentContext", parentContext);
- request.setProperty("otel.span.server.scope", scope);
- }
- }
- if (durationHistogram != null) {
- request.setProperty("otel.metrics.client.start", System.currentTimeMillis());
- }
- }
-
- @Override
- public void filter(final ContainerRequestContext request, final ContainerResponseContext response) {
- Context spanContext = (Context) request.getProperty("otel.span.server.context");
- if (instrumenter != null) {
- Scope scope = (Scope) request.getProperty("otel.span.server.scope");
- if (scope == null) {
- return;
- }
-
- try {
- instrumenter.end(spanContext, request, response, null);
- } finally {
- scope.close();
-
- request.removeProperty("rest.resource.class");
- request.removeProperty("rest.resource.method");
- request.removeProperty("otel.span.server.context");
- request.removeProperty("otel.span.server.parentContext");
- request.removeProperty("otel.span.server.scope");
- }
- }
- if (durationHistogram != null) {
- Long start = (Long) request.getProperty("otel.metrics.client.start");
- if (start != null) {
- try {
- durationHistogram.record(System.currentTimeMillis() - start,
- getHistogramAttributes(request, response),
- spanContext);
- } finally {
- request.removeProperty("otel.metrics.client.start");
- }
- }
- }
- }
-
- private Attributes getHistogramAttributes(ContainerRequestContext request, ContainerResponseContext response) {
- AttributesBuilder builder = Attributes.builder();
- builder.put(HTTP_ROUTE.getKey(), request.getUriInfo().getPath().toString());// Fixme must contain a template /users/:userID?
- if (SemconvStability.emitOldHttpSemconv()) {
- builder.put(HTTP_METHOD, request.getMethod());// FIXME semantic conventions
- builder.put(HTTP_STATUS_CODE, response.getStatus());
- } else {
- builder.put(HTTP_REQUEST_METHOD, request.getMethod());// FIXME semantic conventions
- builder.put(HTTP_RESPONSE_STATUS_CODE, response.getStatus());
- }
- return builder.build();
- }
-
- private static class ContainerRequestContextTextMapGetter implements TextMapGetter {
- @Override
- public Iterable keys(final ContainerRequestContext carrier) {
- return carrier.getHeaders().keySet();
- }
-
- @Override
- public String get(final ContainerRequestContext carrier, final String key) {
- if (carrier == null) {
- return null;
- }
-
- return carrier.getHeaders().getOrDefault(key, singletonList(null)).get(0);
- }
- }
-
- private static class NetworkAttributesGetter implements
- io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesGetter {
- @Override
- public String getNetworkProtocolName(final ContainerRequestContext request, final ContainerResponseContext response) {
- return (String) request.getProperty(SemanticAttributes.NETWORK_PROTOCOL_NAME.getKey());
- }
-
- @Override
- public String getNetworkProtocolVersion(final ContainerRequestContext request,
- final ContainerResponseContext response) {
- return (String) request.getProperty(SemanticAttributes.NETWORK_PROTOCOL_VERSION.getKey());
- }
- }
-
- private static class HttpServerAttributesGetter implements
- io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter {
-
- @Override
- public String getUrlPath(final ContainerRequestContext request) {
- return request.getUriInfo().getRequestUri().getPath();
- }
-
- @Override
- public String getUrlQuery(final ContainerRequestContext request) {
- return request.getUriInfo().getRequestUri().getQuery();
- }
-
- @Override
- public String getHttpRoute(final ContainerRequestContext request) {
- try {
- // This can throw an IllegalArgumentException when determining the route for a subresource
- Class> resource = (Class>) request.getProperty("rest.resource.class");
- Method method = (Method) request.getProperty("rest.resource.method");
-
- UriBuilder uriBuilder = UriBuilder.newInstance();
- String contextRoot = request.getUriInfo().getBaseUri().getPath();
- if (contextRoot != null) {
- uriBuilder.path(contextRoot);
- }
- uriBuilder.path(resource);
- if (method.isAnnotationPresent(Path.class)) {
- uriBuilder.path(method);
- }
-
- return uriBuilder.toTemplate();
- } catch (IllegalArgumentException e) {
- return null;
- }
- }
-
- @Override
- public String getUrlScheme(final ContainerRequestContext request) {
- return request.getUriInfo().getRequestUri().getScheme();
- }
-
- @Override
- public String getHttpRequestMethod(final ContainerRequestContext request) {
- return request.getMethod();
- }
-
- @Override
- public List getHttpRequestHeader(final ContainerRequestContext request, final String name) {
- return request.getHeaders().getOrDefault(name, emptyList());
- }
-
- @Override
- public Integer getHttpResponseStatusCode(final ContainerRequestContext request, final ContainerResponseContext response,
- final Throwable throwable) {
- return response.getStatus();
- }
-
- @Override
- public List getHttpResponseHeader(final ContainerRequestContext request,
- final ContainerResponseContext response,
- final String name) {
- return response.getStringHeaders().getOrDefault(name, emptyList());
- }
- }
-}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/FilterDocumentation.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/FilterDocumentation.java
new file mode 100644
index 00000000..eb4b8881
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/FilterDocumentation.java
@@ -0,0 +1,142 @@
+package io.smallrye.opentelemetry.implementation.rest.observation;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.docs.ObservationDocumentation;
+import io.smallrye.opentelemetry.implementation.rest.observation.client.ClientFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.client.DefaultClientFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.server.DefaultServerFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.server.ServerFilterConvention;
+
+public enum FilterDocumentation implements ObservationDocumentation {
+ SERVER {
+ @Override
+ public Class extends ServerFilterConvention> getDefaultConvention() {
+ return DefaultServerFilterConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return KeyName.merge(LowCardinalityValues.values(), ServerLowCardinalityValues.values());
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return HighCardinalityValues.values();
+ }
+ },
+ CLIENT {
+ @Override
+ public Class extends ClientFilterConvention> getDefaultConvention() {
+ return DefaultClientFilterConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return KeyName.merge(LowCardinalityValues.values(), ClientLowCardinalityValues.values());
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return HighCardinalityValues.values();
+ }
+ };
+
+ public enum LowCardinalityValues implements KeyName {
+ /**
+ * The HTTP method of the request.
+ */
+ HTTP_REQUEST_METHOD {
+ @Override
+ public String asString() {
+ return "http.request.method";
+ }
+ },
+ URL_PATH {
+ @Override
+ public String asString() {
+ return "url.path";
+ }
+ },
+ HTTP_ROUTE {
+ @Override
+ public String asString() {
+ return "http.route";
+ }
+ },
+ URL_SCHEME {
+ @Override
+ public String asString() {
+ return "url.scheme";
+ }
+ },
+ HTTP_RESPONSE_STATUS_CODE {
+ @Override
+ public String asString() {
+ return "http.response.status_code";
+ }
+ },
+ NETWORK_PROTOCOL_NAME {
+ @Override
+ public String asString() {
+ return "network.protocol.name";
+ }
+ },
+ NETWORK_PROTOCOL_VERSION {
+ @Override
+ public String asString() {
+ return "network.protocol.version";
+ }
+ }
+ }
+
+ public enum ServerLowCardinalityValues implements KeyName {
+ SERVER_PORT {
+ @Override
+ public String asString() {
+ return "server.port";
+ }
+ },
+ SERVER_ADDRESS {
+ @Override
+ public String asString() {
+ return "server.address";
+ }
+ }
+ }
+
+ public enum ClientLowCardinalityValues implements KeyName {
+ CLIENT_ADDRESS {
+ @Override
+ public String asString() {
+ return "client.address";
+ }
+ },
+ CLIENT_PORT {
+ @Override
+ public String asString() {
+ return "client.port";
+ }
+ }
+ }
+
+ public enum HighCardinalityValues implements KeyName {
+ URL_QUERY {
+ @Override
+ public String asString() {
+ return "url.query";
+ }
+ },
+ ERROR {
+ @Override
+ public String asString() {
+ return "error";
+ }
+ },
+ URL_FULL {
+ @Override
+ public String asString() {
+ return "url.full";
+ }
+ }
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationClientFilter.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationClientFilter.java
new file mode 100644
index 00000000..924a7a03
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationClientFilter.java
@@ -0,0 +1,167 @@
+package io.smallrye.opentelemetry.implementation.rest.observation;
+
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.ext.Provider;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.context.propagation.TextMapSetter;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter;
+import io.opentelemetry.instrumentation.api.internal.SemconvStability;
+import io.smallrye.opentelemetry.implementation.rest.observation.client.ClientFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.client.DefaultClientFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.client.ObservationClientContext;
+
+@Provider
+public class ObservationClientFilter implements ClientRequestFilter, ClientResponseFilter {
+ private ObservationRegistry registry;
+ private ClientFilterConvention userClientFilterConvention;
+
+ // RESTEasy requires no-arg constructor for CDI injection: https://issues.redhat.com/browse/RESTEASY-1538
+ public ObservationClientFilter() {
+ }
+
+ @Inject
+ public ObservationClientFilter(ObservationRegistry registry) {
+ this.registry = registry;
+ this.userClientFilterConvention = null;
+ }
+
+ @Override
+ public void filter(final ClientRequestContext request) {
+ // CDI is not available in some contexts even if this library is available on the CP
+ if (registry == null) {
+ return;
+ }
+ final ObservationClientContext observationRequestContext = new ObservationClientContext(request);
+
+ Observation observation = FilterDocumentation.SERVER
+ .start(this.userClientFilterConvention,
+ new DefaultClientFilterConvention(),
+ () -> observationRequestContext,
+ registry);
+
+ Observation.Scope observationScope = observation.openScope();
+ request.setProperty("otel.span.client.context",
+ new ObservationRequestContextAndScope(observationRequestContext, observationScope));
+ }
+
+ @Override
+ public void filter(final ClientRequestContext request, final ClientResponseContext response) {
+ ObservationRequestContextAndScope contextAndScope = (ObservationRequestContextAndScope) request
+ .getProperty("otel.span.client.context");
+
+ if (contextAndScope == null) {
+ return;
+ }
+
+ contextAndScope.getObservationRequestContext().setResponseContext(response);
+ Observation.Scope observationScope = contextAndScope.getObservationScope();
+
+ try {
+ observationScope.close();
+ observationScope.getCurrentObservation().stop();
+ } finally {
+ request.removeProperty("otel.span.client.context");
+ }
+ }
+
+ private Attributes getHistogramAttributes(ClientRequestContext request, ClientResponseContext response) {
+ AttributesBuilder builder = Attributes.builder();
+ builder.put(HTTP_ROUTE.getKey(), request.getUri().getPath().toString());// Fixme must contain a template /users/:userID?
+ if (SemconvStability.emitOldHttpSemconv()) {
+ builder.put(HTTP_METHOD, request.getMethod());// FIXME semantic conventions
+ builder.put(HTTP_STATUS_CODE, response.getStatus());
+ } else {
+ builder.put(HTTP_REQUEST_METHOD, request.getMethod());// FIXME semantic conventions
+ builder.put(HTTP_RESPONSE_STATUS_CODE, response.getStatus());
+ }
+ return builder.build();
+ }
+
+ private static class ClientRequestContextTextMapSetter implements TextMapSetter {
+ @Override
+ public void set(final ClientRequestContext carrier, final String key, final String value) {
+ if (carrier != null) {
+ carrier.getHeaders().put(key, singletonList(value));
+ }
+ }
+ }
+
+ private static class ClientAttributesExtractor
+ implements HttpClientAttributesGetter {
+
+ @Override
+ public String getUrlFull(final ClientRequestContext request) {
+ return request.getUri().toString();
+ }
+
+ @Override
+ public String getServerAddress(final ClientRequestContext request) {
+ return request.getUri().getHost();
+ }
+
+ @Override
+ public Integer getServerPort(final ClientRequestContext request) {
+ return request.getUri().getPort();
+ }
+
+ @Override
+ public String getHttpRequestMethod(final ClientRequestContext request) {
+ return request.getMethod();
+ }
+
+ @Override
+ public List getHttpRequestHeader(final ClientRequestContext request, final String name) {
+ return request.getStringHeaders().getOrDefault(name, emptyList());
+ }
+
+ @Override
+ public Integer getHttpResponseStatusCode(final ClientRequestContext request, final ClientResponseContext response,
+ final Throwable throwable) {
+ return response.getStatus();
+ }
+
+ @Override
+ public List getHttpResponseHeader(final ClientRequestContext request, final ClientResponseContext response,
+ final String name) {
+ return response.getHeaders().getOrDefault(name, emptyList());
+ }
+ }
+
+ static class ObservationRequestContextAndScope {
+ private final ObservationClientContext observationRequestContext;
+ private final Observation.Scope observationScope;
+
+ public ObservationRequestContextAndScope(ObservationClientContext observationRequestContext,
+ Observation.Scope observationScope) {
+ this.observationRequestContext = observationRequestContext;
+ this.observationScope = observationScope;
+ }
+
+ public ObservationClientContext getObservationRequestContext() {
+ return observationRequestContext;
+ }
+
+ public Observation.Scope getObservationScope() {
+ return observationScope;
+ }
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationServerFilter.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationServerFilter.java
new file mode 100644
index 00000000..66518123
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationServerFilter.java
@@ -0,0 +1,92 @@
+package io.smallrye.opentelemetry.implementation.rest.observation;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.ext.Provider;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.smallrye.opentelemetry.implementation.rest.observation.server.DefaultServerFilterConvention;
+import io.smallrye.opentelemetry.implementation.rest.observation.server.ObservationServerContext;
+import io.smallrye.opentelemetry.implementation.rest.observation.server.ServerFilterConvention;
+
+@Provider
+public class ObservationServerFilter implements ContainerRequestFilter, ContainerResponseFilter {
+ private ObservationRegistry registry;
+ private ServerFilterConvention userServerFilterConvention;
+
+ @jakarta.ws.rs.core.Context
+ ResourceInfo resourceInfo;
+
+ // RESTEasy requires no-arg constructor for CDI injection: https://issues.redhat.com/browse/RESTEASY-1538
+ public ObservationServerFilter() {
+ }
+
+ @Inject
+ public ObservationServerFilter(ObservationRegistry registry) {
+ this.registry = registry;
+ this.userServerFilterConvention = null;
+ }
+
+ @Override
+ public void filter(final ContainerRequestContext request) {
+ // CDI is not available in some contexts even if this library is available on the CP
+ if (registry == null) {
+ return;
+ }
+ final ObservationServerContext observationRequestContext = new ObservationServerContext(request, resourceInfo);
+
+ Observation observation = FilterDocumentation.SERVER
+ .start(this.userServerFilterConvention,
+ new DefaultServerFilterConvention(),
+ () -> observationRequestContext,
+ registry);
+
+ Observation.Scope observationScope = observation.openScope();
+ request.setProperty("otel.span.server.context",
+ new ObservationRequestContextAndScope(observationRequestContext, observationScope));
+ }
+
+ @Override
+ public void filter(final ContainerRequestContext request, final ContainerResponseContext response) {
+ ObservationRequestContextAndScope contextAndScope = (ObservationRequestContextAndScope) request
+ .getProperty("otel.span.server.context");
+
+ if (contextAndScope == null) {
+ return;
+ }
+
+ contextAndScope.getObservationRequestContext().setResponseContext(response);
+ Observation.Scope observationScope = contextAndScope.getObservationScope();
+
+ try {
+ observationScope.close();
+ observationScope.getCurrentObservation().stop();
+ } finally {
+ request.removeProperty("otel.span.server.context");
+ }
+ }
+
+ static class ObservationRequestContextAndScope {
+ private final ObservationServerContext observationRequestContext;
+ private final Observation.Scope observationScope;
+
+ public ObservationRequestContextAndScope(ObservationServerContext observationRequestContext,
+ Observation.Scope observationScope) {
+ this.observationRequestContext = observationRequestContext;
+ this.observationScope = observationScope;
+ }
+
+ public ObservationServerContext getObservationRequestContext() {
+ return observationRequestContext;
+ }
+
+ public Observation.Scope getObservationScope() {
+ return observationScope;
+ }
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ClientFilterConvention.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ClientFilterConvention.java
new file mode 100644
index 00000000..51b68e52
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ClientFilterConvention.java
@@ -0,0 +1,11 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.client;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+public interface ClientFilterConvention extends ObservationConvention {
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof ObservationClientContext;
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/DefaultClientFilterConvention.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/DefaultClientFilterConvention.java
new file mode 100644
index 00000000..d9c74b03
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/DefaultClientFilterConvention.java
@@ -0,0 +1,46 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.client;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+
+import io.micrometer.common.KeyValues;
+import io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation;
+
+// FIXME there's much duplicated code allong with the DefaultServerFilterConvention. Extract common code to a superclass.
+public class DefaultClientFilterConvention implements ClientFilterConvention {
+
+ public DefaultClientFilterConvention() {
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(ObservationClientContext context) {
+ final ClientRequestContext requestContext = context.getRequestContext();
+ return KeyValues.of(
+ FilterDocumentation.LowCardinalityValues.HTTP_REQUEST_METHOD.withValue(requestContext.getMethod()),
+ FilterDocumentation.LowCardinalityValues.URL_PATH.withValue(requestContext.getUri().getPath()),
+ FilterDocumentation.LowCardinalityValues.URL_SCHEME.withValue(requestContext.getUri().getScheme()),
+ FilterDocumentation.ClientLowCardinalityValues.CLIENT_PORT.withValue("" + requestContext.getUri().getPort()),
+ FilterDocumentation.ClientLowCardinalityValues.CLIENT_ADDRESS.withValue(requestContext.getUri().getHost()));
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(ObservationClientContext context) {
+ final ClientRequestContext requestContext = context.getRequestContext();
+ return KeyValues.of(
+ FilterDocumentation.HighCardinalityValues.URL_QUERY.withValue(requestContext.getUri().getQuery()),
+ FilterDocumentation.HighCardinalityValues.URL_FULL.withValue(requestContext.getUri().toString()));
+ }
+
+ @Override
+ public String getName() {
+ return "http.client";
+ }
+
+ @Override
+ public String getContextualName(ObservationClientContext context) {
+ final ClientRequestContext requestContext = context.getRequestContext();
+ if (requestContext == null) {
+ return null;
+ }
+ return requestContext.getMethod();
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ObservationClientContext.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ObservationClientContext.java
new file mode 100644
index 00000000..928f8b14
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ObservationClientContext.java
@@ -0,0 +1,28 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.client;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+
+import io.micrometer.observation.Observation;
+
+public class ObservationClientContext extends Observation.Context {
+
+ private final ClientRequestContext requestContext;
+ private ClientResponseContext responseContext;
+
+ public ObservationClientContext(final ClientRequestContext requestContext) {
+ this.requestContext = requestContext;
+ }
+
+ public ClientRequestContext getRequestContext() {
+ return requestContext;
+ }
+
+ public ClientResponseContext getResponseContext() {
+ return responseContext;
+ }
+
+ public void setResponseContext(ClientResponseContext responseContext) {
+ this.responseContext = responseContext;
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/DefaultServerFilterConvention.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/DefaultServerFilterConvention.java
new file mode 100644
index 00000000..135ade35
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/DefaultServerFilterConvention.java
@@ -0,0 +1,80 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.server;
+
+import java.lang.reflect.Method;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.UriBuilder;
+
+import io.micrometer.common.KeyValues;
+import io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation;
+
+public class DefaultServerFilterConvention implements ServerFilterConvention {
+
+ public DefaultServerFilterConvention() {
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(ObservationServerContext context) {
+ final ContainerRequestContext requestContext = context.getRequestContext();
+ KeyValues keyValues = KeyValues.of(
+ FilterDocumentation.LowCardinalityValues.HTTP_REQUEST_METHOD.withValue(requestContext.getMethod()),
+ FilterDocumentation.LowCardinalityValues.URL_PATH.withValue(requestContext.getUriInfo().getPath()),
+ FilterDocumentation.LowCardinalityValues.HTTP_ROUTE.withValue(getHttpRoute(context)),
+ FilterDocumentation.LowCardinalityValues.URL_SCHEME
+ .withValue(requestContext.getUriInfo().getRequestUri().getScheme()),
+ FilterDocumentation.ServerLowCardinalityValues.SERVER_PORT
+ .withValue("" + requestContext.getUriInfo().getRequestUri().getPort()),
+ FilterDocumentation.ServerLowCardinalityValues.SERVER_ADDRESS
+ .withValue(requestContext.getUriInfo().getRequestUri().getHost()));
+
+ if (context.getResponseContext() != null) {
+ keyValues.and(
+ FilterDocumentation.LowCardinalityValues.HTTP_RESPONSE_STATUS_CODE
+ .withValue("" + context.getResponseContext().getStatus()));
+ }
+ return keyValues;
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(ObservationServerContext context) {
+ final ContainerRequestContext requestContext = context.getRequestContext();
+ return KeyValues.of(
+ FilterDocumentation.HighCardinalityValues.URL_QUERY
+ .withValue(requestContext.getUriInfo().getRequestUri().getQuery()));
+ }
+
+ @Override
+ public String getName() {
+ return "http.server";
+ }
+
+ @Override
+ public String getContextualName(ObservationServerContext context) {
+ final ContainerRequestContext requestContext = context.getRequestContext();
+ final String route = getHttpRoute(context);
+ return route == null ? requestContext.getMethod() : requestContext.getMethod() + " " + route;
+ }
+
+ private String getHttpRoute(final ObservationServerContext request) {
+ try {
+ // This can throw an IllegalArgumentException when determining the route for a subresource
+ Class> resource = (Class>) request.getResourceInfo().getResourceClass();
+ Method method = (Method) request.getResourceInfo().getResourceMethod();
+
+ UriBuilder uriBuilder = UriBuilder.newInstance();
+ String contextRoot = request.getRequestContext().getUriInfo().getBaseUri().getPath();
+ if (contextRoot != null) {
+ uriBuilder.path(contextRoot);
+ }
+ uriBuilder.path(resource);
+ if (method.isAnnotationPresent(Path.class)) {
+ uriBuilder.path(method);
+ }
+
+ return uriBuilder.toTemplate();
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ObservationServerContext.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ObservationServerContext.java
new file mode 100644
index 00000000..39d4b91a
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ObservationServerContext.java
@@ -0,0 +1,34 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.server;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ResourceInfo;
+
+import io.micrometer.observation.Observation;
+
+public class ObservationServerContext extends Observation.Context {
+ private final ContainerRequestContext requestContext;
+ private final ResourceInfo resourceInfo;
+ private ContainerResponseContext responseContext;
+
+ public ObservationServerContext(final ContainerRequestContext requestContext, final ResourceInfo resourceInfo) {
+ this.requestContext = requestContext;
+ this.resourceInfo = resourceInfo;
+ }
+
+ public ContainerRequestContext getRequestContext() {
+ return requestContext;
+ }
+
+ public ResourceInfo getResourceInfo() {
+ return resourceInfo;
+ }
+
+ public ContainerResponseContext getResponseContext() {
+ return responseContext;
+ }
+
+ public void setResponseContext(ContainerResponseContext responseContext) {
+ this.responseContext = responseContext;
+ }
+}
diff --git a/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ServerFilterConvention.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ServerFilterConvention.java
new file mode 100644
index 00000000..4176a26e
--- /dev/null
+++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ServerFilterConvention.java
@@ -0,0 +1,11 @@
+package io.smallrye.opentelemetry.implementation.rest.observation.server;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+public interface ServerFilterConvention extends ObservationConvention {
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof ObservationServerContext;
+ }
+}
diff --git a/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryClientFilter.java b/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryClientFilter.java
index ebc07804..12fe74fb 100644
--- a/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryClientFilter.java
+++ b/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryClientFilter.java
@@ -53,7 +53,7 @@ public OpenTelemetryClientFilter(final OpenTelemetry openTelemetry) {
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(clientAttributesExtractor))
.addAttributesExtractor(NetworkAttributesExtractor.create(new NetworkAttributesGetter()))
.addAttributesExtractor(HttpClientAttributesExtractor.create(clientAttributesExtractor))
-// .addOperationMetrics(HttpClientMetrics.get()) // includes histogram from bellow
+ // .addOperationMetrics(HttpClientMetrics.get()) // includes histogram from bellow
.addOperationMetrics(HttpClientExperimentalMetrics.get())
.buildClientInstrumenter(new ClientRequestContextTextMapSetter());
}
diff --git a/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryServerFilter.java b/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryServerFilter.java
index d58051ae..d9c40a21 100644
--- a/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryServerFilter.java
+++ b/implementation/rest/src/main/java/io/smallrye/opentelemetry/implementation/rest/OpenTelemetryServerFilter.java
@@ -62,7 +62,7 @@ public OpenTelemetryServerFilter(final OpenTelemetry openTelemetry) {
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(serverAttributesGetter))
.addAttributesExtractor(NetworkAttributesExtractor.create(new NetworkAttributesGetter()))
.addAttributesExtractor(HttpServerAttributesExtractor.create(serverAttributesGetter))
-// .addOperationMetrics(HttpServerMetrics.get())// FIXME how to filter out excluded endpoints? // includes histogram from bellow
+ // .addOperationMetrics(HttpServerMetrics.get())// FIXME how to filter out excluded endpoints? // includes histogram from bellow
.addOperationMetrics(HttpServerExperimentalMetrics.get())
.buildServerInstrumenter(new ContainerRequestContextTextMapGetter());
}
diff --git a/pom.xml b/pom.xml
index 91ecdeb1..e2b00abe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,6 +152,16 @@
smallrye-opentelemetry-micrometer-otel-bridge
${project.version}
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-observation-otel-bridge
+ ${project.version}
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-rest-observation
+ ${project.version}
+
io.vertx
diff --git a/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestClientSpanTest.java b/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestClientSpanTest.java
index 91909d37..4d9f9389 100644
--- a/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestClientSpanTest.java
+++ b/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestClientSpanTest.java
@@ -50,6 +50,7 @@
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -103,9 +104,9 @@ void span() {
SpanData client = spans.get(1);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(server, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span", AttributeKeysStability.get(client, URL_FULL));
assertEquals(client.getTraceId(), server.getTraceId());
assertEquals(server.getParentSpanId(), client.getSpanId());
@@ -121,18 +122,18 @@ void spanName() {
SpanData server = spans.get(0);
assertEquals(SERVER, server.getKind());
assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", server.getName());
- assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
- assertEquals("http", get(server, URL_SCHEME));
- assertEquals(url.getPath() + "span/1", get(server, URL_PATH));
- assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(server, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(server, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals("http", AttributeKeysStability.get(server, URL_SCHEME));
+ Assertions.assertEquals(url.getPath() + "span/1", AttributeKeysStability.get(server, URL_PATH));
+ Assertions.assertEquals(url.getHost(), AttributeKeysStability.get(server, SERVER_ADDRESS));
SpanData client = spans.get(1);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span/1", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(client, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span/1", AttributeKeysStability.get(client, URL_FULL));
assertEquals(server.getTraceId(), client.getTraceId());
assertEquals(server.getParentSpanId(), client.getSpanId());
@@ -158,9 +159,9 @@ void spanNameQuery() {
SpanData client = spans.get(1);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span/1?query=query", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(client, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span/1?query=query", AttributeKeysStability.get(client, URL_FULL));
assertEquals(client.getTraceId(), server.getTraceId());
assertEquals(server.getParentSpanId(), client.getSpanId());
@@ -220,18 +221,18 @@ void spanChild() {
SpanData server = spans.get(1);
assertEquals(SERVER, server.getKind());
assertEquals(HttpMethod.GET + " " + url.getPath() + "span/child", server.getName());
- assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
- assertEquals("http", get(server, URL_SCHEME));
- assertEquals(url.getPath() + "span/child", get(server, URL_PATH));
- assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(server, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(server, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals("http", AttributeKeysStability.get(server, URL_SCHEME));
+ Assertions.assertEquals(url.getPath() + "span/child", AttributeKeysStability.get(server, URL_PATH));
+ Assertions.assertEquals(url.getHost(), AttributeKeysStability.get(server, SERVER_ADDRESS));
SpanData client = spans.get(2);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span/child", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(client, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span/child", AttributeKeysStability.get(client, URL_FULL));
assertEquals(client.getTraceId(), internal.getTraceId());
assertEquals(client.getTraceId(), server.getTraceId());
@@ -249,19 +250,19 @@ void spanCurrent() {
SpanData server = spans.get(0);
assertEquals(SERVER, server.getKind());
assertEquals(HttpMethod.GET + " " + url.getPath() + "span/current", server.getName());
- assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
- assertEquals("http", get(server, URL_SCHEME));
- assertEquals(url.getPath() + "span/current", get(server, URL_PATH));
- assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(server, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(server, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals("http", AttributeKeysStability.get(server, URL_SCHEME));
+ Assertions.assertEquals(url.getPath() + "span/current", AttributeKeysStability.get(server, URL_PATH));
+ Assertions.assertEquals(url.getHost(), AttributeKeysStability.get(server, SERVER_ADDRESS));
assertEquals("tck.current.value", server.getAttributes().get(stringKey("tck.current.key")));
SpanData client = spans.get(1);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span/current", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(client, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span/current", AttributeKeysStability.get(client, URL_FULL));
assertEquals(client.getTraceId(), server.getTraceId());
assertEquals(server.getParentSpanId(), client.getSpanId());
@@ -282,18 +283,18 @@ void spanNew() {
SpanData server = spans.get(1);
assertEquals(SERVER, server.getKind());
assertEquals(HttpMethod.GET + " " + url.getPath() + "span/new", server.getName());
- assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
- assertEquals("http", get(server, URL_SCHEME));
- assertEquals(url.getPath() + "span/new", get(server, URL_PATH));
- assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(server, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(server, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals("http", AttributeKeysStability.get(server, URL_SCHEME));
+ Assertions.assertEquals(url.getPath() + "span/new", AttributeKeysStability.get(server, URL_PATH));
+ Assertions.assertEquals(url.getHost(), AttributeKeysStability.get(server, SERVER_ADDRESS));
SpanData client = spans.get(2);
assertEquals(CLIENT, client.getKind());
assertEquals("GET", client.getName());
- assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
- assertEquals(url.toString() + "span/new", get(client, URL_FULL));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(client, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(client, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(url.toString() + "span/new", AttributeKeysStability.get(client, URL_FULL));
assertEquals(client.getTraceId(), internal.getTraceId());
assertEquals(client.getTraceId(), server.getTraceId());
diff --git a/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestSpanTest.java b/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestSpanTest.java
index cccf1b63..07cb30a4 100644
--- a/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestSpanTest.java
+++ b/testsuite/extra/src/test/java/io/smallrye/opentelemetry/extra/test/trace/rest/RestSpanTest.java
@@ -46,6 +46,7 @@
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -115,8 +116,8 @@ void spanName() {
SpanData span = spanItems.get(0);
assertEquals(SERVER, span.getKind());
assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", span.getName());
- assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(span, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(span, HTTP_REQUEST_METHOD));
}
@Test
@@ -178,19 +179,19 @@ void spanPost() {
assertEquals(HttpMethod.POST + " " + url.getPath() + "span", span.getName());
// Common Attributes
- assertEquals(HttpMethod.POST, get(span, HTTP_REQUEST_METHOD));
- assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.POST, AttributeKeysStability.get(span, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(span, HTTP_RESPONSE_STATUS_CODE));
assertNotNull(span.getAttributes().get(USER_AGENT_ORIGINAL));
assertNull(span.getAttributes().get(NETWORK_LOCAL_ADDRESS));
assertNull(span.getAttributes().get(NETWORK_LOCAL_PORT));
// Server Attributes
- assertEquals("http", get(span, URL_SCHEME));
- assertEquals(url.getPath() + "span", get(span, URL_PATH));
+ Assertions.assertEquals("http", AttributeKeysStability.get(span, URL_SCHEME));
+ Assertions.assertEquals(url.getPath() + "span", AttributeKeysStability.get(span, URL_PATH));
assertEquals(url.getPath() + "span", span.getAttributes().get(HTTP_ROUTE));
- assertNull(get(span, CLIENT_ADDRESS));
- assertEquals(url.getHost(), get(span, SERVER_ADDRESS));
- assertEquals(url.getPort(), get(span, SERVER_PORT));
+ assertNull(AttributeKeysStability.get(span, CLIENT_ADDRESS));
+ Assertions.assertEquals(url.getHost(), AttributeKeysStability.get(span, SERVER_ADDRESS));
+ Assertions.assertEquals(url.getPort(), AttributeKeysStability.get(span, SERVER_PORT));
}
@Test
@@ -202,8 +203,8 @@ void subResource() {
SpanData span = spanItems.get(0);
assertEquals(SERVER, span.getKind());
assertEquals(HttpMethod.GET, span.getName());
- assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
- assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ Assertions.assertEquals(HTTP_OK, AttributeKeysStability.get(span, HTTP_RESPONSE_STATUS_CODE));
+ Assertions.assertEquals(HttpMethod.GET, AttributeKeysStability.get(span, HTTP_REQUEST_METHOD));
}
@Path("/")
diff --git a/testsuite/observation/pom.xml b/testsuite/observation/pom.xml
new file mode 100644
index 00000000..efcd1eab
--- /dev/null
+++ b/testsuite/observation/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-testsuite
+ 2.6.1-SNAPSHOT
+
+
+ smallrye-opentelemetry-observation-otel-bridge-it
+ SmallRye OpenTelemetry: Test Suite for Observation to OpenTelemetry bridge
+
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-api
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-observation-otel-bridge
+ test
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-config
+ test
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-rest-observation
+ test
+
+
+ io.smallrye.opentelemetry
+ smallrye-opentelemetry-test
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ io.rest-assured
+ rest-assured
+
+
+ org.awaitility
+ awaitility
+
+
+
+
+ org.jboss.arquillian.junit5
+ arquillian-junit5-container
+
+
+ io.smallrye.testing
+ smallrye-testing-tck-jetty
+
+
+ io.smallrye.config
+ smallrye-config
+
+
+ org.jboss.resteasy.microprofile
+ microprofile-rest-client
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ stable
+
+ test
+
+
+ false
+
+ http
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianExtension.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianExtension.java
new file mode 100644
index 00000000..5ce644a8
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianExtension.java
@@ -0,0 +1,12 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
+import org.jboss.arquillian.core.spi.LoadableExtension;
+
+public class ArquillianExtension implements LoadableExtension {
+ @Override
+ public void register(ExtensionBuilder extensionBuilder) {
+ extensionBuilder.service(ApplicationArchiveProcessor.class, DeploymentProcessor.class);
+ extensionBuilder.observer(ArquillianLifecycle.class);
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianLifecycle.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianLifecycle.java
new file mode 100644
index 00000000..b23ab327
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianLifecycle.java
@@ -0,0 +1,33 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import org.jboss.arquillian.container.spi.client.protocol.metadata.HTTPContext;
+import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
+import org.jboss.arquillian.container.spi.client.protocol.metadata.Servlet;
+import org.jboss.arquillian.container.spi.event.container.AfterDeploy;
+import org.jboss.arquillian.container.spi.event.container.BeforeDeploy;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.core.api.annotation.Observes;
+import org.jboss.arquillian.test.spi.TestClass;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.restassured.RestAssured;
+
+public class ArquillianLifecycle {
+ public void beforeDeploy(@Observes BeforeDeploy event, TestClass testClass) {
+ GlobalOpenTelemetry.resetForTest();
+ }
+
+ @Inject
+ Instance protocolMetadata;
+
+ public void afterDeploy(@Observes AfterDeploy event, TestClass testClass) {
+ HTTPContext httpContext = protocolMetadata.get().getContexts(HTTPContext.class).iterator().next();
+ Servlet servlet = httpContext.getServlets().iterator().next();
+ String baseUri = servlet.getBaseURI().toString();
+ TestConfigSource.configuration.put("baseUri", baseUri);
+
+ RestAssured.port = httpContext.getPort();
+ RestAssured.basePath = servlet.getBaseURI().getPath();
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/AttributeKeysStability.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/AttributeKeysStability.java
new file mode 100644
index 00000000..896e8688
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/AttributeKeysStability.java
@@ -0,0 +1,89 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import static io.opentelemetry.semconv.SemanticAttributes.CLIENT_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.CLIENT_SOCKET_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.CLIENT_SOCKET_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_CLIENT_IP;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_BODY_SIZE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_BODY_SIZE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_SCHEME;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_URL;
+import static io.opentelemetry.semconv.SemanticAttributes.NETWORK_PROTOCOL_NAME;
+import static io.opentelemetry.semconv.SemanticAttributes.NETWORK_PROTOCOL_VERSION;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_NAME;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_PROTOCOL_NAME;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_PROTOCOL_VERSION;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_ADDR;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_NAME;
+import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_DOMAIN;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_FULL;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_PATH;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_QUERY;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_SCHEME;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.instrumentation.api.internal.SemconvStability;
+import io.opentelemetry.sdk.trace.data.SpanData;
+
+public class AttributeKeysStability {
+ @SuppressWarnings("unchecked")
+ public static T get(SpanData spanData, AttributeKey key) {
+ if (SemconvStability.emitOldHttpSemconv()) {
+ if (SERVER_ADDRESS.equals(key)) {
+ key = (AttributeKey) NET_HOST_NAME;
+ } else if (SERVER_PORT.equals(key)) {
+ key = (AttributeKey) NET_HOST_PORT;
+ } else if (URL_SCHEME.equals(key)) {
+ key = (AttributeKey) HTTP_SCHEME;
+ } else if (URL_FULL.equals(key)) {
+ key = (AttributeKey) HTTP_URL;
+ } else if (URL_PATH.equals(key)) {
+ key = (AttributeKey) HTTP_TARGET;
+ } else if (HTTP_REQUEST_METHOD.equals(key)) {
+ key = (AttributeKey) HTTP_METHOD;
+ } else if (HTTP_REQUEST_BODY_SIZE.equals(key)) {
+ key = (AttributeKey) HTTP_REQUEST_CONTENT_LENGTH;
+ } else if (HTTP_RESPONSE_STATUS_CODE.equals(key)) {
+ key = (AttributeKey) HTTP_STATUS_CODE;
+ } else if (HTTP_RESPONSE_BODY_SIZE.equals(key)) {
+ key = (AttributeKey) HTTP_RESPONSE_CONTENT_LENGTH;
+ } else if (CLIENT_ADDRESS.equals(key)) {
+ key = (AttributeKey) HTTP_CLIENT_IP;
+ } else if (SERVER_SOCKET_ADDRESS.equals(key) || CLIENT_SOCKET_ADDRESS.equals(key)) {
+ key = (AttributeKey) NET_SOCK_PEER_ADDR;
+ } else if (SERVER_SOCKET_PORT.equals(key) || CLIENT_SOCKET_PORT.equals(key)) {
+ key = (AttributeKey) NET_SOCK_PEER_PORT;
+ } else if (NETWORK_PROTOCOL_NAME.equals(key)) {
+ key = (AttributeKey) NET_PROTOCOL_NAME;
+ } else if (NETWORK_PROTOCOL_VERSION.equals(key)) {
+ key = (AttributeKey) NET_PROTOCOL_VERSION;
+ } else if (NET_SOCK_PEER_NAME.equals(key)) {
+ key = (AttributeKey) SERVER_SOCKET_DOMAIN;
+ }
+ } else {
+ if (URL_PATH.equals(key)) {
+ String path = spanData.getAttributes().get(URL_PATH);
+ String query = spanData.getAttributes().get(URL_QUERY);
+ if (path == null && query == null) {
+ return null;
+ }
+ String target = (path == null ? "" : path) + (query == null || query.isEmpty() ? "" : "?" + query);
+ return (T) target;
+ }
+ }
+ return spanData.getAttributes().get((AttributeKey extends T>) key);
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/DeploymentProcessor.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/DeploymentProcessor.java
new file mode 100644
index 00000000..ba3bed84
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/DeploymentProcessor.java
@@ -0,0 +1,24 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
+import org.jboss.arquillian.test.spi.TestClass;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+
+import io.smallrye.opentelemetry.test.InMemoryMetricExporter;
+import io.smallrye.opentelemetry.test.InMemorySpanExporter;
+
+public class DeploymentProcessor implements ApplicationArchiveProcessor {
+ @Override
+ public void process(Archive> archive, TestClass testClass) {
+ if (archive instanceof WebArchive) {
+ WebArchive war = (WebArchive) archive;
+ war.addAsServiceProvider(ConfigSource.class, TestConfigSource.class);
+ war.addClass(HttpServerAttributesFilter.class);
+ war.addClass(InMemorySpanExporter.class);
+ war.addClass(InMemoryMetricExporter.class);
+ war.toString(true);
+ }
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/HttpServerAttributesFilter.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/HttpServerAttributesFilter.java
new file mode 100644
index 00000000..05aecb5a
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/HttpServerAttributesFilter.java
@@ -0,0 +1,31 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.ext.Provider;
+
+import io.opentelemetry.semconv.SemanticAttributes;
+
+@Provider
+@PreMatching
+public class HttpServerAttributesFilter implements ContainerRequestFilter, ContainerResponseFilter {
+ @Context
+ HttpServletRequest httpServletRequest;
+
+ @Override
+ public void filter(final ContainerRequestContext request) {
+ String[] nameAndVersion = httpServletRequest.getProtocol().split("/");
+ request.setProperty(SemanticAttributes.NETWORK_PROTOCOL_NAME.getKey(), nameAndVersion[0]);
+ request.setProperty(SemanticAttributes.NETWORK_PROTOCOL_VERSION.getKey(), nameAndVersion[1]);
+ }
+
+ @Override
+ public void filter(final ContainerRequestContext request, final ContainerResponseContext response) {
+
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestApplication.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestApplication.java
new file mode 100644
index 00000000..5096be62
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestApplication.java
@@ -0,0 +1,91 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.inject.Inject;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(ArquillianExtension.class)
+class TestApplication {
+ @ArquillianResource
+ private URL url;
+ @Inject
+ HelloBean helloBean;
+
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @Test
+ public void servlet() {
+ String uri = url.toExternalForm() + "servlet";
+ WebTarget echoEndpointTarget = ClientBuilder.newClient().target(uri);
+ Response response = echoEndpointTarget.request(TEXT_PLAIN).get();
+ assertEquals(response.getStatus(), HttpURLConnection.HTTP_OK);
+ }
+
+ @Test
+ public void rest() {
+ String uri = url.toExternalForm() + "rest";
+ WebTarget echoEndpointTarget = ClientBuilder.newClient().target(uri);
+ Response response = echoEndpointTarget.request(TEXT_PLAIN).get();
+ assertEquals(response.getStatus(), HttpURLConnection.HTTP_OK);
+ }
+
+ @WebServlet(urlPatterns = "/servlet")
+ public static class TestServlet extends HttpServlet {
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+ resp.getWriter().write(CDI.current().select(HelloBean.class).get().hello());
+ }
+ }
+
+ @ApplicationPath("/rest")
+ public static class RestApplication extends Application {
+
+ }
+
+ @Path("/")
+ public static class TestEndpoint {
+ @Inject
+ HelloBean helloBean;
+
+ @GET
+ public String hello() {
+ return helloBean.hello();
+ }
+ }
+
+ @ApplicationScoped
+ public static class HelloBean {
+ public String hello() {
+ return "hello";
+ }
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestConfigSource.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestConfigSource.java
new file mode 100644
index 00000000..7732f952
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestConfigSource.java
@@ -0,0 +1,26 @@
+package io.smallrye.opentelemetry.observation.test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+public class TestConfigSource implements ConfigSource {
+ static final Map configuration = new HashMap<>();
+
+ @Override
+ public Set getPropertyNames() {
+ return configuration.keySet();
+ }
+
+ @Override
+ public String getValue(final String propertyName) {
+ return configuration.get(propertyName);
+ }
+
+ @Override
+ public String getName() {
+ return TestConfigSource.class.getName();
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/baggage/BaggageTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/baggage/BaggageTest.java
new file mode 100644
index 00000000..e9b335c8
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/baggage/BaggageTest.java
@@ -0,0 +1,71 @@
+package io.smallrye.opentelemetry.observation.test.baggage;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.api.baggage.Baggage;
+import io.smallrye.opentelemetry.test.InMemorySpanExporter;
+
+@ExtendWith(ArquillianExtension.class)
+class BaggageTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @ArquillianResource
+ URL url;
+ @Inject
+ InMemorySpanExporter spanExporter;
+
+ @BeforeEach
+ void setUp() {
+ spanExporter.reset();
+ }
+
+ @Test
+ void baggage() {
+ WebTarget target = ClientBuilder.newClient().target(url.toString() + "baggage");
+ Response response = target.request().header("baggage", "user=naruto").get();
+ assertEquals(HTTP_OK, response.getStatus());
+
+ spanExporter.getFinishedSpanItems(2);
+ }
+
+ @Path("/baggage")
+ public static class BaggageResource {
+ @Inject
+ Baggage baggage;
+
+ @GET
+ public Response baggage() {
+ assertEquals("naruto", baggage.getEntryValue("user"));
+ return Response.ok().build();
+ }
+ }
+
+ @ApplicationPath("/")
+ public static class RestApplication extends Application {
+
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/cdi/GaugeCdiTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/cdi/GaugeCdiTest.java
new file mode 100644
index 00000000..6a07fbdc
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/cdi/GaugeCdiTest.java
@@ -0,0 +1,64 @@
+package io.smallrye.opentelemetry.observation.test.metrics.cdi;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.Meter;
+import io.smallrye.opentelemetry.test.InMemoryMetricExporter;
+
+@ExtendWith(ArquillianExtension.class)
+public class GaugeCdiTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @Inject
+ MeterBean meterBean;
+
+ @Inject
+ InMemoryMetricExporter exporter;
+
+ @BeforeEach
+ void setUp() {
+ exporter.reset();
+ }
+
+ @Test
+ void gauge() throws InterruptedException {
+ meterBean.getMeter()
+ .gaugeBuilder("jvm.memory.total")
+ .setDescription("Reports JVM memory usage.")
+ .setUnit("byte")
+ .buildWithCallback(
+ result -> result.record(Runtime.getRuntime().totalMemory(), Attributes.empty()));
+ exporter.assertCountAtLeast("jvm.memory.total", null, 1);
+ assertNotNull(exporter.getFinishedMetricItems("jvm.memory.total", null).get(0));
+ }
+
+ @Test
+ void meter() {
+ assertNotNull(meterBean.getMeter());
+ }
+
+ @ApplicationScoped
+ public static class MeterBean {
+ @Inject
+ Meter meter;
+
+ public Meter getMeter() {
+ return meter;
+ }
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/rest/RestMetricsTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/rest/RestMetricsTest.java
new file mode 100644
index 00000000..fa74ad2e
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/rest/RestMetricsTest.java
@@ -0,0 +1,159 @@
+package io.smallrye.opentelemetry.observation.test.metrics.rest;
+
+import static io.opentelemetry.sdk.metrics.data.MetricDataType.HISTOGRAM;
+import static io.restassured.RestAssured.given;
+import static io.smallrye.opentelemetry.test.InMemoryMetricExporter.getMostRecentPointsMap;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.hasProperty;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.instrumentation.api.internal.SemconvStability;
+import io.opentelemetry.sdk.metrics.data.HistogramPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.data.PointData;
+import io.smallrye.opentelemetry.test.InMemoryMetricExporter;
+
+@ExtendWith(ArquillianExtension.class)
+public class RestMetricsTest {
+
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @ArquillianResource
+ URL url;
+ @Inject
+ InMemoryMetricExporter metricExporter;
+
+ @AfterEach
+ void reset() {
+ // important, metrics continue to arrive after reset.
+ metricExporter.reset();
+ }
+
+ @Test
+ void metricAttributes() {
+ given().get("/span").then().statusCode(HTTP_OK);
+ given().get("/span/1").then().statusCode(HTTP_OK);
+ given().get("/span/2").then().statusCode(HTTP_OK);
+ given().get("/span/2").then().statusCode(HTTP_OK);
+
+ metricExporter.assertCountAtLeast("http.server.duration", "/span", 1);
+ metricExporter.assertCountAtLeast("http.server.duration", "/span/1", 1);
+ metricExporter.assertCountAtLeast("http.server.duration", "/span/2", 2);
+ List finishedMetricItems = metricExporter.getFinishedMetricItems("http.server.duration", null);
+
+ assertThat(finishedMetricItems, allOf(
+ everyItem(hasProperty("name", equalTo("http.server.duration"))),
+ everyItem(hasProperty("type", equalTo(HISTOGRAM)))));
+
+ Map pointDataMap = getMostRecentPointsMap(finishedMetricItems);
+ if (SemconvStability.emitOldHttpSemconv()) {
+ assertEquals(1, getCount(pointDataMap, "http.method:GET,http.route:/span,http.status_code:200"),
+ finishedMetricItems.toString());
+ assertEquals(1, getCount(pointDataMap, "http.method:GET,http.route:/span/1,http.status_code:200"),
+ finishedMetricItems.toString());
+ assertEquals(2, getCount(pointDataMap, "http.method:GET,http.route:/span/2,http.status_code:200"),
+ finishedMetricItems.toString());
+ } else {
+ assertEquals(1, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,http.route:/span"),
+ pointDataMap.keySet().stream()
+ .collect(Collectors.joining("**")));
+ assertEquals(1, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,http.route:/span/1"),
+ pointDataMap.keySet().stream()
+ .collect(Collectors.joining("**")));
+ assertEquals(2, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,http.route:/span/2"),
+ pointDataMap.keySet().stream()
+ .collect(Collectors.joining("**")));
+ }
+ }
+
+ private long getCount(final Map pointDataMap, final String key) {
+ HistogramPointData histogramPointData = (HistogramPointData) pointDataMap.get(key);
+ if (histogramPointData == null) {
+ return 0;
+ }
+ return histogramPointData.getCount();
+ }
+
+ @Test
+ void metrics() {
+ given().get("/span/12").then().statusCode(HTTP_OK);
+ metricExporter.assertCountAtLeast("queueSize", null, 1);
+ metricExporter.assertCountAtLeast("http.server.duration", "/span/12", 1);
+ metricExporter.assertCountAtLeast("http.server.active_requests", null, 1);
+ metricExporter.assertCountAtLeast("processedSpans", null, 1);
+ }
+
+ @Path("/")
+ public static class SpanResource {
+ @GET
+ @Path("/span")
+ public Response span() {
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/{name}")
+ public Response spanName(@PathParam(value = "name") String name) {
+ return Response.ok().build();
+ }
+
+ @POST
+ @Path("/span")
+ public Response spanPost(String payload) {
+ return Response.ok(payload).build();
+ }
+
+ @Path("/sub/{id}")
+ public SubResource subResource(@PathParam("id") String id) {
+ return new SubResource(id);
+ }
+ }
+
+ public static class SubResource {
+ private final String id;
+
+ public SubResource(final String id) {
+ this.id = id;
+ }
+
+ @GET
+ public Response get() {
+ return Response.ok().build();
+ }
+ }
+
+ @ApplicationPath("/")
+ public static class RestApplication extends Application {
+
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/cdi/TracerTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/cdi/TracerTest.java
new file mode 100644
index 00000000..4e53c417
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/cdi/TracerTest.java
@@ -0,0 +1,41 @@
+package io.smallrye.opentelemetry.observation.test.trace.cdi;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.api.trace.Tracer;
+
+@ExtendWith(ArquillianExtension.class)
+class TracerTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @Inject
+ TracerBean tracerBean;
+
+ @Test
+ void tracer() {
+ assertNotNull(tracerBean.getTracer());
+ }
+
+ @ApplicationScoped
+ public static class TracerBean {
+ @Inject
+ Tracer tracer;
+
+ public Tracer getTracer() {
+ return tracer;
+ }
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/ContextRootTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/ContextRootTest.java
new file mode 100644
index 00000000..29551efa
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/ContextRootTest.java
@@ -0,0 +1,71 @@
+package io.smallrye.opentelemetry.observation.test.trace.rest;
+
+import static io.opentelemetry.api.trace.SpanKind.SERVER;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
+import static io.restassured.RestAssured.given;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+import java.util.List;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.smallrye.opentelemetry.test.InMemorySpanExporter;
+
+@ExtendWith(ArquillianExtension.class)
+class ContextRootTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @ArquillianResource
+ URL url;
+ @Inject
+ InMemorySpanExporter spanExporter;
+
+ @BeforeEach
+ void setUp() {
+ spanExporter.reset();
+ }
+
+ @Test
+ void route() {
+ given().get("/application/resource/span").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ assertEquals(SERVER, spanItems.get(0).getKind());
+ assertEquals(url.getPath() + "application/resource/span", spanItems.get(0).getAttributes().get(HTTP_ROUTE));
+ }
+
+ @ApplicationPath("/application")
+ public static class RestApplication extends Application {
+
+ }
+
+ @Path("/resource")
+ public static class Resource {
+ @GET
+ @Path("/span")
+ public Response span() {
+ return Response.ok().build();
+ }
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestClientSpanTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestClientSpanTest.java
new file mode 100644
index 00000000..2e61aa13
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestClientSpanTest.java
@@ -0,0 +1,377 @@
+package io.smallrye.opentelemetry.observation.test.trace.rest;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+import static io.opentelemetry.api.trace.SpanKind.CLIENT;
+import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
+import static io.opentelemetry.api.trace.SpanKind.SERVER;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_FULL;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_PATH;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_SCHEME;
+import static io.smallrye.opentelemetry.observation.test.AttributeKeysStability.get;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.smallrye.opentelemetry.test.InMemorySpanExporter;
+
+@ExtendWith(ArquillianExtension.class)
+class RestClientSpanTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class)
+ .addAsResource(new StringAsset("client/mp-rest/url=${baseUri}"), "META-INF/microprofile-config.properties");
+ }
+
+ @ArquillianResource
+ URL url;
+ @Inject
+ InMemorySpanExporter spanExporter;
+ @Inject
+ @RestClient
+ SpanResourceClient client;
+
+ @BeforeEach
+ void setUp() {
+ spanExporter.reset();
+ }
+
+ @Test
+ void span() {
+ Response response = client.span();
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(2);
+
+ SpanData server = spans.get(0);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(1);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanName() {
+ Response response = client.spanName("1");
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(2);
+
+ SpanData server = spans.get(0);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/1", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(1);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/1", get(client, URL_FULL));
+
+ assertEquals(server.getTraceId(), client.getTraceId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanNameQuery() {
+ Response response = client.spanNameQuery("1", "query");
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(2);
+
+ SpanData server = spans.get(0);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/1?query=query", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(1);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/1?query=query", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanError() {
+ // Can't use REST Client here due to org.jboss.resteasy.microprofile.client.DefaultResponseExceptionMapper
+ WebTarget target = ClientBuilder.newClient().target(url.toString() + "span/error");
+ Response response = target.request().get();
+ assertEquals(response.getStatus(), HTTP_INTERNAL_ERROR);
+
+ List spans = spanExporter.getFinishedSpanItems(2);
+
+ SpanData server = spans.get(0);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/error", server.getName());
+ assertEquals(HTTP_INTERNAL_ERROR, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/error", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(1);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_INTERNAL_ERROR, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/error", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanChild() {
+ Response response = client.spanChild();
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(3);
+
+ SpanData internal = spans.get(0);
+ assertEquals(INTERNAL, internal.getKind());
+ assertEquals("SpanBean.spanChild", internal.getName());
+
+ SpanData server = spans.get(1);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/child", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/child", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(2);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/child", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), internal.getTraceId());
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(internal.getParentSpanId(), server.getSpanId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanCurrent() {
+ Response response = client.spanCurrent();
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(2);
+
+ SpanData server = spans.get(0);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/current", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/current", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+ assertEquals("tck.current.value", server.getAttributes().get(stringKey("tck.current.key")));
+
+ SpanData client = spans.get(1);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/current", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @Test
+ void spanNew() {
+ Response response = client.spanNew();
+ assertEquals(response.getStatus(), HTTP_OK);
+
+ List spans = spanExporter.getFinishedSpanItems(3);
+
+ SpanData internal = spans.get(0);
+ assertEquals(INTERNAL, internal.getKind());
+ assertEquals("span.new", internal.getName());
+ assertEquals("tck.new.value", internal.getAttributes().get(stringKey("tck.new.key")));
+
+ SpanData server = spans.get(1);
+ assertEquals(SERVER, server.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/new", server.getName());
+ assertEquals(HTTP_OK, get(server, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(server, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(server, URL_SCHEME));
+ assertEquals(url.getPath() + "span/new", get(server, URL_PATH));
+ assertEquals(url.getHost(), get(server, SERVER_ADDRESS));
+
+ SpanData client = spans.get(2);
+ assertEquals(CLIENT, client.getKind());
+ assertEquals("GET", client.getName());
+ assertEquals(HTTP_OK, get(client, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(client, HTTP_REQUEST_METHOD));
+ assertEquals(url.toString() + "span/new", get(client, URL_FULL));
+
+ assertEquals(client.getTraceId(), internal.getTraceId());
+ assertEquals(client.getTraceId(), server.getTraceId());
+ assertEquals(internal.getParentSpanId(), server.getSpanId());
+ assertEquals(server.getParentSpanId(), client.getSpanId());
+ }
+
+ @RequestScoped
+ @Path("/")
+ public static class SpanResource {
+ @Inject
+ SpanBean spanBean;
+ @Inject
+ Span span;
+ @Inject
+ Tracer tracer;
+
+ @GET
+ @Path("/span")
+ public Response span() {
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/{name}")
+ public Response spanName(@PathParam(value = "name") String name, @QueryParam("query") String query) {
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/error")
+ public Response spanError() {
+ return Response.serverError().build();
+ }
+
+ @GET
+ @Path("/span/child")
+ public Response spanChild() {
+ spanBean.spanChild();
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/current")
+ public Response spanCurrent() {
+ span.setAttribute("tck.current.key", "tck.current.value");
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/new")
+ public Response spanNew() {
+ Span span = tracer.spanBuilder("span.new")
+ .setSpanKind(INTERNAL)
+ .setParent(Context.current().with(this.span))
+ .setAttribute("tck.new.key", "tck.new.value")
+ .startSpan();
+
+ span.end();
+
+ return Response.ok().build();
+ }
+ }
+
+ @ApplicationScoped
+ public static class SpanBean {
+ @WithSpan
+ void spanChild() {
+
+ }
+ }
+
+ @RegisterRestClient(configKey = "client")
+ @Path("/")
+ public interface SpanResourceClient {
+ @GET
+ @Path("/span")
+ Response span();
+
+ @GET
+ @Path("/span/{name}")
+ Response spanName(@PathParam(value = "name") String name);
+
+ @GET
+ @Path("/span/{name}")
+ Response spanNameQuery(@PathParam(value = "name") String name, @QueryParam("query") String query);
+
+ @GET
+ @Path("/span/child")
+ Response spanChild();
+
+ @GET
+ @Path("/span/current")
+ Response spanCurrent();
+
+ @GET
+ @Path("/span/new")
+ Response spanNew();
+ }
+
+ @ApplicationPath("/")
+ public static class RestApplication extends Application {
+
+ }
+}
diff --git a/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestSpanTest.java b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestSpanTest.java
new file mode 100644
index 00000000..fde937ce
--- /dev/null
+++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestSpanTest.java
@@ -0,0 +1,204 @@
+package io.smallrye.opentelemetry.observation.test.trace.rest;
+
+import static io.opentelemetry.api.trace.SpanKind.SERVER;
+import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME;
+import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_VERSION;
+import static io.opentelemetry.semconv.SemanticAttributes.CLIENT_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
+import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
+import static io.opentelemetry.semconv.SemanticAttributes.NETWORK_PROTOCOL_NAME;
+import static io.opentelemetry.semconv.SemanticAttributes.NETWORK_PROTOCOL_VERSION;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_ADDRESS;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_DOMAIN;
+import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_PORT;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_PATH;
+import static io.opentelemetry.semconv.SemanticAttributes.URL_SCHEME;
+import static io.opentelemetry.semconv.SemanticAttributes.USER_AGENT_ORIGINAL;
+import static io.restassured.RestAssured.given;
+import static io.smallrye.opentelemetry.observation.test.AttributeKeysStability.get;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.net.URL;
+import java.util.List;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.smallrye.opentelemetry.api.OpenTelemetryConfig;
+import io.smallrye.opentelemetry.test.InMemorySpanExporter;
+
+@ExtendWith(ArquillianExtension.class)
+class RestSpanTest {
+ @Deployment
+ public static WebArchive createDeployment() {
+ return ShrinkWrap.create(WebArchive.class);
+ }
+
+ @ArquillianResource
+ URL url;
+ @Inject
+ InMemorySpanExporter spanExporter;
+
+ @BeforeEach
+ void setUp() {
+ spanExporter.reset();
+ }
+
+ @Test
+ void span() {
+ given().get("/span").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ SpanData span = spanItems.get(0);
+ assertEquals(SERVER, span.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span", span.getName());
+ assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ assertEquals("http", get(span, NETWORK_PROTOCOL_NAME));
+ assertEquals("1.1", get(span, NETWORK_PROTOCOL_VERSION));
+
+ assertEquals("tck", span.getResource().getAttribute(SERVICE_NAME));
+ assertEquals("1.0", span.getResource().getAttribute(SERVICE_VERSION));
+
+ InstrumentationScopeInfo libraryInfo = span.getInstrumentationScopeInfo();
+ assertEquals(OpenTelemetryConfig.INSTRUMENTATION_NAME, libraryInfo.getName());
+ assertEquals(OpenTelemetryConfig.INSTRUMENTATION_VERSION, libraryInfo.getVersion());
+ }
+
+ @Test
+ void spanName() {
+ given().get("/span/1").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ SpanData span = spanItems.get(0);
+ assertEquals(SERVER, span.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", span.getName());
+ assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ }
+
+ @Test
+ void spanNameWithoutQueryString() {
+ given().get("/span/1?id=1").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ SpanData span = spanItems.get(0);
+ assertEquals(SERVER, span.getKind());
+ assertEquals(HttpMethod.GET + " " + url.getPath() + "span/{name}", span.getName());
+ assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ assertEquals(url.getPath() + "span/1?id=1", get(span, URL_PATH));
+ assertEquals(url.getPath() + "span/{name}", span.getAttributes().get(HTTP_ROUTE));
+ }
+
+ @Test
+ void spanPost() {
+ given().body("payload").post("/span").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ SpanData span = spanItems.get(0);
+ assertEquals(SERVER, span.getKind());
+ assertEquals(HttpMethod.POST + " " + url.getPath() + "span", span.getName());
+
+ // Common Attributes
+ assertEquals(HttpMethod.POST, get(span, HTTP_REQUEST_METHOD));
+ assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ assertNotNull(span.getAttributes().get(USER_AGENT_ORIGINAL));
+ assertNull(span.getAttributes().get(SERVER_SOCKET_ADDRESS));
+ assertNull(span.getAttributes().get(SERVER_SOCKET_PORT));
+ assertNull(span.getAttributes().get(SERVER_SOCKET_DOMAIN));
+
+ // Server Attributes
+ assertEquals("http", get(span, URL_SCHEME));
+ assertEquals(url.getPath() + "span", get(span, URL_PATH));
+ assertEquals(url.getPath() + "span", span.getAttributes().get(HTTP_ROUTE));
+ assertNull(get(span, CLIENT_ADDRESS));
+ assertEquals(url.getHost(), get(span, SERVER_ADDRESS));
+ assertEquals(url.getPort(), get(span, SERVER_PORT));
+ }
+
+ @Test
+ void subResource() {
+ given().get("/sub/1").then().statusCode(HTTP_OK);
+
+ List spanItems = spanExporter.getFinishedSpanItems(1);
+ assertEquals(1, spanItems.size());
+ SpanData span = spanItems.get(0);
+ assertEquals(SERVER, span.getKind());
+ assertEquals(HttpMethod.GET, span.getName());
+ assertEquals(HTTP_OK, get(span, HTTP_RESPONSE_STATUS_CODE));
+ assertEquals(HttpMethod.GET, get(span, HTTP_REQUEST_METHOD));
+ }
+
+ @Path("/")
+ public static class SpanResource {
+ @GET
+ @Path("/span")
+ public Response span() {
+ return Response.ok().build();
+ }
+
+ @GET
+ @Path("/span/{name}")
+ public Response spanName(@PathParam(value = "name") String name) {
+ return Response.ok().build();
+ }
+
+ @POST
+ @Path("/span")
+ public Response spanPost(String payload) {
+ return Response.ok(payload).build();
+ }
+
+ @Path("/sub/{id}")
+ public SubResource subResource(@PathParam("id") String id) {
+ return new SubResource(id);
+ }
+ }
+
+ public static class SubResource {
+ private final String id;
+
+ public SubResource(final String id) {
+ this.id = id;
+ }
+
+ @GET
+ public Response get() {
+ return Response.ok().build();
+ }
+ }
+
+ @ApplicationPath("/")
+ public static class RestApplication extends Application {
+
+ }
+}
diff --git a/testsuite/observation/src/test/resources/META-INF/microprofile-config.properties b/testsuite/observation/src/test/resources/META-INF/microprofile-config.properties
new file mode 100644
index 00000000..3275386b
--- /dev/null
+++ b/testsuite/observation/src/test/resources/META-INF/microprofile-config.properties
@@ -0,0 +1,7 @@
+otel.traces.exporter=in-memory
+otel.metrics.exporter=in-memory
+otel.bsp.schedule.delay=100
+otel.metric.export.interval=100
+
+otel.service.name=tck
+otel.resource.attributes=service.version=1.0
diff --git a/testsuite/observation/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/testsuite/observation/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension
new file mode 100644
index 00000000..cbcad939
--- /dev/null
+++ b/testsuite/observation/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension
@@ -0,0 +1 @@
+io.smallrye.opentelemetry.observation.test.ArquillianExtension
\ No newline at end of file