Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Spring Webflux integration (#1529)

## 5.1.0-beta.2

* Fix: Handling missing Spring Security on classpath on Java 8 (#1552)
Expand All @@ -19,7 +21,7 @@
## 5.1.0-beta.1

* Feat: Measure app start time (#1487)
* Feat: Automatic breadcrumbs logging for fragment lifecycle (#1522)
* Feat: Automatic breadcrumbs logging for fragment lifecycle (#1522)

## 5.0.1

Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ object Config {
val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion"
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
val springBootStarterWebflux = "org.springframework.boot:spring-boot-starter-webflux:$springBootVersion"
val springBootStarterAop = "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"

val springWeb = "org.springframework:spring-webmvc"
val springWebflux = "org.springframework:spring-webflux"
val springSecurityWeb = "org.springframework.security:spring-security-web"
val springSecurityConfig = "org.springframework.security:spring-security-config"
val springAop = "org.springframework:spring-aop"
Expand All @@ -85,6 +87,8 @@ object Config {
val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"

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

val reactorCore = "io.projectreactor:reactor-core:3.4.6"
}

object AnnotationProcessors {
Expand Down
19 changes: 19 additions & 0 deletions sentry-samples/sentry-samples-spring-boot-webflux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Sentry Sample Spring Boot Webflux

Sample application showing how to use Sentry with [Spring Webflux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) and [Spring boot](http://spring.io/projects/spring-boot).

## How to run?

To see events triggered in this sample application in your Sentry dashboard, go to `src/main/resources/application.properties` and replace the test DSN with your own DSN.

Then, execute a command from the module directory:

```
../../gradlew bootRun
```

Make an HTTP request that will trigger events:

```
curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
public class io/sentry/samples/spring/boot/Person {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun getFirstName ()Ljava/lang/String;
public fun getLastName ()Ljava/lang/String;
public fun toString ()Ljava/lang/String;
}

public class io/sentry/samples/spring/boot/PersonController {
public fun <init> (Lio/sentry/samples/spring/boot/PersonService;)V
}

public class io/sentry/samples/spring/boot/PersonService {
public fun <init> ()V
}

public class io/sentry/samples/spring/boot/SentryDemoApplication {
public fun <init> ()V
public static fun main ([Ljava/lang/String;)V
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id(Config.BuildPlugins.springBoot) version Config.springBootVersion
id(Config.BuildPlugins.springDependencyManagement) version Config.BuildPlugins.springDependencyManagementVersion
kotlin("jvm")
kotlin("plugin.spring") version Config.kotlinVersion
}

group = "io.sentry.sample.spring-boot"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
mavenCentral()
}

dependencies {
implementation(Config.Libs.springBootStarterWebflux)
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
implementation(project(":sentry-spring-boot-starter"))
implementation(project(":sentry-logback"))
testImplementation(Config.Libs.springBootStarterTest) {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.sentry.samples.spring.boot;

public class Person {
private final String firstName;
private final String lastName;

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

@Override
public String toString() {
return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.sentry.samples.spring.boot;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/person/")
public class PersonController {
private final PersonService personService;
private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class);

public PersonController(PersonService personService) {
this.personService = personService;
}

@GetMapping("{id}")
Person person(@PathVariable Long id) {
LOGGER.info("Loading person with id={}", id);
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
}

@PostMapping
Mono<Person> create(@RequestBody Person person) {
return personService.create(person);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry.samples.spring.boot;

import io.sentry.Sentry;
import java.time.Duration;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@Service
public class PersonService {

Mono<Person> create(Person person) {
return Mono.delay(Duration.ofMillis(100))
.publishOn(Schedulers.boundedElastic())
.doOnNext(__ -> Sentry.captureMessage("Creating person"))
.map(__ -> person);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.sentry.samples.spring.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SentryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SentryDemoApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard
sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563
sentry.send-default-pii=true
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-breadcrumb-level=debug
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ public class io/sentry/spring/boot/SentryProperties$Logging {
public fun setMinimumEventLevel (Lorg/slf4j/event/Level;)V
}

public class io/sentry/spring/boot/SentryWebfluxAutoConfiguration {
public fun <init> ()V
public fun sentryScheduleHookApplicationRunner ()Lorg/springframework/boot/ApplicationRunner;
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebExceptionHandler;
public fun sentryWebFilter (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebFilter;
}

2 changes: 2 additions & 0 deletions sentry-spring-boot-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
compileOnly(Config.Libs.servletApi)
compileOnly(Config.Libs.springBootStarterAop)
compileOnly(Config.Libs.springBootStarterSecurity)
compileOnly(Config.Libs.reactorCore)

annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure)
annotationProcessor(Config.AnnotationProcessors.springBootConfiguration)
Expand All @@ -61,6 +62,7 @@ dependencies {
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.Libs.springBootStarterTest)
testImplementation(Config.Libs.springBootStarterWeb)
testImplementation(Config.Libs.springBootStarterWebflux)
testImplementation(Config.Libs.springBootStarterSecurity)
testImplementation(Config.Libs.springBootStarterAop)
testImplementation(Config.TestLibs.awaitility)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.sentry.spring.boot;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.IHub;
import io.sentry.spring.webflux.SentryScheduleHook;
import io.sentry.spring.webflux.SentryWebExceptionHandler;
import io.sentry.spring.webflux.SentryWebFilter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
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.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.scheduler.Schedulers;

/** Configures Sentry integration for Spring Webflux and Project Reactor. */
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnBean(IHub.class)
@ConditionalOnClass(Schedulers.class)
@Open
@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());
};
}

/** 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);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.sentry.spring.boot.SentryAutoConfiguration,\
io.sentry.spring.boot.SentryLogbackAppenderAutoConfiguration
io.sentry.spring.boot.SentryLogbackAppenderAutoConfiguration, \
io.sentry.spring.boot.SentryWebfluxAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.sentry.spring.boot

import io.sentry.spring.webflux.SentryWebExceptionHandler
import io.sentry.spring.webflux.SentryWebFilter
import kotlin.test.Test
import org.assertj.core.api.Assertions.assertThat
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
import org.springframework.boot.test.context.FilteredClassLoader
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner
import reactor.core.scheduler.Schedulers

class SentryWebfluxAutoConfigurationTest {

private val contextRunner = ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration::class.java, SentryWebfluxAutoConfiguration::class.java, SentryAutoConfiguration::class.java))

@Test
fun `configures sentryWebFilter`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.run {
assertThat(it).hasSingleBean(SentryWebFilter::class.java)
}
}

@Test
fun `configures exception handler`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run {
assertThat(it).hasSingleBean(SentryWebExceptionHandler::class.java)
}
}

@Test
fun `does not run when reactor is not on the classpath`() {
contextRunner
.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.withClassLoader(FilteredClassLoader(Schedulers::class.java))
.run {
assertThat(it).doesNotHaveBean(SentryWebExceptionHandler::class.java)
assertThat(it).doesNotHaveBean(SentryWebFilter::class.java)
}
}

@Test
fun `does not run when dsn is not configured`() {
contextRunner
.withClassLoader(FilteredClassLoader(Schedulers::class.java))
.run {
assertThat(it).doesNotHaveBean(SentryWebExceptionHandler::class.java)
assertThat(it).doesNotHaveBean(SentryWebFilter::class.java)
}
}
}
22 changes: 22 additions & 0 deletions sentry-spring/api/sentry-spring.api
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public final class io/sentry/spring/HttpServletRequestSentryUserProvider : io/se
}

public class io/sentry/spring/SentryExceptionResolver : org/springframework/core/Ordered, org/springframework/web/servlet/HandlerExceptionResolver {
public static final field MECHANISM_TYPE Ljava/lang/String;
public fun <init> (Lio/sentry/IHub;I)V
public fun getOrder ()I
public fun resolveException (Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;Ljava/lang/Object;Ljava/lang/Exception;)Lorg/springframework/web/servlet/ModelAndView;
Expand Down Expand Up @@ -142,3 +143,24 @@ public final class io/sentry/spring/tracing/TransactionNameProvider {
public fun provideTransactionName (Ljavax/servlet/http/HttpServletRequest;)Ljava/lang/String;
}

public class io/sentry/spring/webflux/SentryRequestResolver {
public fun <init> (Lio/sentry/IHub;)V
public fun resolveSentryRequest (Lorg/springframework/http/server/reactive/ServerHttpRequest;)Lio/sentry/protocol/Request;
}

public final class io/sentry/spring/webflux/SentryScheduleHook : java/util/function/Function {
public fun <init> ()V
public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object;
public fun apply (Ljava/lang/Runnable;)Ljava/lang/Runnable;
}

public final class io/sentry/spring/webflux/SentryWebExceptionHandler : org/springframework/web/server/WebExceptionHandler {
public fun <init> (Lio/sentry/IHub;)V
public fun handle (Lorg/springframework/web/server/ServerWebExchange;Ljava/lang/Throwable;)Lreactor/core/publisher/Mono;
}

public final class io/sentry/spring/webflux/SentryWebFilter : org/springframework/web/server/WebFilter {
public fun <init> (Lio/sentry/IHub;)V
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
}

Loading