From 664a2d95795f0476dabf3a2ac95307ea2d310b1a Mon Sep 17 00:00:00 2001 From: brunobat Date: Wed, 21 Feb 2024 15:09:03 +0000 Subject: [PATCH] New rest observation instrumentation and tests --- .../api/OpenTelemetryHandler.java | 5 +- .../observation-otel-bridge/pom.xml | 8 - .../observation/cdi/ObservationExtension.java | 5 +- .../ObservationRegistryProducer.java | 15 +- .../AbstractTracingObservationHandler.java | 23 ++ .../observation/handler/HandlerUtil.java | 6 + .../OpenTelemetryObservationHandler.java | 13 +- .../META-INF/microprofile-config.properties | 2 +- .../observation/ObservationOTelTest.java | 4 +- implementation/rest-observation/pom.xml | 45 +++ .../rest/observation/FilterDocumentation.java | 148 +++++++ .../observation/ObservationClientFilter.java | 161 ++++++++ .../observation/ObservationServerFilter.java | 93 +++++ .../rest/observation/ObservationUtil.java | 38 ++ .../client/ClientFilterConvention.java | 11 + .../client/DefaultClientFilterConvention.java | 103 +++++ .../client/ObservationClientContext.java | 34 ++ .../server/DefaultServerFilterConvention.java | 154 +++++++ .../server/ObservationServerContext.java | 43 ++ .../server/ServerFilterConvention.java | 11 + .../src/main/resources/META-INF/beans.xml | 8 + .../services/jakarta.ws.rs.ext.Providers | 2 + .../rest/OpenTelemetryClientFilter.java | 2 +- .../rest/OpenTelemetryServerFilter.java | 2 +- pom.xml | 11 + .../opentelemetry/test/InMemoryExporter.java | 110 ++++- .../test/trace/rest/RestClientSpanTest.java | 78 ++-- .../extra/test/trace/rest/RestSpanTest.java | 24 +- testsuite/observation/pom.xml | 108 +++++ .../observation/test/ArquillianExtension.java | 12 + .../observation/test/ArquillianLifecycle.java | 33 ++ .../test/AttributeKeysStability.java | 10 + .../observation/test/DeploymentProcessor.java | 19 + .../test/HttpServerAttributesFilter.java | 31 ++ .../observation/test/TestApplication.java | 91 +++++ .../observation/test/TestConfigSource.java | 26 ++ .../observation/test/baggage/BaggageTest.java | 71 ++++ .../test/metrics/cdi/GaugeCdiTest.java | 63 +++ .../test/metrics/rest/RestMetricsTest.java | 150 +++++++ .../test/trace/cdi/TracerTest.java | 41 ++ .../test/trace/rest/ContextRootTest.java | 71 ++++ .../test/trace/rest/RestClientSpanTest.java | 376 ++++++++++++++++++ .../test/trace/rest/RestSpanTest.java | 204 ++++++++++ .../META-INF/microprofile-config.properties | 7 + ...boss.arquillian.core.spi.LoadableExtension | 1 + testsuite/pom.xml | 1 + 46 files changed, 2387 insertions(+), 87 deletions(-) rename implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/{ => cdi}/ObservationRegistryProducer.java (84%) create mode 100644 implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/HandlerUtil.java create mode 100644 implementation/rest-observation/pom.xml create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/FilterDocumentation.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationClientFilter.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationServerFilter.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationUtil.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ClientFilterConvention.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/DefaultClientFilterConvention.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ObservationClientContext.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/DefaultServerFilterConvention.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ObservationServerContext.java create mode 100644 implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ServerFilterConvention.java create mode 100644 implementation/rest-observation/src/main/resources/META-INF/beans.xml create mode 100644 implementation/rest-observation/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers create mode 100644 testsuite/observation/pom.xml create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianExtension.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/ArquillianLifecycle.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/AttributeKeysStability.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/DeploymentProcessor.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/HttpServerAttributesFilter.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestApplication.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/TestConfigSource.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/baggage/BaggageTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/cdi/GaugeCdiTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/rest/RestMetricsTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/cdi/TracerTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/ContextRootTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestClientSpanTest.java create mode 100644 testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestSpanTest.java create mode 100644 testsuite/observation/src/test/resources/META-INF/microprofile-config.properties create mode 100644 testsuite/observation/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension diff --git a/api/src/main/java/io/smallrye/opentelemetry/api/OpenTelemetryHandler.java b/api/src/main/java/io/smallrye/opentelemetry/api/OpenTelemetryHandler.java index 4caef969..3f58490e 100644 --- a/api/src/main/java/io/smallrye/opentelemetry/api/OpenTelemetryHandler.java +++ b/api/src/main/java/io/smallrye/opentelemetry/api/OpenTelemetryHandler.java @@ -56,7 +56,10 @@ private static Severity toSeverity(final Level level) { } public static void install(final OpenTelemetry openTelemetry) { - Logger logger = openTelemetry.getLogsBridge().loggerBuilder(OpenTelemetryConfig.INSTRUMENTATION_NAME).build(); + Logger logger = openTelemetry.getLogsBridge() + .loggerBuilder(OpenTelemetryConfig.INSTRUMENTATION_NAME) + .setInstrumentationVersion(OpenTelemetryConfig.INSTRUMENTATION_VERSION) + .build(); LogManager.getLogManager().getLogger("").addHandler(new OpenTelemetryHandler(logger)); } } diff --git a/implementation/observation-otel-bridge/pom.xml b/implementation/observation-otel-bridge/pom.xml index c9daba79..f5fe46cf 100644 --- a/implementation/observation-otel-bridge/pom.xml +++ b/implementation/observation-otel-bridge/pom.xml @@ -12,14 +12,6 @@ smallrye-opentelemetry-observation-otel-bridge SmallRye OpenTelemetry: Observation to OpenTelemetry bridge - - 1.0.2 - ${project.build.sourceDirectory} - .* - ${project.build.directory}/observation-docs/ - - - io.smallrye.opentelemetry 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..dc7730f4 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,7 +20,6 @@ import jakarta.enterprise.inject.spi.Extension; import jakarta.enterprise.util.Nonbinding; -import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.annotation.Observed; public class ObservationExtension implements Extension { @@ -28,7 +27,9 @@ public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery beforeBeanDiscover 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/main/java/io/smallrye/opentelemetry/instrumentation/observation/ObservationRegistryProducer.java b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationRegistryProducer.java similarity index 84% rename from implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/ObservationRegistryProducer.java rename to implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationRegistryProducer.java index 78361244..cb7677cd 100644 --- a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/ObservationRegistryProducer.java +++ b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/cdi/ObservationRegistryProducer.java @@ -1,4 +1,4 @@ -package io.smallrye.opentelemetry.instrumentation.observation; +package io.smallrye.opentelemetry.instrumentation.observation.cdi; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; @@ -23,15 +23,12 @@ public class ObservationRegistryProducer { OpenTelemetry openTelemetry; @Inject - OpenTelemetryObservationHandler openTelemetryObservationHandler; - - @Inject - MeterRegistry registry; + MeterRegistry meterRegistry; @Produces @Singleton public ObservationRegistry registry() { - ObservationRegistry observationRegistry = ObservationRegistry.create(); + final ObservationRegistry observationRegistry = ObservationRegistry.create(); observationRegistry.observationConfig() // .observationFilter(new CloudObservationFilter()) // Where global filters go @@ -41,9 +38,9 @@ public ObservationRegistry registry() { openTelemetry.getPropagators().getTextMapPropagator()), new PropagatingReceiverTracingObservationHandler(tracer, openTelemetry.getPropagators().getTextMapPropagator()), - // new TracingAwareMeterObservationHandler(tracer) // For exemplars... Maybe not be needed - openTelemetryObservationHandler)) - .observationHandler(new DefaultMeterObservationHandler(registry)); + // new TracingAwareMeterObservationHandler(tracer) // For exemplars... + new OpenTelemetryObservationHandler(tracer))) + .observationHandler(new DefaultMeterObservationHandler(meterRegistry)); // .observationHandler(new PrintOutHandler()) // Can be implemented for debugging. Other handlers for future frameworks can also be added. return observationRegistry; } diff --git a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/AbstractTracingObservationHandler.java b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/AbstractTracingObservationHandler.java index 99ee4eb0..dec1c05c 100644 --- a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/AbstractTracingObservationHandler.java +++ b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/AbstractTracingObservationHandler.java @@ -3,6 +3,8 @@ import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_ADDR; import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_PORT; import static io.opentelemetry.semconv.SemanticAttributes.PEER_SERVICE; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.HIGH_CARD_ATTRIBUTES; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.LOW_CARD_ATTRIBUTES; import java.net.URI; import java.util.logging.Logger; @@ -108,16 +110,37 @@ protected Span getParentSpan(T context) { return null; } + @SuppressWarnings("unchecked") protected void tagSpan(T context, Span span) { + final Attributes highCardAttributes = context.get(HIGH_CARD_ATTRIBUTES); + setOtelAttributes(span, highCardAttributes); + + final Attributes lowCardAttributes = context.get(LOW_CARD_ATTRIBUTES); + setOtelAttributes(span, lowCardAttributes); + for (KeyValue keyValue : context.getAllKeyValues()) { if (!keyValue.getKey().equalsIgnoreCase("ERROR")) { span.setAttribute(keyValue.getKey(), keyValue.getValue()); + } else { span.recordException(new RuntimeException(keyValue.getValue())); } } } + private void setOtelAttributes(Span span, Attributes contextAttributes) { + if (contextAttributes != null) { + contextAttributes.forEach((key, value) -> { + // FIXME this is a bit of a hack because KeyValue only allows String values + if (key.getKey().equalsIgnoreCase("ERROR")) { + span.recordException(new RuntimeException(value.toString())); + } else { + span.setAttribute((AttributeKey) key, value); + } + }); + } + } + protected SpanBuilder remoteSpanBuilder(Kind kind, String remoteServiceName, String remoteServiceAddress, diff --git a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/HandlerUtil.java b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/HandlerUtil.java new file mode 100644 index 00000000..73da6097 --- /dev/null +++ b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/HandlerUtil.java @@ -0,0 +1,6 @@ +package io.smallrye.opentelemetry.instrumentation.observation.handler; + +public class HandlerUtil { + public static final String LOW_CARD_ATTRIBUTES = "low_card_attributes"; + public static final String HIGH_CARD_ATTRIBUTES = "high_card_attributes"; +} diff --git a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/OpenTelemetryObservationHandler.java b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/OpenTelemetryObservationHandler.java index cedb1eca..e47ca89e 100644 --- a/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/OpenTelemetryObservationHandler.java +++ b/implementation/observation-otel-bridge/src/main/java/io/smallrye/opentelemetry/instrumentation/observation/handler/OpenTelemetryObservationHandler.java @@ -2,9 +2,6 @@ import java.util.logging.Logger; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - import io.micrometer.common.util.StringUtils; import io.micrometer.observation.Observation; import io.opentelemetry.api.trace.Span; @@ -12,13 +9,14 @@ import io.opentelemetry.context.Context; import io.smallrye.opentelemetry.instrumentation.observation.context.TracingObservationContext; -@Singleton public class OpenTelemetryObservationHandler extends AbstractTracingObservationHandler { private static final Logger logger = Logger.getLogger(OpenTelemetryObservationHandler.class.getName()); + private final Tracer tracer; - @Inject - Tracer tracer; + public OpenTelemetryObservationHandler(Tracer tracer) { + this.tracer = tracer; + } @Override public void onStart(Observation.Context context) { @@ -52,7 +50,8 @@ private Span nextSpan(Tracer tracer, Span parent) { } private Span nextSpan(Tracer tracer) { - return tracer.spanBuilder("").startSpan(); + return tracer.spanBuilder("") + .startSpan(); } private String getSpanName(Observation.Context context) { diff --git a/implementation/observation-otel-bridge/src/main/resources/META-INF/microprofile-config.properties b/implementation/observation-otel-bridge/src/main/resources/META-INF/microprofile-config.properties index 97152c37..d3552576 100644 --- a/implementation/observation-otel-bridge/src/main/resources/META-INF/microprofile-config.properties +++ b/implementation/observation-otel-bridge/src/main/resources/META-INF/microprofile-config.properties @@ -1,2 +1,2 @@ -otel.logs.exporter=none +#otel.logs.exporter=none otel.metric.export.interval=1000 \ No newline at end of file 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..6aa03318 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,15 +38,13 @@ 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; import io.smallrye.opentelemetry.test.InMemoryExporterProducer; @EnableAutoWeld @AddExtensions({ OpenTelemetryExtension.class, ConfigExtension.class, ObservationExtension.class, MicrometerExtension.class }) -@AddBeanClasses({ OpenTelemetryConfigProducer.class, ObservationRegistryProducer.class, OpenTelemetryObservationHandler.class, +@AddBeanClasses({ OpenTelemetryConfigProducer.class, InMemoryExporter.class, InMemoryExporterProducer.class }) class ObservationOTelTest { @Inject diff --git a/implementation/rest-observation/pom.xml b/implementation/rest-observation/pom.xml new file mode 100644 index 00000000..bb84d514 --- /dev/null +++ b/implementation/rest-observation/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + io.smallrye.opentelemetry + smallrye-opentelemetry-parent + 2.8.2-SNAPSHOT + ../../pom.xml + + + smallrye-opentelemetry-rest-observation + SmallRye OpenTelemetry: REST Observation + + + + io.smallrye.opentelemetry + smallrye-opentelemetry-api + + + io.smallrye.opentelemetry + smallrye-opentelemetry-micrometer-otel-bridge + + + io.smallrye.opentelemetry + smallrye-opentelemetry-observation-otel-bridge + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + io.opentelemetry.semconv + opentelemetry-semconv + + + 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..f2897622 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/FilterDocumentation.java @@ -0,0 +1,148 @@ +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 getDefaultConvention() { + return DefaultServerFilterConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityValues.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return KeyName.merge(HighCardinalityValues.values(), ServerHighCardinalityValues.values()); + } + }, + CLIENT { + @Override + public Class getDefaultConvention() { + return DefaultClientFilterConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityValues.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return KeyName.merge(HighCardinalityValues.values(), ClientHighCardinalityValues.values()); + } + }; + + public enum LowCardinalityValues implements KeyName { + /** + * The HTTP method of the request. + */ + HTTP_REQUEST_METHOD { + @Override + public String asString() { + return "http.request.method"; + } + }, + 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 ServerHighCardinalityValues implements KeyName { + SERVER_PORT { + @Override + public String asString() { + return "server.port"; + } + }, + SERVER_ADDRESS { + @Override + public String asString() { + return "server.address"; + } + } + } + + public enum ClientHighCardinalityValues 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_PATH { + @Override + public String asString() { + return "url.path"; + } + }, + 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"; + } + }, + USER_AGENT_ORIGINAL { + @Override + public String asString() { + return "user_agent.original"; + } + } + } +} 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..5202f662 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationClientFilter.java @@ -0,0 +1,161 @@ +package io.smallrye.opentelemetry.implementation.rest.observation; + +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +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.semconv.http.HttpClientAttributesGetter; +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.CLIENT + .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().setResponse(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? + + 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..9f7ec288 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationServerFilter.java @@ -0,0 +1,93 @@ +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(); + // the observation req context can be obtained from the observation scope + 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().setResponse(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/ObservationUtil.java b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationUtil.java new file mode 100644 index 00000000..b7d9236d --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/ObservationUtil.java @@ -0,0 +1,38 @@ +package io.smallrye.opentelemetry.implementation.rest.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; + +public class ObservationUtil { + public static final String UNKNOWN = "UNKNOWN"; + public static final KeyValue STATUS_UNKNOWN = KeyValue + .of(FilterDocumentation.LowCardinalityValues.HTTP_RESPONSE_STATUS_CODE, UNKNOWN); + public static final KeyValue USER_AGENT_UNKNOWN = KeyValue + .of(FilterDocumentation.HighCardinalityValues.USER_AGENT_ORIGINAL, UNKNOWN); + public static final KeyValue QUERY_UNKNOWN = KeyValue + .of(FilterDocumentation.HighCardinalityValues.URL_QUERY, UNKNOWN); + + private ObservationUtil() { + //no content + } + + // FIXME this is a hack because KeyValue does not support non-string values + public static KeyValue collectAttribute(final AttributesBuilder attributesBuilder, + final KeyName keyName, + final Object value) { + if (value == null) { + return keyName.withValue(UNKNOWN); + } else if (value instanceof String) { + attributesBuilder.put(AttributeKey.stringKey(keyName.asString()), (String) value); + } else if (value instanceof Long) { + attributesBuilder.put(AttributeKey.longKey(keyName.asString()), (Long) value); + } else if (value instanceof Integer) { + attributesBuilder.put(AttributeKey.longKey(keyName.asString()), Long.valueOf((Integer) value)); + } else { + throw new IllegalArgumentException("Unsupported value type: " + value.getClass()); + } + return keyName.withValue(String.valueOf(value)); + } +} 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..66ab5c37 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/DefaultClientFilterConvention.java @@ -0,0 +1,103 @@ +package io.smallrye.opentelemetry.implementation.rest.observation.client; + +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.HighCardinalityValues; +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.LowCardinalityValues; +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.ClientHighCardinalityValues.CLIENT_ADDRESS; +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.ClientHighCardinalityValues.CLIENT_PORT; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.QUERY_UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.STATUS_UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.collectAttribute; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.HIGH_CARD_ATTRIBUTES; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.LOW_CARD_ATTRIBUTES; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +// 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.getCarrier(); + final ClientResponseContext responseContext = context.getResponse(); + + // Ideally, we should place data into KeyValues, however they only support String values and OTel uses other types. + // In order to simplify the setup we will generate data in the final form and place everything + // in the context for later retrieval in the handlers. + final AttributesBuilder attributesBuilder = Attributes.builder(); + // Duplicate data to keyValues, otherwise metrics will not be created properly + final KeyValues lowCardKeyValues = KeyValues.of( + collectAttribute( + attributesBuilder, + LowCardinalityValues.HTTP_REQUEST_METHOD, + requestContext.getMethod()), + collectAttribute( + attributesBuilder, + LowCardinalityValues.URL_SCHEME, + requestContext.getUri().getScheme()), + status(context, attributesBuilder)); + + context.put(LOW_CARD_ATTRIBUTES, attributesBuilder.build()); + return lowCardKeyValues; + } + + @Override + public KeyValues getHighCardinalityKeyValues(ObservationClientContext context) { + final ClientRequestContext requestContext = context.getCarrier(); + + // Ideally, we should place data into KeyValues, however they only support String values and OTel uses other types. + // In order to simplify the setup we will generate data in the final form and place everything + // in the context for later retrieval in the handlers. + final AttributesBuilder attributesBuilder = Attributes.builder(); + // Duplicate data to keyValues, otherwise metrics will not be created properly + final KeyValues highCardKeyValues = KeyValues.of( + collectAttribute(attributesBuilder, HighCardinalityValues.URL_PATH, requestContext.getUri().getPath()), + collectAttribute(attributesBuilder, CLIENT_PORT, Long.valueOf(requestContext.getUri().getPort())), + collectAttribute(attributesBuilder, CLIENT_ADDRESS, requestContext.getUri().getHost()), + collectAttribute(attributesBuilder, HighCardinalityValues.URL_FULL, requestContext.getUri().toString()), + query(context, attributesBuilder)); + + context.put(HIGH_CARD_ATTRIBUTES, attributesBuilder.build()); + return highCardKeyValues; + } + + private KeyValue query(ObservationClientContext context, AttributesBuilder attributesBuilder) { + final ClientRequestContext requestContext = context.getCarrier(); + final String query = requestContext.getUri().getQuery(); + if (query != null) { + return collectAttribute(attributesBuilder, HighCardinalityValues.URL_QUERY, query); + } + return QUERY_UNKNOWN; + } + + private KeyValue status(ObservationClientContext context, AttributesBuilder attributesBuilder) { + if (context.getResponse() != null) { + return collectAttribute(attributesBuilder, + LowCardinalityValues.HTTP_RESPONSE_STATUS_CODE, + context.getResponse().getStatus()); + } + return STATUS_UNKNOWN; + } + + @Override + public String getName() { + return "http.client"; + } + + @Override + public String getContextualName(ObservationClientContext context) { + final ClientRequestContext requestContext = context.getCarrier(); + 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..6bee9cf9 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/client/ObservationClientContext.java @@ -0,0 +1,34 @@ +package io.smallrye.opentelemetry.implementation.rest.observation.client; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; + +import io.micrometer.observation.transport.RequestReplySenderContext; + +public class ObservationClientContext extends RequestReplySenderContext { + + private final ClientRequestContext requestContext; + private ClientResponseContext responseContext; + + public ObservationClientContext(final ClientRequestContext requestContext) { + super((carrier, key, value) -> { + // This is a very naive approach that takes the first ConsumerRecord + carrier.getHeaders().add(key, value); + }); + this.requestContext = requestContext; + setCarrier(requestContext); + } + + // // fixme remove getters and setters + // 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..827016cf --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/DefaultServerFilterConvention.java @@ -0,0 +1,154 @@ +package io.smallrye.opentelemetry.implementation.rest.observation.server; + +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.LowCardinalityValues; +import static io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.ServerHighCardinalityValues; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.QUERY_UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.STATUS_UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.USER_AGENT_UNKNOWN; +import static io.smallrye.opentelemetry.implementation.rest.observation.ObservationUtil.collectAttribute; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.HIGH_CARD_ATTRIBUTES; +import static io.smallrye.opentelemetry.instrumentation.observation.handler.HandlerUtil.LOW_CARD_ATTRIBUTES; + +import java.lang.reflect.Method; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriBuilder; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.smallrye.opentelemetry.implementation.rest.observation.FilterDocumentation.HighCardinalityValues; + +public class DefaultServerFilterConvention implements ServerFilterConvention { + + public DefaultServerFilterConvention() { + } + + @Override + public KeyValues getLowCardinalityKeyValues(ObservationServerContext context) { + final ContainerRequestContext requestContext = context.getCarrier(); + + // Ideally, we should place data into KeyValues, however they only support String values and OTel uses other types. + // In order to simplify the setup we will generate data in the final form and place everything + // in the context for later retrieval in the handlers. + final AttributesBuilder attributesBuilder = Attributes.builder(); + // Duplicate data to keyValues, otherwise metrics will not be created properly + final KeyValues lowCardKeyValues = KeyValues.of( + collectAttribute(attributesBuilder, LowCardinalityValues.HTTP_REQUEST_METHOD, requestContext.getMethod()), + collectAttribute(attributesBuilder, LowCardinalityValues.HTTP_ROUTE, getHttpRoute(context)), + collectAttribute(attributesBuilder, LowCardinalityValues.URL_SCHEME, + requestContext.getUriInfo().getRequestUri().getScheme()), + collectAttribute(attributesBuilder, LowCardinalityValues.NETWORK_PROTOCOL_NAME, "http"), + collectAttribute(attributesBuilder, LowCardinalityValues.NETWORK_PROTOCOL_VERSION, UNKNOWN), + status(context, attributesBuilder)); + + context.put(LOW_CARD_ATTRIBUTES, attributesBuilder.build()); + return lowCardKeyValues; + } + + private KeyValue status(ObservationServerContext context, AttributesBuilder attributesBuilder) { + if (context.getResponse() != null) { + return collectAttribute(attributesBuilder, + LowCardinalityValues.HTTP_RESPONSE_STATUS_CODE, + context.getResponse().getStatus()); + } + return STATUS_UNKNOWN; + } + + @Override + public KeyValues getHighCardinalityKeyValues(ObservationServerContext context) { + final ContainerRequestContext requestContext = context.getCarrier(); + // Ideally, we should place data into KeyValues, however they only support String values and OTel uses other types. + // In order to simplify the setup we will generate data in the final form and place everything + // in the context for later retrieval in the handlers. + final AttributesBuilder attributesBuilder = Attributes.builder(); + // Duplicate data to keyValues, otherwise metrics will not be created properly + final KeyValues highCardKeyValues = KeyValues.of( + collectAttribute( + attributesBuilder, + HighCardinalityValues.URL_PATH, + requestContext.getUriInfo().getRequestUri().getPath()), + collectAttribute( + attributesBuilder, + ServerHighCardinalityValues.SERVER_PORT, + Long.valueOf(requestContext.getUriInfo().getRequestUri().getPort())), + collectAttribute( + attributesBuilder, + ServerHighCardinalityValues.SERVER_ADDRESS, + requestContext.getUriInfo().getRequestUri().getHost()), + agent(context, attributesBuilder), + urlQuery(context, attributesBuilder)); + + context.put(HIGH_CARD_ATTRIBUTES, attributesBuilder.build()); + return highCardKeyValues; + } + + private KeyValue urlQuery(ObservationServerContext context, AttributesBuilder attributesBuilder) { + if (context.getCarrier().getUriInfo().getRequestUri().getQuery() != null) { + context.getCarrier().getUriInfo().getRequestUri().getQuery(); + return collectAttribute( + attributesBuilder, + HighCardinalityValues.URL_QUERY, + context.getCarrier().getUriInfo().getRequestUri().getQuery()); + } + return QUERY_UNKNOWN; + } + + private KeyValue agent(ObservationServerContext context, AttributesBuilder attributesBuilder) { + String userAgent = extractUserAgent(context.getCarrier()); + if (userAgent != null) { + return collectAttribute( + attributesBuilder, + HighCardinalityValues.USER_AGENT_ORIGINAL, + userAgent); + } + return USER_AGENT_UNKNOWN; + } + + @Override + public String getName() { + return "http.server"; + } + + @Override + public String getContextualName(ObservationServerContext context) { + final ContainerRequestContext requestContext = context.getCarrier(); + 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.getCarrier().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; + } + } + + public String extractUserAgent(ContainerRequestContext requestContext) { + String userAgent = requestContext.getHeaderString(HttpHeaders.USER_AGENT); + if (userAgent != null) { + return userAgent; + } else { + 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..3d6cdb81 --- /dev/null +++ b/implementation/rest-observation/src/main/java/io/smallrye/opentelemetry/implementation/rest/observation/server/ObservationServerContext.java @@ -0,0 +1,43 @@ +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.transport.RequestReplyReceiverContext; + +public class ObservationServerContext extends RequestReplyReceiverContext { + private final ContainerRequestContext requestContext; + private final ResourceInfo resourceInfo; + private ContainerResponseContext responseContext; + + public ObservationServerContext(final ContainerRequestContext requestContext, final ResourceInfo resourceInfo) { + super((carrier, key) -> { + // This is a very naive approach that takes the first ConsumerRecord + String headerValue = carrier.getHeaders().getFirst(key); + if (headerValue != null) { + return headerValue; + } + return null; + }); + this.requestContext = requestContext; + setCarrier(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-observation/src/main/resources/META-INF/beans.xml b/implementation/rest-observation/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000..b630cdfb --- /dev/null +++ b/implementation/rest-observation/src/main/resources/META-INF/beans.xml @@ -0,0 +1,8 @@ + + + + diff --git a/implementation/rest-observation/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers b/implementation/rest-observation/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers new file mode 100644 index 00000000..cc29887d --- /dev/null +++ b/implementation/rest-observation/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers @@ -0,0 +1,2 @@ +io.smallrye.opentelemetry.implementation.rest.observation.ObservationServerFilter +io.smallrye.opentelemetry.implementation.rest.observation.ObservationClientFilter 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 2d14c804..1672b221 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()) + .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 a22ef376..bec09554 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? + .addOperationMetrics(HttpServerMetrics.get()) .addOperationMetrics(HttpServerExperimentalMetrics.get()) .buildServerInstrumenter(new ContainerRequestContextTextMapGetter()); } diff --git a/pom.xml b/pom.xml index dc982ee0..a9c2ccf7 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 @@ -196,6 +206,7 @@ implementation/rest implementation/micrometer-otel-bridge implementation/observation-otel-bridge + implementation/rest-observation test diff --git a/test/src/main/java/io/smallrye/opentelemetry/test/InMemoryExporter.java b/test/src/main/java/io/smallrye/opentelemetry/test/InMemoryExporter.java index 9630f4c2..335a4211 100644 --- a/test/src/main/java/io/smallrye/opentelemetry/test/InMemoryExporter.java +++ b/test/src/main/java/io/smallrye/opentelemetry/test/InMemoryExporter.java @@ -1,15 +1,22 @@ package io.smallrye.opentelemetry.test; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; import static java.util.Comparator.comparingLong; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; +import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -32,6 +39,11 @@ @ApplicationScoped public class InMemoryExporter { + + private static final List KEY_COMPONENTS = List.of(HTTP_REQUEST_METHOD.getKey(), + HTTP_ROUTE.getKey(), + HTTP_RESPONSE_STATUS_CODE.getKey()); + @Inject InMemorySpanExporter spanExporter; @Inject @@ -41,7 +53,9 @@ public class InMemoryExporter { public List getFinishedSpanItems(final int count) { assertSpanCount(count); - return spanExporter.getFinishedSpanItems().stream().sorted(comparingLong(SpanData::getStartEpochNanos).reversed()) + return spanExporter.getFinishedSpanItems().stream() + .sorted(comparingLong(SpanData::getStartEpochNanos) + .reversed()) .collect(toList()); } @@ -54,9 +68,9 @@ public MetricData getFinishedMetricItem(final String name) { return metrics(name).get(1); } - public MetricData getFinishedMetricItem(final String name, final String route) { - assertMetricsAtLeast(1, name, route); - return metrics(name).route(route).get(1); + public List getFinishedMetricItemList(final String name) { + assertMetricsAtLeast(1, name); + return metrics(name).metricData.collect(Collectors.toList()); } public void assertMetricsAtLeast(final int count, final String name) { @@ -64,7 +78,7 @@ public void assertMetricsAtLeast(final int count, final String name) { } public void assertMetricsAtLeast(final int count, final String name, final String route) { - await().atMost(5, SECONDS).untilAsserted(() -> assertTrue(metrics(name).route(route).count() >= count)); + await().atMost(10, SECONDS).untilAsserted(() -> assertTrue(metrics(name).route(route).count() >= count)); } public MetricData getFinishedHistogramItem(final String name, final int count) { @@ -83,6 +97,36 @@ public void reset() { logRecordExporter.reset(); } + /* + * ignore points with /export in the route + */ + private static boolean notExporterPointData(PointData pointData) { + return pointData.getAttributes().asMap().entrySet().stream() + .noneMatch(entry -> entry.getKey().getKey().equals(HTTP_ROUTE.getKey()) && + entry.getValue().toString().contains("/export")); + } + + public Map getMostRecentPointsMap(List finishedMetricItems) { + return finishedMetricItems.stream() + .flatMap(metricData -> metricData.getData().getPoints().stream()) + // exclude data from /export endpoint + .filter(InMemoryExporter::notExporterPointData) + // newer first + .sorted(Comparator.comparingLong(PointData::getEpochNanos).reversed()) + .collect(toMap( + pointData -> pointData.getAttributes().asMap().entrySet().stream() + //valid attributes for the resulting map key + .filter(entry -> KEY_COMPONENTS.contains(entry.getKey().getKey())) + // ensure order + .sorted(Comparator.comparing(o -> o.getKey().getKey())) + // build key + .map(entry -> entry.getKey().getKey() + ":" + entry.getValue().toString()) + .collect(joining(",")), + pointData -> pointData, + // most recent points will surface + (older, newer) -> newer)); + } + private class MetricDataFilter { private Stream metricData; @@ -148,6 +192,62 @@ public boolean test(final PointData pointData) { return this; } + MetricDataFilter path(final String path) { + metricData = metricData.map(new Function() { + @Override + public MetricData apply(final MetricData metricData) { + return new MetricData() { + @Override + public Resource getResource() { + return metricData.getResource(); + } + + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return metricData.getInstrumentationScopeInfo(); + } + + @Override + public String getName() { + return metricData.getName(); + } + + @Override + public String getDescription() { + return metricData.getDescription(); + } + + @Override + public String getUnit() { + return metricData.getUnit(); + } + + @Override + public MetricDataType getType() { + return metricData.getType(); + } + + @Override + public Data getData() { + return new Data() { + @Override + public Collection getPoints() { + return metricData.getData().getPoints().stream().filter(new Predicate() { + @Override + public boolean test(final PointData pointData) { + String value = pointData.getAttributes().get(URL_PATH); + return value != null && value.equals(path); + } + }).collect(Collectors.toSet()); + } + }; + } + }; + } + }); + return this; + } + int count() { return metricData.map(this::count) .mapToInt(Integer::intValue) 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..9a38d4e4 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; @@ -60,6 +61,7 @@ import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.semconv.ErrorAttributes; +import io.smallrye.opentelemetry.extra.test.AttributeKeysStability; import io.smallrye.opentelemetry.test.InMemoryExporter; @ExtendWith(ArquillianExtension.class) @@ -103,9 +105,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 +123,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 +160,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 +222,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 +251,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 +284,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..a845cb57 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; @@ -54,6 +55,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.semconv.ErrorAttributes; import io.smallrye.opentelemetry.api.OpenTelemetryConfig; +import io.smallrye.opentelemetry.extra.test.AttributeKeysStability; import io.smallrye.opentelemetry.test.InMemoryExporter; @ExtendWith(ArquillianExtension.class) @@ -115,8 +117,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 +180,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 +204,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..3a457b40 --- /dev/null +++ b/testsuite/observation/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + io.smallrye.opentelemetry + smallrye-opentelemetry-testsuite + 2.8.2-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-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..7b3bc98c --- /dev/null +++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/AttributeKeysStability.java @@ -0,0 +1,10 @@ +package io.smallrye.opentelemetry.observation.test; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.trace.data.SpanData; + +public class AttributeKeysStability { + public static T get(SpanData spanData, AttributeKey key) { + return spanData.getAttributes().get((AttributeKey) 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..6c21992b --- /dev/null +++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/DeploymentProcessor.java @@ -0,0 +1,19 @@ +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; + +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.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..147cc9e6 --- /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.InMemoryExporter; + +@ExtendWith(ArquillianExtension.class) +class BaggageTest { + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class); + } + + @ArquillianResource + URL url; + @Inject + InMemoryExporter 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..984ad617 --- /dev/null +++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/cdi/GaugeCdiTest.java @@ -0,0 +1,63 @@ +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.InMemoryExporter; + +@ExtendWith(ArquillianExtension.class) +public class GaugeCdiTest { + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class); + } + + @Inject + MeterBean meterBean; + + @Inject + InMemoryExporter exporter; + + @BeforeEach + void setUp() { + exporter.reset(); + } + + @Test + void gauge() throws InterruptedException { + meterBean.getMeter() + .gaugeBuilder("jvm.memory.total") + .setDescription("Reports JVM memory usage. -> Manual") + .setUnit("byte") + .buildWithCallback( + result -> result.record(Runtime.getRuntime().totalMemory(), Attributes.empty())); + exporter.assertMetricsAtLeast(1, "jvm.memory.total"); + } + + @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..3b0ca41b --- /dev/null +++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/metrics/rest/RestMetricsTest.java @@ -0,0 +1,150 @@ +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 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.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.smallrye.opentelemetry.test.InMemoryExporter; + +@ExtendWith(ArquillianExtension.class) +public class RestMetricsTest { + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class); + } + + @ArquillianResource + URL url; + + @Inject + InMemoryExporter 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); + given().get("/span/2").then().statusCode(HTTP_OK); + + metricExporter.assertMetricsAtLeast(1, "http.server.active.duration", url.getPath() + "span"); + metricExporter.assertMetricsAtLeast(4, "http.server.active.duration", url.getPath() + "span/{name}"); +// metricExporter.assertMetricsAtLeast(3, "http.server.active.duration", url.getPath() + "span/2"); + List finishedMetricItems = metricExporter.getFinishedMetricItemList("http.server.duration"); + + assertThat(finishedMetricItems, allOf( + everyItem(hasProperty("name", equalTo("http.server.active.duration"))), + everyItem(hasProperty("type", equalTo(HISTOGRAM))))); + + Map pointDataMap = metricExporter.getMostRecentPointsMap(finishedMetricItems); + assertEquals(1, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,url.path:/span"), + pointDataMap.keySet().stream() + .collect(Collectors.joining("**"))); + assertEquals(1, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,url.path:/span/1"), + pointDataMap.keySet().stream() + .collect(Collectors.joining("**"))); + assertEquals(3, getCount(pointDataMap, "http.request.method:GET,http.response.status_code:200,url.path:/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.assertMetricsAtLeast(1, "queueSize"); + metricExporter.assertMetricsAtLeast(1, "http.server.active.duration", url.getPath() + "span/{name}"); + // metricExporter.assertMetricsAtLeast(1, "http.server.active_requests"); + metricExporter.assertMetricsAtLeast(1, "processedSpans"); + } + + @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..90b8ee92 --- /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.InMemoryExporter; + +@ExtendWith(ArquillianExtension.class) +class ContextRootTest { + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class); + } + + @ArquillianResource + URL url; + @Inject + InMemoryExporter 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..f442a2f9 --- /dev/null +++ b/testsuite/observation/src/test/java/io/smallrye/opentelemetry/observation/test/trace/rest/RestClientSpanTest.java @@ -0,0 +1,376 @@ +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.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; +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.InMemoryExporter; + +@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 + InMemoryExporter 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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(1); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_OK, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/1", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(1); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_OK, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/1", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/1", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(1); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_OK, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/1?query=query", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/error", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(1); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_INTERNAL_ERROR, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/error", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/child", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(2); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_OK, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/child", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/current", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(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, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/current", client.getAttributes().get(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, server.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, server.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", server.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span/new", server.getAttributes().get(URL_PATH)); + assertEquals(url.getHost(), server.getAttributes().get(SERVER_ADDRESS)); + + SpanData client = spans.get(2); + assertEquals(CLIENT, client.getKind()); + assertEquals("GET", client.getName()); + assertEquals(HTTP_OK, client.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, client.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.toString() + "span/new", client.getAttributes().get(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..9de49b16 --- /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.ClientAttributes.CLIENT_ADDRESS; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_LOCAL_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_LOCAL_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_NAME; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_VERSION; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.UrlAttributes.URL_QUERY; +import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; +import static io.opentelemetry.semconv.UserAgentAttributes.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.InMemoryExporter; + +@ExtendWith(ArquillianExtension.class) +class RestSpanTest { + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class); + } + + @ArquillianResource + URL url; + @Inject + InMemoryExporter 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, span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, span.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals("http", span.getAttributes().get(NETWORK_PROTOCOL_NAME)); + assertEquals("UNKNOWN", span.getAttributes().get(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()); FIXME + } + + @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, span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, span.getAttributes().get(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, span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, span.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(url.getPath() + "span/1", get(span, URL_PATH)); + assertEquals("id=1", get(span, URL_QUERY)); + 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, span.getAttributes().get(HTTP_REQUEST_METHOD)); + assertEquals(HTTP_OK, span.getAttributes().get(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", span.getAttributes().get(URL_SCHEME)); + assertEquals(url.getPath() + "span", span.getAttributes().get(URL_PATH)); + assertEquals(url.getPath() + "span", span.getAttributes().get(HTTP_ROUTE)); + assertNull(span.getAttributes().get(CLIENT_ADDRESS)); + assertEquals(url.getHost(), span.getAttributes().get(SERVER_ADDRESS)); + assertEquals(url.getPort(), span.getAttributes().get(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, span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + assertEquals(HttpMethod.GET, span.getAttributes().get(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 diff --git a/testsuite/pom.xml b/testsuite/pom.xml index a3259ba1..c23de6b3 100644 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -34,6 +34,7 @@ extra + observation tck