-
Notifications
You must be signed in to change notification settings - Fork 873
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support Spring Web MVC in library instrumentation (#7552)
Part of #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.
- Loading branch information
Mateusz Rzeszutek
authored
Jan 12, 2023
1 parent
fe6d7eb
commit ca310b4
Showing
13 changed files
with
838 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
17 changes: 17 additions & 0 deletions
17
instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
153 changes: 153 additions & 0 deletions
153
...y/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/HttpRouteSupport.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
.../io/opentelemetry/instrumentation/spring/webmvc/v6_0/JakartaHttpServletRequestGetter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.