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.
This commit is contained in:
Mateusz Rzeszutek 2023-01-12 02:22:55 +01:00 committed by GitHub
parent fe6d7ebe3f
commit ca310b4ddb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 838 additions and 1 deletions

View File

@ -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] |

View File

@ -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
<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.

View File

@ -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)
}

View File

@ -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<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));
}
}

View File

@ -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<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);
}
}

View File

@ -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<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();
}
}

View File

@ -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<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();
}
}

View File

@ -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<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);
}
}

View File

@ -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<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);
}
}

View File

@ -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<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;
}
}

View File

@ -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<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);
}
}
}

View File

@ -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<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);
});
}
}

View File

@ -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")