Skip to content

Commit

Permalink
Implement ThreadLocalAccessor for propagating Sentry hub with react…
Browse files Browse the repository at this point in the history
…or / WebFlux (#2570)
  • Loading branch information
adinauer committed Mar 1, 2023
1 parent d691d8f commit e5bbb00
Show file tree
Hide file tree
Showing 26 changed files with 666 additions and 39 deletions.
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

0 comments on commit e5bbb00

Please sign in to comment.