diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts index 0b20b8c6eb..7e74de3c77 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") testImplementation(Config.Libs.springBootStarterTest) { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java index f659c91bef..6e8357c4ae 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java @@ -8,6 +8,17 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; + +import java.util.UUID; + +import io.sentry.spring.webflux.SentryWebfluxHubHolder; +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryOnComplete; +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryOnFirst; +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryFinally; +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryOnError; +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryOnNext; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController @@ -20,12 +31,91 @@ public PersonController(PersonService personService) { this.personService = personService; } - @GetMapping("{id}") + @GetMapping("p/{id}") Person person(@PathVariable Long id) { LOGGER.info("Loading person with id={}", id); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } + @GetMapping("p4/{id}") + Person person4(@PathVariable Long id) { + return new Person("first", "last"); + } + + @GetMapping("p2/{id}") + Mono person2(@PathVariable Long id) { + return Mono.error(new IllegalArgumentException("Something went wrong2 [id=" + id + "]")); +// LOGGER.info("Loading person2 with id={}", id); + } + + @GetMapping("p3/{id}") + Mono person3(@PathVariable Long id, ServerWebExchange serverWebExchange) { + String uniq = UUID.randomUUID().toString(); + return personService.find(id) + .doFirst(withSentryOnFirst(serverWebExchange, () -> LOGGER.info("Finding person with id " + uniq))) + .doOnError(withSentryOnError(serverWebExchange, e -> LOGGER.error("Hello from error " + uniq, e))) + .doFinally(withSentryFinally(serverWebExchange, s -> LOGGER.warn("Finally for person with id " + uniq + ":" + s))) + .doOnEach(withSentryOnComplete(x -> LOGGER.info("oncomplete " + uniq))) + .doOnEach(withSentryOnNext(p -> LOGGER.info("Found " + uniq))) + .doOnEach(withSentryOnError(e -> LOGGER.error("oneach error " + uniq, e))); + } + + @GetMapping("all1") + Flux all(ServerWebExchange serverWebExchange) { + String uniq = UUID.randomUUID().toString(); + return personService.findAll() + .doFirst(withSentryOnFirst(serverWebExchange, () -> LOGGER.info("Finding all people " + uniq))) + .doOnError(withSentryOnError(serverWebExchange, e -> LOGGER.error("Hello from error " + uniq, e))) + .doFinally(withSentryFinally(serverWebExchange, __ -> LOGGER.warn("Finally for all people " + uniq))) + .doOnEach(withSentryOnComplete(__ -> LOGGER.info("oncomplete " + uniq))) + .doOnEach(withSentryOnNext(p -> LOGGER.info("Found " + uniq + " " + p.toString()))) + .doOnComplete(withSentryOnComplete(serverWebExchange, () -> LOGGER.info("on complete " + uniq))) + .doOnEach(withSentryOnError(e -> LOGGER.error("oneach error " + uniq, e))); + } + + @GetMapping("allerror") + Flux allError(ServerWebExchange serverWebExchange) { + String uniq = UUID.randomUUID().toString(); + return personService.findAllException() + .doFirst(withSentryOnFirst(serverWebExchange, () -> LOGGER.info("Finding all people with error " + uniq))) + .doOnError(withSentryOnError(serverWebExchange, e -> LOGGER.error("Hello from error " + uniq, e))) + .doFinally(withSentryFinally(serverWebExchange, s -> LOGGER.warn("Finally for all people with error " + uniq))) + .doOnEach(withSentryOnComplete(x -> LOGGER.info("oncomplete " + uniq))) + .doOnEach(withSentryOnNext(p -> LOGGER.info("Found " + uniq + " " + p.toString()))) + .doOnComplete(withSentryOnComplete(serverWebExchange, () -> LOGGER.info("on complete " + uniq))) + .doOnEach(withSentryOnError(e -> LOGGER.error("oneach error " + uniq, e))); + } + + @GetMapping("allerror3") + Flux allError3(ServerWebExchange serverWebExchange) { + String uniq = UUID.randomUUID().toString(); + return personService.findAll() + .doFirst(withSentryOnFirst(serverWebExchange, () -> LOGGER.info("Finding all people with error 3 " + uniq))) + .doOnError(withSentryOnError(serverWebExchange, e -> LOGGER.error("Hello from error 3 " + uniq, e))) + .doFinally(withSentryFinally(serverWebExchange, s -> LOGGER.error("Finally for all people with error 3 " + uniq))) + .doOnEach(withSentryOnComplete(x -> LOGGER.info("oncomplete 3 " + uniq))) + .doOnEach(withSentryOnNext(p -> LOGGER.info("Found 3 " + uniq + " " + p.toString()))) + .doOnComplete(withSentryOnComplete(serverWebExchange, () -> LOGGER.info("on complete 3 " + uniq))) + .doOnEach(withSentryOnError(e -> LOGGER.error("oneach error 3 " + uniq, e))); + } + + @GetMapping("all") + Flux findAll(ServerWebExchange serverWebExchange) { + return personService.findAll() + .doFirst(withSentryOnFirst(serverWebExchange, () -> LOGGER.info("doFirst"))) + .doOnNext(withSentryOnNext(serverWebExchange, p -> LOGGER.info("doOnNext " + p.toString()))) + .doOnComplete(withSentryOnComplete(serverWebExchange, () -> LOGGER.info("doOnComplete"))) + .doOnError(withSentryOnError(serverWebExchange, e -> LOGGER.error("doOnError", e))) + .doFinally(withSentryFinally(serverWebExchange, __ -> LOGGER.info("doFinally"))) + .doOnEach(withSentryOnComplete(__ -> LOGGER.info("onEachComplete"))) + .doOnEach(withSentryOnNext(p -> LOGGER.info("onEachNext " + p.toString()))) + .doOnEach(withSentryOnError(e -> LOGGER.error("onEachError", e))) + .flatMap(p -> SentryWebfluxHubHolder.getHubFlux() + .map(hub -> hub.captureMessage("Hello message")) + .flatMap(__ -> Flux.just(p))) + ; + } + @PostMapping Mono create(@RequestBody Person person) { return personService.create(person); diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonService.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonService.java index ed7422d9d0..71afcbd8f8 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonService.java @@ -3,6 +3,9 @@ import io.sentry.Sentry; import java.time.Duration; import org.springframework.stereotype.Service; + +import static io.sentry.spring.webflux.SentryWebfluxHubHolder.withSentryOnNext; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -12,7 +15,35 @@ public class PersonService { Mono create(Person person) { return Mono.delay(Duration.ofMillis(100)) .publishOn(Schedulers.boundedElastic()) - .doOnNext(__ -> Sentry.captureMessage("Creating person")) + .doOnEach(withSentryOnNext(__ -> Sentry.captureMessage("Creating person"))) +// .doOnNext(__ -> Sentry.captureMessage("Creating person")) .map(__ -> person); } + + Mono find(Long id) { + return Mono.delay(Duration.ofMillis(100)) + .publishOn(Schedulers.boundedElastic()) + .doOnEach(withSentryOnNext(__ -> Sentry.captureMessage("Finding person"))) + .flatMap(p -> { + if (id > 10) { + return Mono.error(new RuntimeException("Caused on purpose for webflux " + Thread.currentThread().getName())); + } else { + return Mono.just(new Person("first", "last")); + } + }); + } + + Flux findAll() { + return Mono.delay(Duration.ofMillis(100)).flux() + .publishOn(Schedulers.boundedElastic()) + .doOnEach(withSentryOnNext(__ -> Sentry.captureMessage("Finding all people"))) + .flatMap(__ -> Flux.just(new Person("first1", "last1"), new Person("first2", "last2"))); + } + + Flux findAllException() { + return Mono.delay(Duration.ofMillis(100)).flux() + .publishOn(Schedulers.boundedElastic()) + .doOnEach(withSentryOnNext(__ -> Sentry.captureMessage("Found another person"))) + .flatMap(__ -> Flux.error(new RuntimeException("Caused on purpose for webflux findAll"))); + } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties index a7107b70f2..ccf7b917e6 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties @@ -5,5 +5,9 @@ sentry.debug=true # Sentry Spring Boot integration allows more fine-grained SentryOptions configuration sentry.max-breadcrumbs=150 # Logback integration configuration options -sentry.logging.minimum-event-level=info +sentry.logging.minimum-event-level=error sentry.logging.minimum-breadcrumb-level=debug +#sentry.traces-sample-rate=1.0 +management.endpoints.web.exposure.include=health,info,prometheus +logging.level.root=INFO +logging.level.io.sentry=DEBUG diff --git a/sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java b/sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java index 7a5998a29a..a061f23322 100644 --- a/sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java +++ b/sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java @@ -2,6 +2,8 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.spring.tracing.SentryTracingFilter; +import io.sentry.spring.tracing.TransactionNameProvider; import io.sentry.spring.webflux.SentryScheduleHook; import io.sentry.spring.webflux.SentryWebExceptionHandler; import io.sentry.spring.webflux.SentryWebFilter; @@ -10,9 +12,16 @@ import org.springframework.boot.ApplicationRunner; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import io.sentry.spring.webflux.SentryWebTracingFilter; import reactor.core.scheduler.Schedulers; /** Configures Sentry integration for Spring Webflux and Project Reactor. */ @@ -24,20 +33,31 @@ @ApiStatus.Experimental public class SentryWebfluxAutoConfiguration { + private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE; + /** Configures hook that sets correct hub on the executing thread. */ - @Bean - public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() { - return args -> { - Schedulers.onScheduleHook("sentry", new SentryScheduleHook()); - }; - } +// @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 + @Order(SENTRY_SPRING_FILTER_PRECEDENCE) public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) { return new SentryWebFilter(hub); } + @Bean + @Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1) + @Conditional(SentryAutoConfiguration.SentryTracingCondition.class) + @ConditionalOnMissingBean(name = "sentryWebTracingFilter") + public @NotNull SentryWebTracingFilter sentryWebTracingFilter() { + return new SentryWebTracingFilter(); + } + /** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */ @Bean public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) { diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java index 52c6ef49d9..6d9863d6c6 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java @@ -20,11 +20,13 @@ public class SentryRequestResolver { private static final List SENSITIVE_HEADERS = Arrays.asList("X-FORWARDED-FOR", "AUTHORIZATION", "COOKIE"); + private final boolean isSendDefaultPii; - private final @NotNull IHub hub; +// private final @NotNull IHub hub; - public SentryRequestResolver(final @NotNull IHub hub) { - this.hub = Objects.requireNonNull(hub, "options is required"); + public SentryRequestResolver(final boolean isSendDefaultPii) { +// this.hub = Objects.requireNonNull(hub, "options is required"); + this.isSendDefaultPii = isSendDefaultPii; } public @NotNull Request resolveSentryRequest(final @NotNull ServerHttpRequest httpRequest) { @@ -34,7 +36,7 @@ public SentryRequestResolver(final @NotNull IHub hub) { sentryRequest.setUrl(httpRequest.getURI().toString()); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); - if (hub.getOptions().isSendDefaultPii()) { + if (isSendDefaultPii) { sentryRequest.setCookies(toString(httpRequest.getHeaders().get("Cookies"))); } return sentryRequest; @@ -45,7 +47,7 @@ Map resolveHeadersMap(final HttpHeaders request) { final Map headersMap = new HashMap<>(); for (Map.Entry> entry : request.entrySet()) { // do not copy personal information identifiable headers - if (hub.getOptions().isSendDefaultPii() + if (isSendDefaultPii || !SENSITIVE_HEADERS.contains(entry.getKey().toUpperCase(Locale.ROOT))) { headersMap.put(entry.getKey(), toString(entry.getValue())); } diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java index efa01e377d..267265e1d8 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java @@ -5,6 +5,8 @@ import java.util.function.Function; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Hook meant to used with {@link reactor.core.scheduler.Schedulers#onScheduleHook(String, @@ -12,8 +14,13 @@ */ @ApiStatus.Experimental public final class SentryScheduleHook implements Function { + + private static final Logger log = LoggerFactory.getLogger("hooklogger"); + @Override public Runnable apply(final @NotNull Runnable runnable) { + String threadName = Thread.currentThread().getName(); + log.debug("Starting hook on " + threadName); final IHub oldState = Sentry.getCurrentHub(); final IHub newHub = Sentry.getCurrentHub().clone(); return () -> { @@ -22,6 +29,7 @@ public Runnable apply(final @NotNull Runnable runnable) { runnable.run(); } finally { Sentry.setCurrentHub(oldState); + log.debug("Ended hook on " + threadName); } }; } diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebExceptionHandler.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebExceptionHandler.java index 5e9b89842a..fc35ce7132 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebExceptionHandler.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebExceptionHandler.java @@ -12,6 +12,8 @@ import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; @@ -24,15 +26,17 @@ // at -1 @ApiStatus.Experimental public final class SentryWebExceptionHandler implements WebExceptionHandler { - private final @NotNull IHub hub; +// private final @NotNull IHub hub; +// private static final Logger log = LoggerFactory.getLogger("exceptionlogger"); public SentryWebExceptionHandler(final @NotNull IHub hub) { - this.hub = Objects.requireNonNull(hub, "hub is required"); +// this.hub = Objects.requireNonNull(hub, "hub is required"); } @Override public @NotNull Mono handle( final @NotNull ServerWebExchange serverWebExchange, final @NotNull Throwable ex) { +// log.debug("Exception handled on thread " + Thread.currentThread().getName()); if (!(ex instanceof ResponseStatusException)) { final Mechanism mechanism = new Mechanism(); mechanism.setType("SentryWebExceptionHandler"); @@ -47,7 +51,16 @@ public SentryWebExceptionHandler(final @NotNull IHub hub) { hint.set(WEBFLUX_EXCEPTION_HANDLER_REQUEST, serverWebExchange.getRequest()); hint.set(WEBFLUX_EXCEPTION_HANDLER_RESPONSE, serverWebExchange.getResponse()); - hub.captureEvent(event, hint); + return SentryWebfluxHubHolder.getHub(serverWebExchange).flatMap(hub -> { +// log.warn("hub " + hub); + hub.captureEvent(event, hint); + return Mono.error(ex); + }); +// return SentryWebfluxHubHolder.getHub(serverWebExchange).map(hub -> { +// log.warn("hub " + hub); +// hub.captureEvent(event, hint); +// return null; +// }); } return Mono.error(ex); } diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java index 0163ddc2bf..d6da6da4fb 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java @@ -1,5 +1,6 @@ package io.sentry.spring.webflux; +import io.sentry.Sentry; import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_REQUEST; import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_RESPONSE; @@ -9,33 +10,47 @@ import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; + +import java.util.UUID; + import reactor.core.publisher.Mono; /** Manages {@link io.sentry.Scope} in Webflux request processing. */ @ApiStatus.Experimental public final class SentryWebFilter implements WebFilter { - private final @NotNull IHub hub; + public static final String SENTRY_HUB_KEY = "sentry-hub"; private final @NotNull SentryRequestResolver sentryRequestResolver; public SentryWebFilter(final @NotNull IHub hub) { - this.hub = Objects.requireNonNull(hub, "hub is required"); - this.sentryRequestResolver = new SentryRequestResolver(hub); + this.sentryRequestResolver = new SentryRequestResolver(hub.getOptions().isSendDefaultPii()); } @Override public Mono filter( final @NotNull ServerWebExchange serverWebExchange, final @NotNull WebFilterChain webFilterChain) { + @NotNull final String hubId = UUID.randomUUID().toString(); + @NotNull final IHub hub = Sentry.getHub(hubId); + + serverWebExchange.getAttributes().put(SENTRY_HUB_KEY, hub); + return webFilterChain .filter(serverWebExchange) + .contextWrite(ctx -> { + return ctx; + }) .doFinally( __ -> { hub.popScope(); + Sentry.clearHub(hubId); }) .doFirst( () -> { @@ -51,6 +66,7 @@ public Mono filter( Breadcrumb.http(request.getURI().toString(), request.getMethodValue()), hint); hub.configureScope( scope -> scope.setRequest(sentryRequestResolver.resolveSentryRequest(request))); - }); + }) + .contextWrite(ctx -> ctx.put(SENTRY_HUB_KEY, hub)); } } diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebTracingFilter.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebTracingFilter.java new file mode 100644 index 0000000000..8c6da468b0 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebTracingFilter.java @@ -0,0 +1,78 @@ +package io.sentry.spring.webflux; + +import com.jakewharton.nopen.annotation.Open; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +import io.sentry.CustomSamplingContext; +import io.sentry.IHub; +import io.sentry.ITransaction; +import io.sentry.Sentry; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.TransactionNameSource; +import static io.sentry.spring.webflux.SentryWebFilter.SENTRY_HUB_KEY; +import reactor.core.publisher.Mono; + +@Open +public class SentryWebTracingFilter implements WebFilter { + + private static final String TRANSACTION_OP = "http.server"; + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + final @Nullable Object hubObject = exchange.getAttributes().getOrDefault(SENTRY_HUB_KEY, null); + // TODO not use currentHub as fallback? + final @NotNull IHub hub = hubObject == null ? Sentry.getCurrentHub() : (IHub) hubObject; + + final @NotNull ITransaction transaction = startTransaction(hub, exchange); + + return chain.filter(exchange) + .doFinally(__ -> { + String transactionName = TransactionNameProvider.provideTransactionName(exchange); + if (transactionName != null) { + transaction.setName(transactionName, TransactionNameSource.ROUTE); + transaction.setOperation(TRANSACTION_OP); + } + if (transaction.getStatus() == null) { + final @Nullable ServerHttpResponse response = exchange.getResponse(); + if (response != null) { + final @Nullable Integer rawStatusCode = response.getRawStatusCode(); + if (rawStatusCode != null) { + transaction.setStatus(SpanStatus.fromHttpStatusCode(rawStatusCode)); + } + } + } + transaction.finish(); + }) + .doOnError(e -> { + transaction.setStatus(SpanStatus.INTERNAL_ERROR); + transaction.setThrowable(e); + }); + } + + private @NotNull ITransaction startTransaction(@NotNull IHub hub, @NotNull ServerWebExchange exchange) { + // TODO resume from headers including baggage support + + final @NotNull ServerHttpRequest request = exchange.getRequest(); + final @NotNull String name = request.getMethod() + " " + request.getURI().getPath(); + + final @NotNull CustomSamplingContext customSamplingContext = new CustomSamplingContext(); + customSamplingContext.set("request", request); + + final @NotNull TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setCustomSamplingContext(customSamplingContext); + transactionOptions.setBindToScope(true); + + return hub.startTransaction(new TransactionContext(name, TransactionNameSource.URL, "http.server"), transactionOptions); + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebfluxHubHolder.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebfluxHubHolder.java new file mode 100644 index 0000000000..fbbbdf9076 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebfluxHubHolder.java @@ -0,0 +1,182 @@ +package io.sentry.spring.webflux; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.server.ServerWebExchange; + +import java.util.Optional; +import java.util.function.Consumer; + +import io.sentry.IHub; +import io.sentry.Sentry; +import static io.sentry.spring.webflux.SentryWebFilter.SENTRY_HUB_KEY; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; +import reactor.core.publisher.SignalType; + +public final class SentryWebfluxHubHolder { + + public static @Nullable IHub getHubFromAttributes(final @NotNull ServerWebExchange serverWebExchange) { + @Nullable Object hubFromAttributesObject = serverWebExchange.getAttributes().get(SENTRY_HUB_KEY); + return hubFromAttributesObject == null ? null : (IHub) hubFromAttributesObject; + } + + public static @NotNull Mono getHub() { + return Mono.deferContextual(ctx -> + { + @Nullable final Object hubObject = ctx.get(SENTRY_HUB_KEY); + if (hubObject == null) { + return Mono.error(new RuntimeException("Unable to find sentry hub in reactor context.")); + } + return Mono.just((IHub) hubObject); + }); + } + + public static @NotNull Flux getHubFlux() { + return getHub().flux(); + } + + public static @NotNull Mono getHub(final @NotNull ServerWebExchange serverWebExchange) { + return Mono.deferContextual(ctx -> { + @NotNull final Optional hub = ctx.getOrEmpty(SENTRY_HUB_KEY); + if (hub.isPresent()) { + return Mono.just(hub.get()); + } else { + @Nullable final IHub hubFromAttributes = getHubFromAttributes(serverWebExchange); + if (hubFromAttributes == null) { + return Mono.error(new RuntimeException("Unable to find sentry hub in reactor context or attributes.")); + } else { + return Mono.just(hubFromAttributes); + } + } + }); + } + + public static @NotNull Runnable withSentryOnFirst(final @NotNull ServerWebExchange serverWebExchange, final @NotNull Runnable runnable) { + return () -> { + @Nullable final IHub hub = getHubFromAttributes(serverWebExchange); + if (hub != null) { + Sentry.setCurrentHub(hub); + runnable.run(); + } else { + runnable.run(); + } + }; + } + + public static @NotNull Runnable withSentryOnComplete(final @NotNull ServerWebExchange serverWebExchange, final @NotNull Runnable runnable) { + return withSentryOnFirst(serverWebExchange, runnable); + } + + public static @NotNull Consumer withSentryFinally(final @NotNull ServerWebExchange serverWebExchange, final @NotNull Consumer consumer) { + return signalType -> { + @Nullable final IHub hub = getHubFromAttributes(serverWebExchange); + if (hub != null) { + Sentry.setCurrentHub(hub); + // TODO does resetting hub in finally make a difference? + consumer.accept(signalType); + } else { + consumer.accept(signalType); + } + }; + } + + public static @NotNull Consumer withSentryOnNext(final @NotNull ServerWebExchange serverWebExchange, final @NotNull Consumer consumer) { + return param -> { + @Nullable final IHub hub = getHubFromAttributes(serverWebExchange); + if (hub != null) { + Sentry.setCurrentHub(hub); + // TODO does resetting hub in finally make a difference? + consumer.accept(param); + } else { + consumer.accept(param); + } + }; + } + + public static @NotNull Consumer withSentryOnError(final @NotNull ServerWebExchange serverWebExchange, final @NotNull Consumer consumer) { + return t -> { +// if (SignalType.ON_COMPLETE.equals(signalType)) { +// return; +// } + @Nullable final IHub hub = getHubFromAttributes(serverWebExchange); + if (hub != null) { + Sentry.setCurrentHub(hub); + // TODO does resetting hub in finally make a difference? + consumer.accept(t); + } else { + consumer.accept(t); + } + }; + } + + public static @NotNull Consumer> withSentryOnNext(final @NotNull Consumer consumer) { + return signal -> { + if (!signal.isOnNext()) { + return; + } + + Optional hub = signal.getContextView().getOrEmpty(SENTRY_HUB_KEY); + if (hub.isPresent()) { + Sentry.setCurrentHub(hub.get()); + // TODO does resetting hub in finally make a difference? + consumer.accept(signal.get()); + } else { + consumer.accept(signal.get()); + } + }; + } + + public static @NotNull Consumer> withSentryOnComplete(final @NotNull Consumer consumer) { + return signal -> { + if (!signal.isOnComplete()) { + return; + } + + Optional hub = signal.getContextView().getOrEmpty(SENTRY_HUB_KEY); + if (hub.isPresent()) { + Sentry.setCurrentHub(hub.get()); + // TODO does resetting hub in finally make a difference? + consumer.accept(signal.get()); + } else { + consumer.accept(signal.get()); + } + }; + } + + // TODO not working? + public static @NotNull Consumer> withSentryOnSubscribe(final @NotNull Consumer consumer) { + return signal -> { + if (!signal.isOnSubscribe()) { + return; + } + + Optional hub = signal.getContextView().getOrEmpty(SENTRY_HUB_KEY); + if (hub.isPresent()) { + Sentry.setCurrentHub(hub.get()); + // TODO does resetting hub in finally make a difference? + consumer.accept(signal.get()); + } else { + consumer.accept(signal.get()); + } + }; + } + + public static @NotNull Consumer> withSentryOnError(final @NotNull Consumer consumer) { + return signal -> { + if (!signal.isOnError()) { + return; + } + Optional hub = signal.getContextView().getOrEmpty(SENTRY_HUB_KEY); + if (hub.isPresent()) { + Sentry.setCurrentHub(hub.get()); + // TODO does resetting hub in finally make a difference? + consumer.accept(signal.getThrowable()); + } else { + consumer.accept(signal.getThrowable()); + } + }; + } + +} diff --git a/sentry/src/main/java/io/sentry/HubContainer.java b/sentry/src/main/java/io/sentry/HubContainer.java new file mode 100644 index 0000000000..d9f0365523 --- /dev/null +++ b/sentry/src/main/java/io/sentry/HubContainer.java @@ -0,0 +1,23 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class HubContainer { + + private @Nullable IHub hub; + + public HubContainer(@NotNull IHub hub) { + this.hub = hub; + } + + public @Nullable IHub getHub() { + return hub; + } + + public void unset() { + hub = null; + } +} diff --git a/sentry/src/main/java/io/sentry/HubStore.java b/sentry/src/main/java/io/sentry/HubStore.java new file mode 100644 index 0000000000..75fb6346ea --- /dev/null +++ b/sentry/src/main/java/io/sentry/HubStore.java @@ -0,0 +1,129 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@ApiStatus.Internal +public final class HubStore { + private static class SingletonHolder { + + public static final HubStore instance = new HubStore(); + + } + public static HubStore getInstance() { + return SingletonHolder.instance; + } + + private static final @NotNull ThreadLocal currentThreadHub = new ThreadLocal<>(); + + private final @NotNull ConcurrentHashMap hubs = new ConcurrentHashMap<>(); + // this only serves to keep a strong ref on the IHub until it is removed explicitly + + public @Nullable IHub get() { + return get(getDefaultHubId()); + } + + public @Nullable IHub get(@NotNull final String hubId) { + @Nullable HubStorageEntry entry = getEntry(hubId); + if (entry != null) { + return entry.hub.get(); + } + return null; + } + + private @Nullable HubStorageEntry getEntry(@NotNull final String hubId) { + return hubs.get(hubId); + } + + public void set(@NotNull final IHub hub) { + String threadId = getDefaultHubId(); + set(threadId, HubIdType.THREAD_ID, hub); + } + + public void remove() { + remove(getDefaultHubId()); + } + + public void remove(@NotNull final String hubId) { + @Nullable HubStorageEntry entry = getEntry(hubId); + if (entry != null) { + entry.cleanup(); + } + hubs.remove(hubId); + } + + // TODO find places to call cleanup + public void cleanup() { + @NotNull final Set> entries = ((Map) hubs).entrySet(); + @NotNull final Set toBeRemoved = new HashSet<>(); + @NotNull final Set threads = Thread.getAllStackTraces().keySet(); + @NotNull final Set threadIds = new HashSet<>(); + + for (final Thread t: threads) { + threadIds.add(String.valueOf(t.getId())); + } + + for (final Map.Entry entry : entries) { + @NotNull final String hubId = entry.getKey(); + @NotNull final HubStorageEntry value = entry.getValue(); + if (value.hub.get() == null) { + toBeRemoved.add(hubId); + } + if (HubIdType.THREAD_ID.equals(value.hubIdType)) { + if (!threadIds.contains(hubId)) { + toBeRemoved.add(hubId); + } + } + } + + for (String hubId: toBeRemoved) { + remove(hubId); + } + } + + private String getDefaultHubId() { + // TODO is it a good idea to use thread id? how fast will it be reused? + // thread name could be changed and thus be even worse as key + return String.valueOf(Thread.currentThread().getId()); + } + + public void set(@NotNull final String hubId, @NotNull HubIdType hubIdType, @NotNull final IHub hub) { + HubContainer hubContainer = new HubContainer(hub); + if (HubIdType.THREAD_ID.equals(hubIdType)) { + currentThreadHub.set(hubContainer); + } + hubs.put(hubId, new HubStorageEntry(hubIdType, hub, hubContainer)); + } + + public enum HubIdType { + THREAD_ID, + WEBFLUX_CONTEXT_ID, + UNKNOWN + } + + private static final class HubStorageEntry { + private @NotNull final HubIdType hubIdType; + private @NotNull final WeakReference hub; + private @NotNull final WeakReference hubContainer; + + private HubStorageEntry(@NotNull HubIdType hubIdType, @NotNull IHub hub, @NotNull HubContainer hubContainer) { + this.hubIdType = hubIdType; + this.hub = new WeakReference<>(hub); + this.hubContainer = new WeakReference<>(hubContainer); + } + + private void cleanup() { + @Nullable HubContainer hubContainer = this.hubContainer.get(); + if (hubContainer != null) { + hubContainer.unset(); + } + } + } +} diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index cdb108a057..2c88d3888b 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -18,7 +18,9 @@ public final class Sentry { private Sentry() {} /** Holds Hubs per thread or only mainHub if globalHubMode is enabled. */ - private static final @NotNull ThreadLocal currentHub = new ThreadLocal<>(); +// private static final @NotNull ThreadLocal currentHub = new ThreadLocal<>(); +// private static final @NotNull ThreadLocal currentHub = new ThreadLocal<>(); + private static final @NotNull HubStore hubStore = HubStore.getInstance(); /** The Main Hub or NoOp if Sentry is disabled. */ private static volatile @NotNull IHub mainHub = NoOpHub.getInstance(); @@ -39,17 +41,42 @@ private Sentry() {} if (globalHubMode) { return mainHub; } - IHub hub = currentHub.get(); + + IHub hub = hubStore.get(); +// IHub hub = currentHub.get(); if (hub == null || hub instanceof NoOpHub) { hub = mainHub.clone(); - currentHub.set(hub); + hubStore.set(hub); +// currentHub.set(hub); + } + return hub; + } + + @ApiStatus.Internal + public static @NotNull IHub getHub(@NotNull String hubId) { + if (globalHubMode) { + return mainHub; + } + + IHub hub = hubStore.get(hubId); + if (hub == null || hub instanceof NoOpHub) { + hub = mainHub.clone(); + hubStore.set(hubId, HubStore.HubIdType.WEBFLUX_CONTEXT_ID, hub); +// currentHub.set(hub); } return hub; } @ApiStatus.Internal // exposed for the coroutines integration in SentryContext public static void setCurrentHub(final @NotNull IHub hub) { - currentHub.set(hub); +// currentHub.set(hub); + hubStore.set(hub); + } + + @ApiStatus.Internal + public static void clearHub(@NotNull String hubId) { + hubStore.remove(hubId); + hubStore.cleanup(); } /** @@ -176,7 +203,8 @@ private static synchronized void init( final IHub hub = getCurrentHub(); mainHub = new Hub(options); - currentHub.set(mainHub); +// currentHub.set(mainHub); + hubStore.set(mainHub); hub.close(); @@ -261,7 +289,8 @@ public static synchronized void close() { final IHub hub = getCurrentHub(); mainHub = NoOpHub.getInstance(); // remove thread local to avoid memory leak - currentHub.remove(); +// currentHub.remove(); + hubStore.remove(); hub.close(); }