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 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 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 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 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], |
|
| [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] |
|
| [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-3.1:wildfly-testing")
|
||||||
hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library")
|
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: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:javaagent")
|
||||||
hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:testing")
|
hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:testing")
|
||||||
hideFromDependabot(":instrumentation:spring:spring-webflux-5.0:javaagent")
|
hideFromDependabot(":instrumentation:spring:spring-webflux-5.0:javaagent")
|
||||||
|
|
Loading…
Reference in New Issue