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

Automatic performance instrumentation for WebFlux #2597

Merged
merged 9 commits into from
Mar 14, 2023
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Features

- Improve versatility of exception resolver component for Spring with more flexible API for consumers. ([#2577](https://github.com/getsentry/sentry-java/pull/2577))
- Automatic performance instrumentation for WebFlux ([#2597](https://github.com/getsentry/sentry-java/pull/2597))
- You can enable it by adding `sentry.enable-tracing=true` to your `application.properties`

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ sentry.max-breadcrumbs=150
sentry.logging.minimum-event-level=info
sentry.logging.minimum-breadcrumb-level=debug
sentry.reactive.thread-local-accessor-enabled=true
sentry.enable-tracing=false
adinauer marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ sentry.max-breadcrumbs=150
# Logback integration configuration options
sentry.logging.minimum-event-level=info
sentry.logging.minimum-breadcrumb-level=debug
sentry.enable-tracing=true
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ public class io/sentry/spring/boot/jakarta/SentryProperties$Reactive {
public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration {
public fun <init> ()V
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler;
public fun sentryWebTracingFilter ()Lio/sentry/spring/jakarta/webflux/SentryWebTracingFilter;
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@
import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler;
import io.sentry.spring.jakarta.webflux.SentryWebFilter;
import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor;
import io.sentry.spring.jakarta.webflux.SentryWebTracingFilter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Hooks;
import reactor.core.scheduler.Schedulers;

Expand All @@ -30,6 +34,7 @@
@Open
@ApiStatus.Experimental
public class SentryWebfluxAutoConfiguration {
private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE;

@Configuration(proxyBeanMethods = false)
@Conditional(SentryThreadLocalAccessorCondition.class)
Expand All @@ -43,6 +48,7 @@ static class SentryWebfluxFilterThreadLocalAccessorConfiguration {
* ThreadLocalAccessor to propagate the Sentry hub.
*/
@Bean
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation(
final @NotNull IHub hub) {
Hooks.enableAutomaticContextPropagation();
Expand All @@ -65,11 +71,20 @@ static class SentryWebfluxFilterConfiguration {

/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
@Bean
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
return new SentryWebFilter(hub);
}
}

@Bean
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
return new SentryWebTracingFilter();
}

/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
@Bean
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ public class io/sentry/spring/boot/SentryWebfluxAutoConfiguration {
public fun sentryScheduleHookApplicationRunner ()Lorg/springframework/boot/ApplicationRunner;
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebExceptionHandler;
public fun sentryWebFilter (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebFilter;
public fun sentryWebTracingFilter ()Lio/sentry/spring/webflux/SentryWebTracingFilter;
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
import io.sentry.spring.webflux.SentryScheduleHook;
import io.sentry.spring.webflux.SentryWebExceptionHandler;
import io.sentry.spring.webflux.SentryWebFilter;
import io.sentry.spring.webflux.SentryWebTracingFilter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import reactor.core.scheduler.Schedulers;

/** Configures Sentry integration for Spring Webflux and Project Reactor. */
Expand All @@ -23,6 +28,7 @@
@Open
@ApiStatus.Experimental
public class SentryWebfluxAutoConfiguration {
private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE;

/** Configures hook that sets correct hub on the executing thread. */
@Bean
Expand All @@ -34,10 +40,19 @@ public class SentryWebfluxAutoConfiguration {

/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
@Bean
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
return new SentryWebFilter(hub);
}

@Bean
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
return new SentryWebTracingFilter();
}

/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
@Bean
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {
Expand Down
5 changes: 5 additions & 0 deletions sentry-spring-jakarta/api/sentry-spring-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,8 @@ public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLoc
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
}

public class io/sentry/spring/jakarta/webflux/SentryWebTracingFilter : org/springframework/web/server/WebFilter {
public fun <init> ()V
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.sentry.spring.jakarta.webflux;

import static io.sentry.spring.jakarta.webflux.AbstractSentryWebFilter.SENTRY_HUB_KEY;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.Baggage;
import io.sentry.BaggageHeader;
import io.sentry.CustomSamplingContext;
import io.sentry.IHub;
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanStatus;
import io.sentry.TransactionContext;
import io.sentry.TransactionOptions;
import io.sentry.exception.InvalidSentryTraceHeaderException;
import io.sentry.protocol.TransactionNameSource;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Open
@ApiStatus.Experimental
public class SentryWebTracingFilter implements WebFilter {

private static final String TRANSACTION_OP = "http.server";

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
final @Nullable Object hubObject = exchange.getAttributes().getOrDefault(SENTRY_HUB_KEY, null);
final @NotNull IHub hub = hubObject == null ? Sentry.getCurrentHub() : (IHub) hubObject;
final @NotNull ServerHttpRequest request = exchange.getRequest();

if (hub.isEnabled() && shouldTraceRequest(hub, request)) {
final @NotNull ITransaction transaction = startTransaction(hub, request);

return chain
.filter(exchange)
.doFinally(
__ -> {
String transactionName = TransactionNameProvider.provideTransactionName(exchange);
if (transactionName != null) {
transaction.setName(transactionName, TransactionNameSource.ROUTE);
transaction.setOperation(TRANSACTION_OP);
}
if (transaction.getStatus() == null) {
final @Nullable ServerHttpResponse response = exchange.getResponse();
if (response != null) {
final @Nullable HttpStatusCode statusCode = response.getStatusCode();
if (statusCode != null) {
transaction.setStatus(SpanStatus.fromHttpStatusCode(statusCode.value()));
}
}
}
transaction.finish();
})
.doOnError(
e -> {
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
transaction.setThrowable(e);
});
} else {
return chain.filter(exchange);
}
}

private boolean shouldTraceRequest(
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
return hub.getOptions().isTraceOptionsRequests()
|| !HttpMethod.OPTIONS.equals(request.getMethod());
}

private @NotNull ITransaction startTransaction(
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
final @NotNull HttpHeaders headers = request.getHeaders();
final @Nullable List<String> sentryTraceHeaders =
headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER);
final @Nullable List<String> baggageHeaders = headers.get(BaggageHeader.BAGGAGE_HEADER);
final @NotNull String name = request.getMethod() + " " + request.getURI().getPath();
final @NotNull CustomSamplingContext customSamplingContext = new CustomSamplingContext();
customSamplingContext.set("request", request);

final TransactionOptions transactionOptions = new TransactionOptions();
transactionOptions.setCustomSamplingContext(customSamplingContext);
transactionOptions.setBindToScope(true);

if (sentryTraceHeaders != null && sentryTraceHeaders.size() > 0) {
final @NotNull Baggage baggage =
Baggage.fromHeader(baggageHeaders, hub.getOptions().getLogger());
try {
final @NotNull TransactionContext contexts =
TransactionContext.fromSentryTrace(
name,
TransactionNameSource.URL,
TRANSACTION_OP,
new SentryTraceHeader(sentryTraceHeaders.get(0)),
baggage,
null);

return hub.startTransaction(contexts, transactionOptions);
} catch (InvalidSentryTraceHeaderException e) {
hub.getOptions()
.getLogger()
.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
}
}

return hub.startTransaction(
new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP),
transactionOptions);
}
}
Loading