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

Add support for async methods in Spring MVC. #1652

Merged
merged 3 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Add support for async methods in Spring MVC (#1652)

## 5.1.0

* Feat: Spring WebClient integration (#1621)
Expand Down
5 changes: 5 additions & 0 deletions sentry-spring/api/sentry-spring.api
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public class io/sentry/spring/SentrySpringServletContainerInitializer : javax/se
public fun onStartup (Ljava/util/Set;Ljavax/servlet/ServletContext;)V
}

public final class io/sentry/spring/SentryTaskDecorator : org/springframework/core/task/TaskDecorator {
public fun <init> ()V
public fun decorate (Ljava/lang/Runnable;)Ljava/lang/Runnable;
}

public class io/sentry/spring/SentryUserFilter : javax/servlet/Filter {
public fun <init> (Lio/sentry/IHub;Ljava/util/List;)V
public fun doFilter (Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.spring;

import io.sentry.IHub;
import io.sentry.Sentry;
import java.util.concurrent.Callable;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.Async;

/**
* Sets a current hub on a thread running a {@link Runnable} given by parameter. Used to propagate
* the current {@link IHub} on the thread executing async task - like MVC controller methods
* returning a {@link Callable} or Spring beans methods annotated with {@link Async}.
*/
public final class SentryTaskDecorator implements TaskDecorator {
@Override
public @NotNull Runnable decorate(final @NotNull Runnable runnable) {
final IHub oldState = Sentry.getCurrentHub();
final IHub newHub = Sentry.getCurrentHub().clone();
return () -> {
Sentry.setCurrentHub(newHub);
try {
runnable.run();
} finally {
Sentry.setCurrentHub(oldState);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.sentry.SpanStatus
import io.sentry.spring.EnableSentry
import io.sentry.spring.SentryExceptionResolver
import io.sentry.spring.SentrySpringFilter
import io.sentry.spring.SentryTaskDecorator
import io.sentry.spring.SentryUserFilter
import io.sentry.spring.SentryUserProvider
import io.sentry.spring.tracing.SentryTracingConfiguration
Expand All @@ -23,8 +24,8 @@ import io.sentry.spring.tracing.SentryTransaction
import io.sentry.test.checkEvent
import io.sentry.test.checkTransaction
import io.sentry.transport.ITransport
import java.lang.Exception
import java.time.Duration
import java.util.concurrent.Callable
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.kotlin.await
import org.junit.Before
Expand Down Expand Up @@ -237,6 +238,21 @@ class SentrySpringIntegrationTest {
}, anyOrNull())
}
}

@Test
fun `scope is applied to events triggered in async methods`() {
val restTemplate = TestRestTemplate().withBasicAuth("user", "password")

restTemplate.getForEntity("http://localhost:$port/callable", String::class.java)

await.untilAsserted {
verify(transport).send(checkEvent { event ->
assertThat(event.message!!.formatted).isEqualTo("this message should be in the scope of the request")
assertThat(event.request).isNotNull()
assertThat(event.request!!.url).isEqualTo("http://localhost:$port/callable")
}, anyOrNull())
}
}
}

@SpringBootApplication
Expand Down Expand Up @@ -278,6 +294,9 @@ open class App {
this.filter = SentryTracingFilter(hub)
this.order = Ordered.HIGHEST_PRECEDENCE + 1 // must run after SentrySpringFilter
}

@Bean
open fun sentryTaskDecorator() = SentryTaskDecorator()
}

@Service
Expand Down Expand Up @@ -330,6 +349,14 @@ class HelloController {
fun throwsHandled() {
throw CustomException("handled exception")
}

@GetMapping("/callable")
fun callable(): Callable<String> {
return Callable {
Sentry.captureMessage("this message should be in the scope of the request")
"from callable"
}
}
}

class CustomException(message: String) : RuntimeException(message)
Expand Down