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