Skip to content

Commit 7620651

Browse files
authored
Merge 4af7a7d into 2384db9
2 parents 2384db9 + 4af7a7d commit 7620651

File tree

11 files changed

+252
-12
lines changed

11 files changed

+252
-12
lines changed

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ object Config {
66
val kotlinStdLib = "stdlib-jdk8"
77

88
val springBootVersion = "2.7.5"
9-
val springBoot3Version = "3.0.0"
9+
val springBoot3Version = "3.0.2"
1010
val kotlinCompatibleLanguageVersion = "1.4"
1111

1212
val composeVersion = "1.1.1"

sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ dependencies {
3636
implementation(projects.sentrySpringBootStarterJakarta)
3737
implementation(projects.sentryLogback)
3838

39+
// TODO remove these (v) once this lands in Spring (Boot)
40+
implementation("io.projectreactor:reactor-core:3.5.3")
41+
implementation("io.projectreactor.netty:reactor-netty:1.1.3")
42+
implementation("io.micrometer:context-propagation:1.0.2")
43+
// TODO remove these (^) once this lands in Spring (Boot)
44+
3945
// database query tracing
4046
implementation(projects.sentryJdbc)
4147
runtimeOnly(Config.TestLibs.hsqldb)

sentry-spring-boot-starter-jakarta/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ dependencies {
6464
errorprone(Config.CompileOnly.errorProneNullAway)
6565
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
6666

67+
// TODO remove these (v) once this lands in Spring (Boot)
68+
compileOnly("io.projectreactor:reactor-core:3.5.3")
69+
compileOnly("io.projectreactor.netty:reactor-netty:1.1.3")
70+
compileOnly("io.micrometer:context-propagation:1.0.2")
71+
// TODO remove these (^) once this lands in Spring (Boot)
72+
6773
// tests
6874
testImplementation(projects.sentryLogback)
6975
testImplementation(projects.sentryApacheHttpClient5)

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public class SentryProperties extends SentryOptions {
2828
/** Logging framework integration properties. */
2929
private @NotNull Logging logging = new Logging();
3030

31+
/** Reactive framework (e.g. WebFlux) integration properties */
32+
private @NotNull Reactive reactive = new Reactive();
33+
3134
public boolean isUseGitCommitIdAsRelease() {
3235
return useGitCommitIdAsRelease;
3336
}
@@ -72,6 +75,14 @@ public void setLogging(@NotNull Logging logging) {
7275
this.logging = logging;
7376
}
7477

78+
public @NotNull Reactive getReactive() {
79+
return reactive;
80+
}
81+
82+
public void setReactive(@NotNull Reactive reactive) {
83+
this.reactive = reactive;
84+
}
85+
7586
@Open
7687
public static class Logging {
7788
/** Enable/Disable logging auto-configuration. */
@@ -107,4 +118,18 @@ public void setMinimumEventLevel(@Nullable Level minimumEventLevel) {
107118
this.minimumEventLevel = minimumEventLevel;
108119
}
109120
}
121+
122+
@Open
123+
public static class Reactive {
124+
/** Enable/Disable usage of {@link io.micrometer.context.ThreadLocalAccessor} for Hub propagation */
125+
private boolean threadLocalAccessorEnabled = true;
126+
127+
public boolean isThreadLocalAccessorEnabled() {
128+
return threadLocalAccessorEnabled;
129+
}
130+
131+
public void setThreadLocalAccessorEnabled(boolean threadLocalAccessorEnabled) {
132+
this.threadLocalAccessorEnabled = threadLocalAccessorEnabled;
133+
}
134+
}
110135
}
Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
package io.sentry.spring.boot.jakarta;
22

33
import com.jakewharton.nopen.annotation.Open;
4+
45
import io.sentry.IHub;
56
import io.sentry.spring.jakarta.webflux.SentryScheduleHook;
67
import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler;
78
import io.sentry.spring.jakarta.webflux.SentryWebFilter;
89
import org.jetbrains.annotations.ApiStatus;
910
import org.jetbrains.annotations.NotNull;
1011
import org.springframework.boot.ApplicationRunner;
12+
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
13+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
1114
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
1215
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
16+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
17+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1318
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1419
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Conditional;
1521
import org.springframework.context.annotation.Configuration;
22+
23+
import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor;
24+
import reactor.core.publisher.Hooks;
1625
import reactor.core.scheduler.Schedulers;
1726

1827
/** Configures Sentry integration for Spring Webflux and Project Reactor. */
@@ -24,23 +33,77 @@
2433
@ApiStatus.Experimental
2534
public class SentryWebfluxAutoConfiguration {
2635

27-
/** Configures hook that sets correct hub on the executing thread. */
28-
@Bean
29-
public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() {
30-
return args -> {
31-
Schedulers.onScheduleHook("sentry", new SentryScheduleHook());
32-
};
36+
@Configuration(proxyBeanMethods = false)
37+
@Conditional(SentryThreadLocalAccessorCondition.class)
38+
@Open
39+
static class SentryWebfluxFilterThreadLocalAccessorConfiguration {
40+
41+
/**
42+
* Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request.
43+
*
44+
* Makes use of newer reactor-core and context-propagation library feature ThreadLocalAccessor
45+
* to propagate the Sentry hub.
46+
*/
47+
@Bean
48+
public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation(final @NotNull IHub hub) {
49+
Hooks.enableAutomaticContextPropagation();
50+
return new SentryWebFilterWithThreadLocalAccessor(hub);
51+
}
3352
}
3453

35-
/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
36-
@Bean
37-
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
38-
return new SentryWebFilter(hub);
54+
@Configuration(proxyBeanMethods = false)
55+
@Conditional(SentryLegacyFilterConfigurationCondition.class)
56+
@Open
57+
static class SentryWebfluxFilterConfiguration {
58+
59+
/** Configures hook that sets correct hub on the executing thread. */
60+
@Bean
61+
public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() {
62+
return args -> {
63+
Schedulers.onScheduleHook("sentry", new SentryScheduleHook());
64+
};
65+
}
66+
67+
/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
68+
@Bean
69+
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
70+
return new SentryWebFilter(hub);
71+
}
3972
}
4073

4174
/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
4275
@Bean
4376
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {
4477
return new SentryWebExceptionHandler(hub);
4578
}
79+
80+
static final class SentryLegacyFilterConfigurationCondition extends AnyNestedCondition {
81+
82+
public SentryLegacyFilterConfigurationCondition() {
83+
super(ConfigurationPhase.REGISTER_BEAN);
84+
}
85+
86+
@ConditionalOnProperty(name = "sentry.reactive.thread-local-accessor-enabled", havingValue = "false", matchIfMissing = true)
87+
@SuppressWarnings("UnusedNestedClass")
88+
private static class SentryDisableThreadLocalAccessorCondition {}
89+
90+
@ConditionalOnMissingClass("io.micrometer.context.ThreadLocalAccessor")
91+
@SuppressWarnings("UnusedNestedClass")
92+
private static class ThreadLocalAccessorClassCondition {}
93+
}
94+
95+
static final class SentryThreadLocalAccessorCondition extends AllNestedConditions {
96+
97+
public SentryThreadLocalAccessorCondition() {
98+
super(ConfigurationPhase.REGISTER_BEAN);
99+
}
100+
101+
@ConditionalOnProperty(name = "sentry.reactive.thread-local-accessor-enabled", havingValue = "true")
102+
@SuppressWarnings("UnusedNestedClass")
103+
private static class SentryEnableThreadLocalAccessorCondition {}
104+
105+
@ConditionalOnClass(io.micrometer.context.ThreadLocalAccessor.class)
106+
@SuppressWarnings("UnusedNestedClass")
107+
private static class ThreadLocalAccessorClassCondition {}
108+
}
46109
}

sentry-spring-jakarta/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ dependencies {
5959
errorprone(Config.CompileOnly.errorProneNullAway)
6060
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
6161

62+
// TODO remove these (v) once this lands in Spring (Boot)
63+
compileOnly("io.projectreactor:reactor-core:3.5.3")
64+
compileOnly("io.projectreactor.netty:reactor-netty:1.1.3")
65+
compileOnly("io.micrometer:context-propagation:1.0.2")
66+
// TODO remove these (^) once this lands in Spring (Boot)
67+
6268
// tests
6369
testImplementation(projects.sentryTestSupport)
6470
testImplementation(kotlin(Config.kotlinStdLib))
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.sentry.spring.jakarta.webflux;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
import io.sentry.IHub;
7+
import io.sentry.Sentry;
8+
import reactor.core.publisher.Flux;
9+
import reactor.core.publisher.Mono;
10+
import reactor.util.context.Context;
11+
12+
@ApiStatus.Experimental
13+
public final class ReactorUtils {
14+
15+
/**
16+
* Writes the Sentry {@link IHub} to the {@link Context} and uses {@link io.micrometer.context.ThreadLocalAccessor} to propagate it.
17+
*
18+
* This requires
19+
* - reactor.core.publisher.Hooks#enableAutomaticContextPropagation() to be enabled
20+
* - having `io.micrometer:context-propagation:1.0.2` or newer as dependency
21+
* - having `io.projectreactor:reactor-core:3.5.3` or newer as dependency
22+
*/
23+
@ApiStatus.Experimental
24+
public static <T> Mono<T> withSentry(Mono<T> mono) {
25+
final @NotNull IHub oldHub = Sentry.getCurrentHub();
26+
final @NotNull IHub clonedHub = oldHub.clone();
27+
28+
/**
29+
* WARNING: Cannot set the clonedHub as current hub.
30+
* It would be used by others to clone again causing shared hubs and scopes and thus
31+
* leading to issues like unrelated breadcrumbs showing up in events.
32+
*/
33+
// Sentry.setCurrentHub(clonedHub);
34+
35+
return Mono.deferContextual(ctx -> mono).contextWrite(Context.of(SentryReactorThreadLocalAccessor.KEY, clonedHub));
36+
}
37+
38+
/**
39+
* Writes the Sentry {@link IHub} to the {@link Context} and uses {@link io.micrometer.context.ThreadLocalAccessor} to propagate it.
40+
*
41+
* This requires
42+
* - reactor.core.publisher.Hooks#enableAutomaticContextPropagation() to be enabled
43+
* - having `io.micrometer:context-propagation:1.0.2` or newer as dependency
44+
* - having `io.projectreactor:reactor-core:3.5.3` or newer as dependency
45+
*/
46+
@ApiStatus.Experimental
47+
public static <T> Flux<T> withSentry(Flux<T> flux) {
48+
final @NotNull IHub oldHub = Sentry.getCurrentHub();
49+
final @NotNull IHub clonedHub = oldHub.clone();
50+
51+
/**
52+
* WARNING: Cannot set the clonedHub as current hub.
53+
* It would be used by others to clone again causing shared hubs and scopes and thus
54+
* leading to issues like unrelated breadcrumbs showing up in events.
55+
*/
56+
// Sentry.setCurrentHub(clonedHub);
57+
58+
return Flux.deferContextual(ctx -> flux).contextWrite(Context.of(SentryReactorThreadLocalAccessor.KEY, clonedHub));
59+
}
60+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.sentry.spring.jakarta.webflux;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
5+
import io.micrometer.context.ThreadLocalAccessor;
6+
import io.sentry.IHub;
7+
import io.sentry.NoOpHub;
8+
import io.sentry.Sentry;
9+
10+
@ApiStatus.Experimental
11+
public final class SentryReactorThreadLocalAccessor implements ThreadLocalAccessor<IHub> {
12+
13+
public static final String KEY = "sentry-hub";
14+
15+
@Override
16+
public Object key() {
17+
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "get");
18+
return KEY;
19+
}
20+
21+
@Override
22+
public IHub getValue() {
23+
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "get value");
24+
return Sentry.getCurrentHub();
25+
}
26+
27+
@Override
28+
public void setValue(IHub value) {
29+
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "set value " + value);
30+
Sentry.setCurrentHub(value);
31+
}
32+
33+
@Override
34+
public void reset() {
35+
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "reset");
36+
Sentry.setCurrentHub(NoOpHub.getInstance());
37+
}
38+
39+
// @Override
40+
// public void restore(IHub previousValue) {
41+
//// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "restore value " + previousValue);
42+
// ThreadLocalAccessor.super.restore(previousValue);
43+
// }
44+
}

sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.sentry.spring.jakarta.webflux;
22

3+
import com.jakewharton.nopen.annotation.Open;
4+
5+
import io.sentry.Sentry;
36
import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_REQUEST;
47
import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_RESPONSE;
58

@@ -18,7 +21,8 @@
1821

1922
/** Manages {@link io.sentry.Scope} in Webflux request processing. */
2023
@ApiStatus.Experimental
21-
public final class SentryWebFilter implements WebFilter {
24+
@Open
25+
public class SentryWebFilter implements WebFilter {
2226
private final @NotNull IHub hub;
2327
private final @NotNull SentryRequestResolver sentryRequestResolver;
2428

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.sentry.spring.jakarta.webflux;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.springframework.web.server.ServerWebExchange;
6+
import org.springframework.web.server.WebFilterChain;
7+
8+
import io.sentry.IHub;
9+
import reactor.core.publisher.Mono;
10+
11+
/** Manages {@link io.sentry.Scope} in Webflux request processing. */
12+
@ApiStatus.Experimental
13+
public final class SentryWebFilterWithThreadLocalAccessor extends SentryWebFilter {
14+
15+
public SentryWebFilterWithThreadLocalAccessor(final @NotNull IHub hub) {
16+
super(hub);
17+
}
18+
19+
@Override
20+
public Mono<Void> filter(
21+
final @NotNull ServerWebExchange serverWebExchange,
22+
final @NotNull WebFilterChain webFilterChain) {
23+
return ReactorUtils.withSentry(super.filter(serverWebExchange, webFilterChain));
24+
}
25+
}

0 commit comments

Comments
 (0)