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

Implement ThreadLocalAccessor for propagating Sentry hub with reactor / WebFlux #2570

Merged
merged 10 commits into from
Mar 1, 2023
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@
- If set to `false` performance is disabled, regardless of `tracesSampleRate` and `tracesSampler` options.
- Detect dependencies by listing MANIFEST.MF files at runtime ([#2538](https://github.com/getsentry/sentry-java/pull/2538))
- Report integrations in use, report packages in use more consistently ([#2179](https://github.com/getsentry/sentry-java/pull/2179))
- Implement `ThreadLocalAccessor` for propagating Sentry hub with reactor / WebFlux ([#2570](https://github.com/getsentry/sentry-java/pull/2570))
- Requires `io.micrometer:context-propagation:1.0.2+` as well as Spring Boot 3.0.3+
- Enable the feature by setting `sentry.reactive.thread-local-accessor-enabled=true`
- This is still considered experimental. Once we have enough feedback we may turn this on by default.
- Checkout the sample here: https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux-jakarta
- A new hub is now cloned from the main hub for every request

### Fixes

- Leave `inApp` flag for stack frames undecided in SDK if unsure and let ingestion decide instead ([#2547](https://github.com/getsentry/sentry-java/pull/2547))
- Allow `0.0` error sample rate ([#2573](https://github.com/getsentry/sentry-java/pull/2573))
- Use the same hub in WebFlux exception handler as we do in WebFilter ([#2566](https://github.com/getsentry/sentry-java/pull/2566))

## 6.14.0

Expand Down
5 changes: 3 additions & 2 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ object Config {
val kotlinStdLib = "stdlib-jdk8"

val springBootVersion = "2.7.5"
val springBoot3Version = "3.0.0"
val springBoot3Version = "3.0.3"
val kotlinCompatibleLanguageVersion = "1.4"

val composeVersion = "1.1.1"
Expand Down Expand Up @@ -107,7 +107,8 @@ object Config {

val fragment = "androidx.fragment:fragment-ktx:1.3.5"

val reactorCore = "io.projectreactor:reactor-core:3.4.6"
val reactorCore = "io.projectreactor:reactor-core:3.5.3"
val contextPropagation = "io.micrometer:context-propagation:1.0.2"

private val feignVersion = "11.6"
val feignCore = "io.github.openfeign:feign-core:$feignVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.sentry.samples.spring.boot.jakarta;

import io.sentry.spring.jakarta.webflux.ReactorUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@RestController
public class TodoController {
Expand All @@ -24,11 +28,18 @@ Todo todo(@PathVariable Long id) {

@GetMapping("/todo-webclient/{id}")
Todo todoWebClient(@PathVariable Long id) {
return webClient
.get()
.uri("https://jsonplaceholder.typicode.com/todos/{id}", id)
.retrieve()
.bodyToMono(Todo.class)
Hooks.enableAutomaticContextPropagation();
return ReactorUtils.withSentry(
Mono.just(true)
.publishOn(Schedulers.boundedElastic())
.flatMap(
x ->
webClient
.get()
.uri("https://jsonplaceholder.typicode.com/todos/{id}", id)
.retrieve()
.bodyToMono(Todo.class)
.map(response -> response)))
.block();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ repositories {

dependencies {
implementation(Config.Libs.springBoot3StarterWebflux)
implementation(Config.Libs.contextPropagation)
implementation(Config.Libs.kotlinReflect)
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
implementation(projects.sentrySpringBootStarterJakarta)
implementation(projects.sentryLogback)

testImplementation(Config.Libs.springBoot3StarterTest) {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.sentry.samples.spring.boot;
package io.sentry.samples.spring.boot.jakarta;

public class Person {
private final String firstName;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.sentry.samples.spring.boot;
package io.sentry.samples.spring.boot.jakarta;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.sentry.samples.spring.boot;
package io.sentry.samples.spring.boot.jakarta;

import io.sentry.Sentry;
import java.time.Duration;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package io.sentry.samples.spring.boot;
package io.sentry.samples.spring.boot.jakarta;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

@SpringBootApplication
public class SentryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SentryDemoApplication.class, args);
}

@Bean
WebClient webClient(WebClient.Builder builder) {
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.sentry.samples.spring.boot.jakarta;

public class Todo {
private final Long id;
private final String title;
private final boolean completed;

public Todo(Long id, String title, boolean completed) {
this.id = id;
this.title = title;
this.completed = completed;
}

public Long getId() {
return id;
}

public String getTitle() {
return title;
}

public boolean isCompleted() {
return completed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.sentry.samples.spring.boot.jakarta;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
public class TodoController {
private final WebClient webClient;

public TodoController(WebClient webClient) {
this.webClient = webClient;
}

@GetMapping("/todo-webclient/{id}")
Mono<Todo> todoWebClient(@PathVariable Long id) {
return webClient
.get()
.uri("https://jsonplaceholder.typicode.com/todos/{id}", id)
.retrieve()
.bodyToMono(Todo.class)
.map(response -> response);
}
}
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.reactive.thread-local-accessor-enabled=true
2 changes: 2 additions & 0 deletions sentry-spring-boot-starter-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
compileOnly(Config.Libs.springBoot3StarterAop)
compileOnly(Config.Libs.springBoot3StarterSecurity)
compileOnly(Config.Libs.reactorCore)
compileOnly(Config.Libs.contextPropagation)
compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore)

annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure)
Expand All @@ -79,6 +80,7 @@ dependencies {
testImplementation(Config.Libs.springBoot3StarterSecurity)
testImplementation(Config.Libs.springBoot3StarterAop)
testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore)
testImplementation(Config.Libs.contextPropagation)
}

configure<SourceSetContainer> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class SentryProperties extends SentryOptions {
/** Logging framework integration properties. */
private @NotNull Logging logging = new Logging();

/** Reactive framework (e.g. WebFlux) integration properties */
private @NotNull Reactive reactive = new Reactive();

public boolean isUseGitCommitIdAsRelease() {
return useGitCommitIdAsRelease;
}
Expand Down Expand Up @@ -72,6 +75,14 @@ public void setLogging(@NotNull Logging logging) {
this.logging = logging;
}

public @NotNull Reactive getReactive() {
return reactive;
}

public void setReactive(@NotNull Reactive reactive) {
this.reactive = reactive;
}

@Open
public static class Logging {
/** Enable/Disable logging auto-configuration. */
Expand Down Expand Up @@ -107,4 +118,18 @@ public void setMinimumEventLevel(@Nullable Level minimumEventLevel) {
this.minimumEventLevel = minimumEventLevel;
}
}

@Open
public static class Reactive {
/** Enable/Disable usage of {@link io.micrometer.context.ThreadLocalAccessor} for Hub propagation */
private boolean threadLocalAccessorEnabled = true;

public boolean isThreadLocalAccessorEnabled() {
return threadLocalAccessorEnabled;
}

public void setThreadLocalAccessorEnabled(boolean threadLocalAccessorEnabled) {
this.threadLocalAccessorEnabled = threadLocalAccessorEnabled;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package io.sentry.spring.boot.jakarta;

import com.jakewharton.nopen.annotation.Open;

import io.sentry.IHub;
import io.sentry.spring.jakarta.webflux.SentryScheduleHook;
import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler;
import io.sentry.spring.jakarta.webflux.SentryWebFilter;
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.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 io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor;
import reactor.core.publisher.Hooks;
import reactor.core.scheduler.Schedulers;

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

/** Configures hook that sets correct hub on the executing thread. */
@Bean
public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() {
return args -> {
Schedulers.onScheduleHook("sentry", new SentryScheduleHook());
};
@Configuration(proxyBeanMethods = false)
@Conditional(SentryThreadLocalAccessorCondition.class)
@Open
static class SentryWebfluxFilterThreadLocalAccessorConfiguration {

/**
* Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request.
*
* Makes use of newer reactor-core and context-propagation library feature ThreadLocalAccessor
* to propagate the Sentry hub.
*/
@Bean
public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation(final @NotNull IHub hub) {
Hooks.enableAutomaticContextPropagation();
return new SentryWebFilterWithThreadLocalAccessor(hub);
}
}

/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
@Bean
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
return new SentryWebFilter(hub);
@Configuration(proxyBeanMethods = false)
@Conditional(SentryLegacyFilterConfigurationCondition.class)
@Open
static class SentryWebfluxFilterConfiguration {

/** Configures hook that sets correct hub on the executing thread. */
@Bean
public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() {
return args -> {
Schedulers.onScheduleHook("sentry", new SentryScheduleHook());
};
}

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

/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
@Bean
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {
return new SentryWebExceptionHandler(hub);
}

static final class SentryLegacyFilterConfigurationCondition extends AnyNestedCondition {

public SentryLegacyFilterConfigurationCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnProperty(name = "sentry.reactive.thread-local-accessor-enabled", havingValue = "false", matchIfMissing = true)
@SuppressWarnings("UnusedNestedClass")
private static class SentryDisableThreadLocalAccessorCondition {}

@ConditionalOnMissingClass("io.micrometer.context.ThreadLocalAccessor")
@SuppressWarnings("UnusedNestedClass")
private static class ThreadLocalAccessorClassCondition {}
}

static final class SentryThreadLocalAccessorCondition extends AllNestedConditions {

public SentryThreadLocalAccessorCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnProperty(name = "sentry.reactive.thread-local-accessor-enabled", havingValue = "true")
@SuppressWarnings("UnusedNestedClass")
private static class SentryEnableThreadLocalAccessorCondition {}

@ConditionalOnClass(io.micrometer.context.ThreadLocalAccessor.class)
@SuppressWarnings("UnusedNestedClass")
private static class ThreadLocalAccessorClassCondition {}
}
}
Loading