Skip to content

Commit 776fb7f

Browse files
authored
Merge 8f00991 into b290db5
2 parents b290db5 + 8f00991 commit 776fb7f

File tree

16 files changed

+838
-0
lines changed

16 files changed

+838
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
### Features
66

77
- Improve versatility of exception resolver component for Spring with more flexible API for consumers. ([#2577](https://github.com/getsentry/sentry-java/pull/2577))
8+
- Automatic performance instrumentation for WebFlux ([#2597](https://github.com/getsentry/sentry-java/pull/2597))
9+
- You can enable it by adding `sentry.enable-tracing=true` to your `application.properties`
810
- The Spring Boot integration can now be configured to add the `SentryAppender` to specific loggers instead of the `ROOT` logger ([#2173](https://github.com/getsentry/sentry-java/pull/2173))
911
- You can specify the loggers using `"sentry.logging.loggers[0]=foo.bar` and `"sentry.logging.loggers[1]=baz` in your `application.properties`
1012

sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ sentry.max-breadcrumbs=150
88
sentry.logging.minimum-event-level=info
99
sentry.logging.minimum-breadcrumb-level=debug
1010
sentry.reactive.thread-local-accessor-enabled=true
11+
sentry.enable-tracing=true

sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ sentry.max-breadcrumbs=150
77
# Logback integration configuration options
88
sentry.logging.minimum-event-level=info
99
sentry.logging.minimum-breadcrumb-level=debug
10+
sentry.enable-tracing=true

sentry-spring-boot-starter-jakarta/api/sentry-spring-boot-starter-jakarta.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ public class io/sentry/spring/boot/jakarta/SentryProperties$Reactive {
5353
public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration {
5454
public fun <init> ()V
5555
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler;
56+
public fun sentryWebTracingFilter ()Lio/sentry/spring/jakarta/webflux/SentryWebTracingFilter;
5657
}
5758

sentry-spring-boot-starter-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,23 @@
66
import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler;
77
import io.sentry.spring.jakarta.webflux.SentryWebFilter;
88
import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor;
9+
import io.sentry.spring.jakarta.webflux.SentryWebTracingFilter;
910
import org.jetbrains.annotations.ApiStatus;
1011
import org.jetbrains.annotations.NotNull;
1112
import org.springframework.boot.ApplicationRunner;
1213
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
1314
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
1415
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
1516
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
17+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1618
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
1719
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1820
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1921
import org.springframework.context.annotation.Bean;
2022
import org.springframework.context.annotation.Conditional;
2123
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.core.Ordered;
25+
import org.springframework.core.annotation.Order;
2226
import reactor.core.publisher.Hooks;
2327
import reactor.core.scheduler.Schedulers;
2428

@@ -30,6 +34,7 @@
3034
@Open
3135
@ApiStatus.Experimental
3236
public class SentryWebfluxAutoConfiguration {
37+
private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE;
3338

3439
@Configuration(proxyBeanMethods = false)
3540
@Conditional(SentryThreadLocalAccessorCondition.class)
@@ -43,6 +48,7 @@ static class SentryWebfluxFilterThreadLocalAccessorConfiguration {
4348
* ThreadLocalAccessor to propagate the Sentry hub.
4449
*/
4550
@Bean
51+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
4652
public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation(
4753
final @NotNull IHub hub) {
4854
Hooks.enableAutomaticContextPropagation();
@@ -65,11 +71,20 @@ static class SentryWebfluxFilterConfiguration {
6571

6672
/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
6773
@Bean
74+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
6875
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
6976
return new SentryWebFilter(hub);
7077
}
7178
}
7279

80+
@Bean
81+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
82+
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
83+
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
84+
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
85+
return new SentryWebTracingFilter();
86+
}
87+
7388
/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
7489
@Bean
7590
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {

sentry-spring-boot-starter/api/sentry-spring-boot-starter.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ public class io/sentry/spring/boot/SentryWebfluxAutoConfiguration {
4747
public fun sentryScheduleHookApplicationRunner ()Lorg/springframework/boot/ApplicationRunner;
4848
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebExceptionHandler;
4949
public fun sentryWebFilter (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebFilter;
50+
public fun sentryWebTracingFilter ()Lio/sentry/spring/webflux/SentryWebTracingFilter;
5051
}
5152

sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@
55
import io.sentry.spring.webflux.SentryScheduleHook;
66
import io.sentry.spring.webflux.SentryWebExceptionHandler;
77
import io.sentry.spring.webflux.SentryWebFilter;
8+
import io.sentry.spring.webflux.SentryWebTracingFilter;
89
import org.jetbrains.annotations.ApiStatus;
910
import org.jetbrains.annotations.NotNull;
1011
import org.springframework.boot.ApplicationRunner;
1112
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
1213
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
14+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1315
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1416
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Conditional;
1518
import org.springframework.context.annotation.Configuration;
19+
import org.springframework.core.Ordered;
20+
import org.springframework.core.annotation.Order;
1621
import reactor.core.scheduler.Schedulers;
1722

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

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

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

48+
@Bean
49+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
50+
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
51+
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
52+
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
53+
return new SentryWebTracingFilter();
54+
}
55+
4156
/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
4257
@Bean
4358
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {

sentry-spring-jakarta/api/sentry-spring-jakarta.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,8 @@ public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLoc
214214
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
215215
}
216216

217+
public class io/sentry/spring/jakarta/webflux/SentryWebTracingFilter : org/springframework/web/server/WebFilter {
218+
public fun <init> ()V
219+
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
220+
}
221+
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package io.sentry.spring.jakarta.webflux;
2+
3+
import static io.sentry.spring.jakarta.webflux.AbstractSentryWebFilter.SENTRY_HUB_KEY;
4+
5+
import com.jakewharton.nopen.annotation.Open;
6+
import io.sentry.Baggage;
7+
import io.sentry.BaggageHeader;
8+
import io.sentry.CustomSamplingContext;
9+
import io.sentry.IHub;
10+
import io.sentry.ITransaction;
11+
import io.sentry.Sentry;
12+
import io.sentry.SentryLevel;
13+
import io.sentry.SentryTraceHeader;
14+
import io.sentry.SpanStatus;
15+
import io.sentry.TransactionContext;
16+
import io.sentry.TransactionOptions;
17+
import io.sentry.exception.InvalidSentryTraceHeaderException;
18+
import io.sentry.protocol.TransactionNameSource;
19+
import java.util.List;
20+
import org.jetbrains.annotations.ApiStatus;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpMethod;
25+
import org.springframework.http.HttpStatusCode;
26+
import org.springframework.http.server.reactive.ServerHttpRequest;
27+
import org.springframework.http.server.reactive.ServerHttpResponse;
28+
import org.springframework.web.server.ServerWebExchange;
29+
import org.springframework.web.server.WebFilter;
30+
import org.springframework.web.server.WebFilterChain;
31+
import reactor.core.publisher.Mono;
32+
33+
@Open
34+
@ApiStatus.Experimental
35+
public class SentryWebTracingFilter implements WebFilter {
36+
37+
private static final String TRANSACTION_OP = "http.server";
38+
39+
@Override
40+
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
41+
final @Nullable Object hubObject = exchange.getAttributes().getOrDefault(SENTRY_HUB_KEY, null);
42+
final @NotNull IHub hub = hubObject == null ? Sentry.getCurrentHub() : (IHub) hubObject;
43+
final @NotNull ServerHttpRequest request = exchange.getRequest();
44+
45+
if (hub.isEnabled() && shouldTraceRequest(hub, request)) {
46+
final @NotNull ITransaction transaction = startTransaction(hub, request);
47+
48+
return chain
49+
.filter(exchange)
50+
.doFinally(
51+
__ -> {
52+
String transactionName = TransactionNameProvider.provideTransactionName(exchange);
53+
if (transactionName != null) {
54+
transaction.setName(transactionName, TransactionNameSource.ROUTE);
55+
transaction.setOperation(TRANSACTION_OP);
56+
}
57+
if (transaction.getStatus() == null) {
58+
final @Nullable ServerHttpResponse response = exchange.getResponse();
59+
if (response != null) {
60+
final @Nullable HttpStatusCode statusCode = response.getStatusCode();
61+
if (statusCode != null) {
62+
transaction.setStatus(SpanStatus.fromHttpStatusCode(statusCode.value()));
63+
}
64+
}
65+
}
66+
transaction.finish();
67+
})
68+
.doOnError(
69+
e -> {
70+
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
71+
transaction.setThrowable(e);
72+
});
73+
} else {
74+
return chain.filter(exchange);
75+
}
76+
}
77+
78+
private boolean shouldTraceRequest(
79+
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
80+
return hub.getOptions().isTraceOptionsRequests()
81+
|| !HttpMethod.OPTIONS.equals(request.getMethod());
82+
}
83+
84+
private @NotNull ITransaction startTransaction(
85+
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
86+
final @NotNull HttpHeaders headers = request.getHeaders();
87+
final @Nullable List<String> sentryTraceHeaders =
88+
headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER);
89+
final @Nullable List<String> baggageHeaders = headers.get(BaggageHeader.BAGGAGE_HEADER);
90+
final @NotNull String name = request.getMethod() + " " + request.getURI().getPath();
91+
final @NotNull CustomSamplingContext customSamplingContext = new CustomSamplingContext();
92+
customSamplingContext.set("request", request);
93+
94+
final TransactionOptions transactionOptions = new TransactionOptions();
95+
transactionOptions.setCustomSamplingContext(customSamplingContext);
96+
transactionOptions.setBindToScope(true);
97+
98+
if (sentryTraceHeaders != null && sentryTraceHeaders.size() > 0) {
99+
final @NotNull Baggage baggage =
100+
Baggage.fromHeader(baggageHeaders, hub.getOptions().getLogger());
101+
try {
102+
final @NotNull TransactionContext contexts =
103+
TransactionContext.fromSentryTrace(
104+
name,
105+
TransactionNameSource.URL,
106+
TRANSACTION_OP,
107+
new SentryTraceHeader(sentryTraceHeaders.get(0)),
108+
baggage,
109+
null);
110+
111+
return hub.startTransaction(contexts, transactionOptions);
112+
} catch (InvalidSentryTraceHeaderException e) {
113+
hub.getOptions()
114+
.getLogger()
115+
.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
116+
}
117+
}
118+
119+
return hub.startTransaction(
120+
new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP),
121+
transactionOptions);
122+
}
123+
}

0 commit comments

Comments
 (0)