Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Spring Web MVC in library instrumentation #7552

Merged
merged 1 commit into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,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+ | [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] |
Expand Down
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.
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)
}
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));
}
}
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);
}
}
Loading