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:
parent
fe6d7ebe3f
commit
ca310b4ddb
|
@ -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] |
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue