Skip to content

Commit

Permalink
Merge 9409e6c into 184d40a
Browse files Browse the repository at this point in the history
  • Loading branch information
adinauer authored Feb 27, 2023
2 parents 184d40a + 9409e6c commit b3f3d7a
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
- 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

### Fixes

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
1 change: 1 addition & 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 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 {}
}
}
1 change: 1 addition & 0 deletions sentry-spring-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
compileOnly(Config.Libs.aspectj)
compileOnly(Config.Libs.servletApiJakarta)
compileOnly(Config.Libs.slf4jApi)
compileOnly(Config.Libs.contextPropagation)

compileOnly(Config.Libs.springWebflux)

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

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

import io.sentry.IHub;
import io.sentry.Sentry;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

@ApiStatus.Experimental
public final class ReactorUtils {

/**
* Writes the Sentry {@link IHub} to the {@link Context} and uses {@link io.micrometer.context.ThreadLocalAccessor} to propagate it.
*
* This requires
* - reactor.core.publisher.Hooks#enableAutomaticContextPropagation() to be enabled
* - having `io.micrometer:context-propagation:1.0.2` or newer as dependency
* - having `io.projectreactor:reactor-core:3.5.3` or newer as dependency
*/
@ApiStatus.Experimental
public static <T> Mono<T> withSentry(Mono<T> mono) {
final @NotNull IHub oldHub = Sentry.getCurrentHub();
final @NotNull IHub clonedHub = oldHub.clone();

/**
* WARNING: Cannot set the clonedHub as current hub.
* It would be used by others to clone again causing shared hubs and scopes and thus
* leading to issues like unrelated breadcrumbs showing up in events.
*/
// Sentry.setCurrentHub(clonedHub);

return Mono.deferContextual(ctx -> mono).contextWrite(Context.of(SentryReactorThreadLocalAccessor.KEY, clonedHub));
}

/**
* Writes the Sentry {@link IHub} to the {@link Context} and uses {@link io.micrometer.context.ThreadLocalAccessor} to propagate it.
*
* This requires
* - reactor.core.publisher.Hooks#enableAutomaticContextPropagation() to be enabled
* - having `io.micrometer:context-propagation:1.0.2` or newer as dependency
* - having `io.projectreactor:reactor-core:3.5.3` or newer as dependency
*/
@ApiStatus.Experimental
public static <T> Flux<T> withSentry(Flux<T> flux) {
final @NotNull IHub oldHub = Sentry.getCurrentHub();
final @NotNull IHub clonedHub = oldHub.clone();

/**
* WARNING: Cannot set the clonedHub as current hub.
* It would be used by others to clone again causing shared hubs and scopes and thus
* leading to issues like unrelated breadcrumbs showing up in events.
*/
// Sentry.setCurrentHub(clonedHub);

return Flux.deferContextual(ctx -> flux).contextWrite(Context.of(SentryReactorThreadLocalAccessor.KEY, clonedHub));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.sentry.spring.jakarta.webflux;

import org.jetbrains.annotations.ApiStatus;

import io.micrometer.context.ThreadLocalAccessor;
import io.sentry.IHub;
import io.sentry.NoOpHub;
import io.sentry.Sentry;

@ApiStatus.Experimental
public final class SentryReactorThreadLocalAccessor implements ThreadLocalAccessor<IHub> {

public static final String KEY = "sentry-hub";

@Override
public Object key() {
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "get");
return KEY;
}

@Override
public IHub getValue() {
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "get value");
return Sentry.getCurrentHub();
}

@Override
public void setValue(IHub value) {
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "set value " + value);
Sentry.setCurrentHub(value);
}

@Override
public void reset() {
// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "reset");
Sentry.setCurrentHub(NoOpHub.getInstance());
}

// @Override
// public void restore(IHub previousValue) {
//// Sentry.getCurrentHub().getOptions().getLogger().log(SentryLevel.WARNING, "restore value " + previousValue);
// ThreadLocalAccessor.super.restore(previousValue);
// }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.sentry.spring.jakarta.webflux;

import com.jakewharton.nopen.annotation.Open;

import io.sentry.Sentry;
import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_REQUEST;
import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_RESPONSE;

Expand All @@ -18,7 +21,8 @@

/** Manages {@link io.sentry.Scope} in Webflux request processing. */
@ApiStatus.Experimental
public final class SentryWebFilter implements WebFilter {
@Open
public class SentryWebFilter implements WebFilter {
private final @NotNull IHub hub;
private final @NotNull SentryRequestResolver sentryRequestResolver;

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

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;

import io.sentry.IHub;
import reactor.core.publisher.Mono;

/** Manages {@link io.sentry.Scope} in Webflux request processing. */
@ApiStatus.Experimental
public final class SentryWebFilterWithThreadLocalAccessor extends SentryWebFilter {

public SentryWebFilterWithThreadLocalAccessor(final @NotNull IHub hub) {
super(hub);
}

@Override
public Mono<Void> filter(
final @NotNull ServerWebExchange serverWebExchange,
final @NotNull WebFilterChain webFilterChain) {
return ReactorUtils.withSentry(super.filter(serverWebExchange, webFilterChain));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.sentry.spring.jakarta.webflux.SentryReactorThreadLocalAccessor

0 comments on commit b3f3d7a

Please sign in to comment.