From ca310b4ddb6d634b46c3e7a587ef476df56f047a Mon Sep 17 00:00:00 2001 From: Mateusz Rzeszutek Date: Thu, 12 Jan 2023 02:22:55 +0100 Subject: [PATCH] Support Spring Web MVC in library instrumentation (#7552) Part of https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/7312 This is pretty much a copy of the `spring-webvmc-5.3:library` module with `s/javax/jakarta/` applied. I'm planning on removing the 5.3 instrumentation after #7312 is done. --- docs/supported-libraries.md | 2 +- .../spring-webmvc-6.0/library/README.md | 94 +++++++++++ .../library/build.gradle.kts | 17 ++ .../spring/webmvc/v6_0/HttpRouteSupport.java | 153 ++++++++++++++++++ .../v6_0/JakartaHttpServletRequestGetter.java | 24 +++ .../SpringWebMvcHttpAttributesGetter.java | 101 ++++++++++++ .../v6_0/SpringWebMvcNetAttributesGetter.java | 53 ++++++ .../webmvc/v6_0/SpringWebMvcTelemetry.java | 42 +++++ .../v6_0/SpringWebMvcTelemetryBuilder.java | 95 +++++++++++ .../v6_0/WebMvcTelemetryProducingFilter.java | 79 +++++++++ .../webmvc/v6_0/TestWebSpringBootApp.java | 130 +++++++++++++++ .../webmvc/v6_0/WebMvcHttpServerTest.java | 48 ++++++ settings.gradle.kts | 1 + 13 files changed, 838 insertions(+), 1 deletion(-) create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetry.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index e4266c8049..bcb7fbf8dc 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -116,7 +116,7 @@ These are the supported libraries and frameworks: | [Spring RabbitMQ](https://spring.io/projects/spring-amqp) | 1.0+ | N/A | [Messaging Spans] | | [Spring Scheduling](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/package-summary.html) | 3.1+ | N/A | none | | [Spring RestTemplate](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/package-summary.html) | 3.1+ | [opentelemetry-spring-web-3.1](../instrumentation/spring/spring-web/spring-web-3.1/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [Spring Web MVC](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/package-summary.html) | 3.1+ | [opentelemetry-spring-webmvc-5.3](../instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library) | [HTTP Server Spans], [HTTP Server Metrics] | +| [Spring Web MVC](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/package-summary.html) | 3.1+ | [opentelemetry-spring-webmvc-5.3](../instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library),
[opentelemetry-spring-webmvc-6.0](../instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library) | [HTTP Server Spans], [HTTP Server Metrics] | | [Spring Web Services](https://spring.io/projects/spring-ws) | 2.0+ | N/A | none | | [Spring WebFlux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.0+ (not including 6.0+ yet) | [opentelemetry-spring-webflux-5.0](../instrumentation/spring/spring-webflux-5.0/library) | [HTTP Client Spans], [HTTP Client Metrics], | | [Spymemcached](https://github.com/couchbase/spymemcached) | 2.12+ | N/A | [Database Client Spans] | diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md new file mode 100644 index 0000000000..e0809c70f4 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md @@ -0,0 +1,94 @@ +# Library Instrumentation for Spring Web MVC version 6.0.0 and higher + +Provides OpenTelemetry instrumentation for Spring WebMVC controllers. + +## Quickstart + +### Add these dependencies to your project + +Replace `SPRING_VERSION` with the version of spring you're using. + +- `Minimum version: 6.0.0` + +Replace `OPENTELEMETRY_VERSION` with the [latest +release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-spring-webmvc-6.0). + +For Maven add the following to your `pom.xml`: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-webmvc-6.0 + OPENTELEMETRY_VERSION + + + + + + io.opentelemetry + opentelemetry-exporter-logging + OPENTELEMETRY_VERSION + + + + + + org.springframework + spring-webmvc + SPRING_VERSION + + + +``` + +For Gradle add the following to your dependencies: + +```groovy + +// OpenTelemetry instrumentation +implementation("io.opentelemetry.instrumentation:opentelemetry-spring-webmvc-6.0:OPENTELEMETRY_VERSION") + +// OpenTelemetry exporter +// replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/jaeger/..) +implementation("io.opentelemetry:opentelemetry-exporter-logging:OPENTELEMETRY_VERSION") + +// required to instrument Spring WebMVC +// this artifact should already be present in your application +implementation("org.springframework:spring-webmvc:SPRING_VERSION") +``` + +### Features + +#### `SpringWebMvcTelemetry` + +`SpringWebMvcTelemetry` enables creating OpenTelemetry server spans around HTTP requests processed +by the Spring servlet container. + +##### Usage in Spring Boot + +Spring Boot allows servlet `Filter`s to be registered as beans: + +```java +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.webmvc.v6_0.SpringWebMvcTelemetry; +import jakarta.servlet.Filter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringWebMvcTelemetryConfiguration { + + @Bean + public Filter telemetryFilter(OpenTelemetry openTelemetry) { + return SpringWebMvcTelemetry.create(openTelemetry).createServletFilter(); + } +} +``` + +### Starter Guide + +Check +out [OpenTelemetry Manual Instrumentation](https://opentelemetry.io/docs/instrumentation/java/manual/) +to learn more about using the OpenTelemetry API to instrument your code. diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts new file mode 100644 index 0000000000..4fb5e4f64a --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + compileOnly("org.springframework:spring-webmvc:6.0.0") + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") + + testImplementation(project(":testing-common")) + testImplementation("org.springframework.boot:spring-boot-starter-web:3.0.0") + testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.0") +} + +// spring 6 requires java 17 +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java new file mode 100644 index 0000000000..b0e1926cae --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import static java.util.Objects.requireNonNull; +import static org.springframework.web.util.ServletRequestPathUtils.PATH_ATTRIBUTE; + +import io.opentelemetry.context.Context; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; + +final class HttpRouteSupport { + + private final AtomicBoolean contextRefreshTriggered = new AtomicBoolean(); + @Nullable private volatile DispatcherServlet dispatcherServlet; + @Nullable private volatile List handlerMappings; + private volatile boolean parseRequestPath; + + void onFilterInit(FilterConfig filterConfig) { + WebApplicationContext context = + WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext()); + if (!(context instanceof ConfigurableWebApplicationContext)) { + return; + } + + DispatcherServlet servlet = context.getBeanProvider(DispatcherServlet.class).getIfAvailable(); + if (servlet != null) { + dispatcherServlet = servlet; + + ((ConfigurableWebApplicationContext) context) + .addApplicationListener(new WebContextRefreshListener()); + } + } + + // we can't retrieve the handler mappings from the DispatcherServlet in the onRefresh listener, + // because it loads them just after the application context refreshed event is processed + // to work around this, we're setting a boolean flag that'll cause this filter to load the + // mappings the next time it attempts to set the http.route + final class WebContextRefreshListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + contextRefreshTriggered.set(true); + } + } + + boolean hasMappings() { + if (contextRefreshTriggered.compareAndSet(true, false)) { + // reload the handler mappings only if the web app context was recently refreshed + Optional.ofNullable(dispatcherServlet) + .map(DispatcherServlet::getHandlerMappings) + .ifPresent(this::setHandlerMappings); + } + return handlerMappings != null; + } + + private void setHandlerMappings(List mappings) { + List handlerMappings = new ArrayList<>(); + for (HandlerMapping mapping : mappings) { + // Originally we ran findMapping at the very beginning of the request. This turned out to have + // application-crashing side-effects with grails. That is why we don't add all HandlerMapping + // classes here. Although now that we run findMapping after the request, and only when server + // span name has not been updated by a controller, the probability of bad side-effects is much + // reduced even if we did add all HandlerMapping classes here. + if (mapping instanceof RequestMappingHandlerMapping) { + handlerMappings.add(mapping); + if (mapping.usesPathPatterns()) { + this.parseRequestPath = true; + } + } + } + if (!handlerMappings.isEmpty()) { + this.handlerMappings = handlerMappings; + } + } + + @Nullable + String getHttpRoute(Context context, HttpServletRequest request) { + boolean parsePath = this.parseRequestPath; + Object previousValue = null; + if (parsePath) { + previousValue = request.getAttribute(PATH_ATTRIBUTE); + // sets new value for PATH_ATTRIBUTE of request + ServletRequestPathUtils.parseAndCache(request); + } + try { + if (findMapping(request)) { + // Name the parent span based on the matching pattern + // Let the parent span resource name be set with the attribute set in findMapping. + Object bestMatchingPattern = + request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (bestMatchingPattern != null) { + return prependContextPath(request, bestMatchingPattern.toString()); + } + } + } finally { + // mimic spring DispatcherServlet and restore the previous value of PATH_ATTRIBUTE + if (parsePath) { + if (previousValue == null) { + request.removeAttribute(PATH_ATTRIBUTE); + } else { + request.setAttribute(PATH_ATTRIBUTE, previousValue); + } + } + } + return null; + } + + /** + * When a HandlerMapping matches a request, it sets HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE + * as an attribute on the request. This attribute set as the HTTP route. + */ + private boolean findMapping(HttpServletRequest request) { + try { + // handlerMapping already null-checked above + for (HandlerMapping mapping : requireNonNull(handlerMappings)) { + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return true; + } + } + } catch (Exception ignored) { + // mapping.getHandler() threw exception. Ignore + } + return false; + } + + private static String prependContextPath(HttpServletRequest request, String route) { + String contextPath = request.getContextPath(); + if (contextPath == null) { + return route; + } + return contextPath + (route.startsWith("/") ? route : ("/" + route)); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java new file mode 100644 index 0000000000..2955fb5d7b --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import io.opentelemetry.context.propagation.TextMapGetter; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; + +enum JakartaHttpServletRequestGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(HttpServletRequest carrier) { + return Collections.list(carrier.getHeaderNames()); + } + + @Override + public String get(HttpServletRequest carrier, String key) { + return carrier.getHeader(key); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java new file mode 100644 index 0000000000..7039d75b26 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import javax.annotation.Nullable; + +enum SpringWebMvcHttpAttributesGetter + implements HttpServerAttributesGetter { + INSTANCE; + + @Override + @Nullable + public String method(HttpServletRequest request) { + return request.getMethod(); + } + + @Override + public List requestHeader(HttpServletRequest request, String name) { + Enumeration headers = request.getHeaders(name); + return headers == null ? Collections.emptyList() : Collections.list(headers); + } + + @Override + @Nullable + public String flavor(HttpServletRequest request) { + String flavor = request.getProtocol(); + if (flavor == null) { + return null; + } + if (flavor.startsWith("HTTP/")) { + flavor = flavor.substring("HTTP/".length()); + } + return flavor; + } + + @Override + public Integer statusCode( + HttpServletRequest request, HttpServletResponse response, @Nullable Throwable error) { + + int statusCode; + // if response is not committed and there is a throwable set status to 500 / + // INTERNAL_SERVER_ERROR, due to servlet spec + // https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf: + // "If a servlet generates an error that is not handled by the error page mechanism as + // described above, the container must ensure to send a response with status 500." + if (!response.isCommitted() && error != null) { + statusCode = 500; + } else { + statusCode = response.getStatus(); + } + + return statusCode; + } + + @Override + public List responseHeader( + HttpServletRequest request, HttpServletResponse response, String name) { + Collection headers = response.getHeaders(name); + if (headers == null) { + return Collections.emptyList(); + } + if (headers instanceof List) { + return (List) headers; + } + return new ArrayList<>(headers); + } + + @Override + @Nullable + public String target(HttpServletRequest request) { + String target = request.getRequestURI(); + String queryString = request.getQueryString(); + if (queryString != null) { + target += "?" + queryString; + } + return target; + } + + @Override + @Nullable + public String route(HttpServletRequest request) { + return null; + } + + @Override + @Nullable + public String scheme(HttpServletRequest request) { + return request.getScheme(); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java new file mode 100644 index 0000000000..982526d699 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.servlet.http.HttpServletRequest; +import javax.annotation.Nullable; + +enum SpringWebMvcNetAttributesGetter implements NetServerAttributesGetter { + INSTANCE; + + @Override + public String transport(HttpServletRequest request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Nullable + @Override + public String hostName(HttpServletRequest request) { + return request.getServerName(); + } + + @Override + public Integer hostPort(HttpServletRequest request) { + return request.getServerPort(); + } + + @Override + @Nullable + public String sockPeerAddr(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + @Override + public Integer sockPeerPort(HttpServletRequest request) { + return request.getRemotePort(); + } + + @Nullable + @Override + public String sockHostAddr(HttpServletRequest request) { + return request.getLocalAddr(); + } + + @Override + public Integer sockHostPort(HttpServletRequest request) { + return request.getLocalPort(); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetry.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetry.java new file mode 100644 index 0000000000..3125731c8a --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetry.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** Entrypoint for instrumenting Spring Web MVC apps. */ +public final class SpringWebMvcTelemetry { + + /** + * Returns a new {@link SpringWebMvcTelemetry} configured with the given {@link OpenTelemetry}. + */ + public static SpringWebMvcTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link SpringWebMvcTelemetryBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static SpringWebMvcTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new SpringWebMvcTelemetryBuilder(openTelemetry); + } + + private final Instrumenter instrumenter; + + SpringWebMvcTelemetry(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + /** Returns a new {@link Filter} that generates telemetry for received HTTP requests. */ + public Filter createServletFilter() { + return new WebMvcTelemetryProducingFilter(instrumenter); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java new file mode 100644 index 0000000000..2e9188aff1 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; +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 jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** A builder of {@link SpringWebMvcTelemetry}. */ +public final class SpringWebMvcTelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-webmvc-6.0"; + + private final OpenTelemetry openTelemetry; + private final List> + additionalExtractors = new ArrayList<>(); + private final HttpServerAttributesExtractorBuilder + httpAttributesExtractorBuilder = + HttpServerAttributesExtractor.builder( + SpringWebMvcHttpAttributesGetter.INSTANCE, SpringWebMvcNetAttributesGetter.INSTANCE); + + SpringWebMvcTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + /** + * Configures the HTTP request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Returns a new {@link SpringWebMvcTelemetry} with the settings of this {@link + * SpringWebMvcTelemetryBuilder}. + */ + public SpringWebMvcTelemetry build() { + SpringWebMvcHttpAttributesGetter httpAttributesGetter = + SpringWebMvcHttpAttributesGetter.INSTANCE; + + Instrumenter instrumenter = + Instrumenter.builder( + openTelemetry, + INSTRUMENTATION_NAME, + HttpSpanNameExtractor.create(httpAttributesGetter)) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor(httpAttributesExtractorBuilder.build()) + .addAttributesExtractors(additionalExtractors) + .addOperationMetrics(HttpServerMetrics.get()) + .addContextCustomizer(HttpRouteHolder.get()) + .buildServerInstrumenter(JakartaHttpServletRequestGetter.INSTANCE); + + return new SpringWebMvcTelemetry(instrumenter); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java new file mode 100644 index 0000000000..4047ef7549 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.core.Ordered; +import org.springframework.web.filter.OncePerRequestFilter; + +final class WebMvcTelemetryProducingFilter extends OncePerRequestFilter implements Ordered { + + private final Instrumenter instrumenter; + private final HttpRouteSupport httpRouteSupport = new HttpRouteSupport(); + + WebMvcTelemetryProducingFilter( + Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + @Override + public void afterPropertiesSet() { + // don't do anything, in particular do not call initFilterBean() + } + + @Override + protected void initFilterBean() { + // FilterConfig must be non-null at this point + httpRouteSupport.onFilterInit(requireNonNull(getFilterConfig())); + } + + @Override + public void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + Context parentContext = Context.current(); + if (!instrumenter.shouldStart(parentContext, request)) { + filterChain.doFilter(request, response); + return; + } + + Context context = instrumenter.start(parentContext, request); + Throwable error = null; + try (Scope ignored = context.makeCurrent()) { + filterChain.doFilter(request, response); + } catch (Throwable t) { + error = t; + throw t; + } finally { + if (httpRouteSupport.hasMappings()) { + HttpRouteHolder.updateHttpRoute( + context, CONTROLLER, httpRouteSupport::getHttpRoute, request); + } + instrumenter.end(context, request, response, error); + } + } + + @Override + public void destroy() {} + + @Override + public int getOrder() { + // Run after all HIGHEST_PRECEDENCE items + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java new file mode 100644 index 0000000000..28e3795c24 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest.controller; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import jakarta.servlet.Filter; +import java.util.Properties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.view.RedirectView; + +@SpringBootApplication +class TestWebSpringBootApp { + + static ConfigurableApplicationContext start(int port, String contextPath) { + Properties props = new Properties(); + props.put("server.port", port); + props.put("server.servlet.contextPath", contextPath); + + SpringApplication app = new SpringApplication(TestWebSpringBootApp.class); + app.setDefaultProperties(props); + return app.run(); + } + + @Bean + Filter telemetryFilter() { + return SpringWebMvcTelemetry.builder(GlobalOpenTelemetry.get()) + .setCapturedRequestHeaders(singletonList(AbstractHttpServerTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders(singletonList(AbstractHttpServerTest.TEST_RESPONSE_HEADER)) + .build() + .createServletFilter(); + } + + @Controller + static class TestController { + + @RequestMapping("/success") + @ResponseBody + String success() { + return controller(SUCCESS, SUCCESS::getBody); + } + + @RequestMapping("/query") + @ResponseBody + String query_param(@RequestParam("some") String param) { + return controller(QUERY_PARAM, () -> "some=" + param); + } + + @RequestMapping("/redirect") + @ResponseBody + RedirectView redirect() { + return controller(REDIRECT, () -> new RedirectView(REDIRECT.getBody())); + } + + @RequestMapping("/error-status") + ResponseEntity error() { + return controller( + ERROR, + () -> new ResponseEntity<>(ERROR.getBody(), HttpStatus.valueOf(ERROR.getStatus()))); + } + + @RequestMapping("/exception") + ResponseEntity exception() { + return controller( + EXCEPTION, + () -> { + throw new RuntimeException(EXCEPTION.getBody()); + }); + } + + @RequestMapping("/captureHeaders") + ResponseEntity capture_headers( + @RequestHeader("X-Test-Request") String testRequestHeader) { + return controller( + CAPTURE_HEADERS, + () -> + ResponseEntity.ok() + .header("X-Test-Response", testRequestHeader) + .body(CAPTURE_HEADERS.getBody())); + } + + @RequestMapping("/path/{id}/param") + @ResponseBody + String path_param(@PathVariable("id") int id) { + return controller(PATH_PARAM, () -> String.valueOf(id)); + } + + @RequestMapping("/child") + @ResponseBody + String indexed_child(@RequestParam("id") String id) { + return controller( + INDEXED_CHILD, + () -> { + INDEXED_CHILD.collectSpanAttributes(name -> "id".equals(name) ? id : null); + return INDEXED_CHILD.getBody(); + }); + } + + @ExceptionHandler + ResponseEntity handleException(Throwable throwable) { + return new ResponseEntity<>(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java new file mode 100644 index 0000000000..3f76d4a994 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.ConfigurableApplicationContext; + +class WebMvcHttpServerTest extends AbstractHttpServerTest { + + private static final String CONTEXT_PATH = "/test"; + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forLibrary(); + + @Override + protected ConfigurableApplicationContext setupServer() { + return TestWebSpringBootApp.start(port, CONTEXT_PATH); + } + + @Override + protected void stopServer(ConfigurableApplicationContext applicationContext) { + applicationContext.close(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath(CONTEXT_PATH); + options.setTestPathParam(true); + // servlet filters don't capture exceptions thrown in controllers + options.setTestException(false); + + options.setExpectedHttpRoute( + endpoint -> { + if (endpoint == ServerEndpoint.PATH_PARAM) { + return CONTEXT_PATH + "/path/{id}/param"; + } + return expectedHttpRoute(endpoint); + }); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c55cbb8044..6691cc5c8b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -455,6 +455,7 @@ hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:java hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:wildfly-testing") hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library") hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:javaagent") +hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library") hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:javaagent") hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:testing") hideFromDependabot(":instrumentation:spring:spring-webflux-5.0:javaagent")