Support Spring Web MVC in library instrumentation
Part of
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 is done.
MODIFY
docs/supported-libraries.md
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),<br>[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] |

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md
+# 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
+<dependencies>
+ <!-- OpenTelemetry instrumentation -->
+ <dependency>
+ <groupId>io.opentelemetry.instrumentation</groupId>
+ <artifactId>opentelemetry-spring-webmvc-6.0</artifactId>
+ <version>OPENTELEMETRY_VERSION</version>
+ </dependency>
+
+ <!-- OpenTelemetry exporter -->
+ <!-- replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/jaeger/..) -->
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-exporter-logging</artifactId>
+ <version>OPENTELEMETRY_VERSION</version>
+ </dependency>
+
+ <!-- required to instrument Spring WebMVC -->
+ <!-- this artifact should already be present in your application -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-webmvc</artifactId>
+ <version>SPRING_VERSION</version>
+ </dependency>
+
+</dependencies>
+```
+
+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.

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts
+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)
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java
+/*
+ * 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<HandlerMapping> 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<ContextRefreshedEvent> {
+
+ @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<HandlerMapping> mappings) {
+ List<HandlerMapping> 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));
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java
+/*
+ * 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<HttpServletRequest> {
+ INSTANCE;
+
+ @Override
+ public Iterable<String> keys(HttpServletRequest carrier) {
+ return Collections.list(carrier.getHeaderNames());
+ }
+
+ @Override
+ public String get(HttpServletRequest carrier, String key) {
+ return carrier.getHeader(key);
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java
+/*
+ * 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<HttpServletRequest, HttpServletResponse> {
+ INSTANCE;
+
+ @Override
+ @Nullable
+ public String method(HttpServletRequest request) {
+ return request.getMethod();
+ }
+
+ @Override
+ public List<String> requestHeader(HttpServletRequest request, String name) {
+ Enumeration<String> 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<String> responseHeader(
+ HttpServletRequest request, HttpServletResponse response, String name) {
+ Collection<String> headers = response.getHeaders(name);
+ if (headers == null) {
+ return Collections.emptyList();
+ }
+ if (headers instanceof List) {
+ return (List<String>) 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();
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java
+/*
+ * 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<HttpServletRequest> {
+ 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();
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetry.java
+/*
+ * 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<HttpServletRequest, HttpServletResponse> instrumenter;
+
+ SpringWebMvcTelemetry(Instrumenter<HttpServletRequest, HttpServletResponse> instrumenter) {
+ this.instrumenter = instrumenter;
+ }
+
+ /** Returns a new {@link Filter} that generates telemetry for received HTTP requests. */
+ public Filter createServletFilter() {
+ return new WebMvcTelemetryProducingFilter(instrumenter);
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java
+/*
+ * 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<AttributesExtractor<HttpServletRequest, HttpServletResponse>>
+ additionalExtractors = new ArrayList<>();
+ private final HttpServerAttributesExtractorBuilder<HttpServletRequest, HttpServletResponse>
+ 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<HttpServletRequest, HttpServletResponse> 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<String> 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<String> 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<HttpServletRequest, HttpServletResponse> instrumenter =
+ Instrumenter.<HttpServletRequest, HttpServletResponse>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);
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java
+/*
+ * 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<HttpServletRequest, HttpServletResponse> instrumenter;
+ private final HttpRouteSupport httpRouteSupport = new HttpRouteSupport();
+
+ WebMvcTelemetryProducingFilter(
+ Instrumenter<HttpServletRequest, HttpServletResponse> 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;
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java
+/*
+ * 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<String> error() {
+ return controller(
+ ERROR,
+ () -> new ResponseEntity<>(ERROR.getBody(), HttpStatus.valueOf(ERROR.getStatus())));
+ }
+
+ @RequestMapping("/exception")
+ ResponseEntity<String> exception() {
+ return controller(
+ EXCEPTION,
+ () -> {
+ throw new RuntimeException(EXCEPTION.getBody());
+ });
+ }
+
+ @RequestMapping("/captureHeaders")
+ ResponseEntity<String> 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<String> handleException(Throwable throwable) {
+ return new ResponseEntity<>(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+}

ADD
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java
+/*
+ * 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<ConfigurableApplicationContext> {
+
+ 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);
+ });
+ }
+}

MODIFY
settings.gradle.kts
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")

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.