Skip to content

Commit 3dd4220

Browse files
authored
Merge c6cb568 into c2c78de
2 parents c2c78de + c6cb568 commit 3dd4220

File tree

20 files changed

+223
-43
lines changed

20 files changed

+223
-43
lines changed

.github/ISSUE_TEMPLATE/bug_report_java.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ body:
3434
- sentry-openfeign
3535
- sentry-apache-http-client-5
3636
- sentry-okhttp
37+
- sentry-reactor
3738
- other
3839
validations:
3940
required: true

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ object Config {
259259
val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta"
260260
val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper"
261261
val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp"
262+
val SENTRY_REACTOR_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.reactor"
262263
val group = "io.sentry"
263264
val description = "SDK for sentry.io"
264265
val versionNameProp = "versionName"

sentry-reactor/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# sentry-reactor
2+
3+
This module provides a set of utilities to manually instrument your application to send data to Sentry when using [Reactor](https://projectreactor.io/).
4+
5+
## Setup
6+
7+
Please refer to the documentation on how to set up our [Java SDK](https://docs.sentry.io/platforms/java/),
8+
or our [Spring](https://docs.sentry.io/platforms/java/guides/spring/)
9+
or [Spring Boot](https://docs.sentry.io/platforms/java/guides/spring-boot/) integrations if you are using Spring WebFlux.
10+
11+
If you're using our Spring Boot integration, this module will be available and set up automatically.
12+
13+
Otherwise, you'll need to perform the following steps to get started.
14+
15+
Add the latest version of `io.sentry.reactor` as a dependency.
16+
Make sure you are using `io.micrometer:context-propagation:1.0.2` or later, provided by `io.projectreactor:reactor-core:3.5.3` or later.
17+
18+
Then, enable automatic context propagation, which should happen as early as possible in your application lifecycle:
19+
```java
20+
import reactor.core.publisher.Hooks;
21+
// ...
22+
Hooks.enableAutomaticContextPropagation();
23+
```
24+
25+
Finally, enable the `SentryReactorThreadLocalAccessor`:
26+
```java
27+
import io.micrometer.context.ContextRegistry;
28+
import io.sentry.reactor.SentryReactorThreadLocalAccessor;
29+
// ...
30+
ContextRegistry.getInstance().registerThreadLocalAccessor(SentryReactorThreadLocalAccessor());
31+
```
32+
33+
You can also use SPI to enable it by creating a file in `resources/META-INF.services/io.micrometer.context.ThreadLocalAccessor` with the content:
34+
```
35+
io.sentry.reactor.SentryReactorThreadLocalAccessor
36+
```
37+
and then calling
38+
```java
39+
import io.micrometer.context.ContextRegistry;
40+
// ...
41+
ContextRegistry.getInstance().loadThreadLocalAccessors();
42+
```
43+
44+
## Usage
45+
46+
You can use the provided utilities to wrap `Mono` and `Flux` objects to enable correct errors, breadcrumbs and tracing reporting in your application.
47+
48+
For normal use cases, you should wrap your operations on `Mono` or `Flux` objects using the `withSentry` function.
49+
This will fork the *current scopes* and use them throughout the stream's execution context.
50+
51+
You can use the provided utilities to wrap `Mono` and `Flux` objects to enable correct error, breadcrumbs and tracing reporting for your reactive streams.
52+
53+
For more complex use cases, you can also use the `withSentryForkedRoots` to fork the root scopes or `withSentryScopes` to wrap the operation in arbitrary scopes.
54+
For more information, you can consult our [scopes documentation](https://docs.sentry.io/platforms/java/enriching-events/scopes).
55+
56+
Examples of usage of this module with Spring WebFlux are provided in
57+
[sentry-samples-spring-boot-webflux](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux)
58+
and
59+
[sentry-samples-spring-boot-webflux-jakarta](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux-jakarta)
60+
.

sentry-reactor/api/sentry-reactor.api

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
public final class io/sentry/reactor/BuildConfig {
2+
public static final field SENTRY_REACTOR_SDK_NAME Ljava/lang/String;
3+
public static final field VERSION_NAME Ljava/lang/String;
4+
}
5+
6+
public final class io/sentry/reactor/SentryReactorThreadLocalAccessor : io/micrometer/context/ThreadLocalAccessor {
7+
public static final field KEY Ljava/lang/String;
8+
public fun <init> ()V
9+
public fun getValue ()Lio/sentry/IScopes;
10+
public synthetic fun getValue ()Ljava/lang/Object;
11+
public fun key ()Ljava/lang/Object;
12+
public fun reset ()V
13+
public fun setValue (Lio/sentry/IScopes;)V
14+
public synthetic fun setValue (Ljava/lang/Object;)V
15+
}
16+
17+
public class io/sentry/reactor/SentryReactorUtils {
18+
public fun <init> ()V
19+
public static fun withSentry (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux;
20+
public static fun withSentry (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono;
21+
public static fun withSentryForkedRoots (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux;
22+
public static fun withSentryForkedRoots (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono;
23+
public static fun withSentryScopes (Lreactor/core/publisher/Flux;Lio/sentry/IScopes;)Lreactor/core/publisher/Flux;
24+
public static fun withSentryScopes (Lreactor/core/publisher/Mono;Lio/sentry/IScopes;)Lreactor/core/publisher/Mono;
25+
}
26+

sentry-reactor/build.gradle.kts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import net.ltgt.gradle.errorprone.errorprone
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
plugins {
5+
`java-library`
6+
kotlin("jvm")
7+
jacoco
8+
id(Config.QualityPlugins.errorProne)
9+
id(Config.QualityPlugins.gradleVersions)
10+
id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion
11+
}
12+
13+
configure<JavaPluginExtension> {
14+
sourceCompatibility = JavaVersion.VERSION_17
15+
targetCompatibility = JavaVersion.VERSION_17
16+
}
17+
18+
tasks.withType<KotlinCompile>().configureEach {
19+
kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()
20+
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
21+
}
22+
23+
dependencies {
24+
api(projects.sentry)
25+
compileOnly(Config.Libs.reactorCore)
26+
compileOnly(Config.Libs.contextPropagation)
27+
28+
compileOnly(Config.CompileOnly.nopen)
29+
errorprone(Config.CompileOnly.nopenChecker)
30+
errorprone(Config.CompileOnly.errorprone)
31+
errorprone(Config.CompileOnly.errorProneNullAway)
32+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
33+
34+
// tests
35+
testImplementation(projects.sentryTestSupport)
36+
testImplementation(kotlin(Config.kotlinStdLib))
37+
testImplementation(Config.TestLibs.kotlinTestJunit)
38+
testImplementation(Config.TestLibs.mockitoKotlin)
39+
40+
testImplementation(Config.Libs.reactorCore)
41+
testImplementation(Config.Libs.contextPropagation)
42+
43+
testImplementation(platform("org.junit:junit-bom:5.10.0"))
44+
testImplementation("org.junit.jupiter:junit-jupiter")
45+
}
46+
47+
configure<SourceSetContainer> {
48+
test {
49+
java.srcDir("src/test/java")
50+
}
51+
}
52+
53+
jacoco {
54+
toolVersion = Config.QualityPlugins.Jacoco.version
55+
}
56+
57+
tasks.jacocoTestReport {
58+
reports {
59+
xml.required.set(true)
60+
html.required.set(false)
61+
}
62+
}
63+
64+
tasks {
65+
jacocoTestCoverageVerification {
66+
violationRules {
67+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
68+
}
69+
}
70+
check {
71+
dependsOn(jacocoTestCoverageVerification)
72+
dependsOn(jacocoTestReport)
73+
}
74+
}
75+
76+
buildConfig {
77+
useJavaOutput()
78+
packageName("io.sentry.reactor")
79+
buildConfigField("String", "SENTRY_REACTOR_SDK_NAME", "\"${Config.Sentry.SENTRY_REACTOR_SDK_NAME}\"")
80+
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
81+
}
82+
83+
val generateBuildConfig by tasks
84+
tasks.withType<JavaCompile>().configureEach {
85+
dependsOn(generateBuildConfig)
86+
options.errorprone {
87+
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
88+
option("NullAway:AnnotatedPackages", "io.sentry")
89+
}
90+
}
91+
92+
repositories {
93+
mavenCentral()
94+
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
package io.sentry.spring.jakarta.webflux;
1+
package io.sentry.reactor;
22

33
import io.micrometer.context.ThreadLocalAccessor;
44
import io.sentry.IScopes;
55
import io.sentry.NoOpScopes;
66
import io.sentry.Sentry;
7-
import org.jetbrains.annotations.ApiStatus;
87

9-
@ApiStatus.Experimental
108
public final class SentryReactorThreadLocalAccessor implements ThreadLocalAccessor<IScopes> {
119

1210
public static final String KEY = "sentry-scopes";
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
package io.sentry.spring.jakarta.webflux;
1+
package io.sentry.reactor;
22

3+
import com.jakewharton.nopen.annotation.Open;
34
import io.sentry.IScopes;
45
import io.sentry.Sentry;
5-
import org.jetbrains.annotations.ApiStatus;
66
import org.jetbrains.annotations.NotNull;
77
import reactor.core.publisher.Flux;
88
import reactor.core.publisher.Mono;
99
import reactor.util.context.Context;
1010

11-
@ApiStatus.Experimental
12-
public final class ReactorUtils {
11+
@Open
12+
public class SentryReactorUtils {
1313

1414
/**
1515
* Writes the current Sentry {@link IScopes} to the {@link Context} and uses {@link
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.sentry.reactor.SentryReactorThreadLocalAccessor
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.sentry.spring.jakarta.webflux
1+
package io.sentry.reactor
22

33
import io.sentry.IScopes
44
import io.sentry.NoOpScopes
@@ -18,11 +18,12 @@ import kotlin.test.assertEquals
1818
import kotlin.test.assertNotSame
1919
import kotlin.test.assertSame
2020

21-
class ReactorUtilsTest {
21+
class SentryReactorUtilsTest {
2222

2323
@BeforeTest
2424
fun setup() {
2525
Hooks.enableAutomaticContextPropagation()
26+
Sentry.init("https://key@sentry.io/proj")
2627
}
2728

2829
@AfterTest
@@ -34,7 +35,7 @@ class ReactorUtilsTest {
3435
fun `propagates scopes inside mono`() {
3536
val scopesToUse = mock<IScopes>()
3637
var scopesInside: IScopes? = null
37-
val mono = ReactorUtils.withSentryScopes(
38+
val mono = SentryReactorUtils.withSentryScopes(
3839
Mono.just("hello")
3940
.publishOn(Schedulers.boundedElastic())
4041
.map { it ->
@@ -52,7 +53,7 @@ class ReactorUtilsTest {
5253
fun `propagates scopes inside flux`() {
5354
val scopesToUse = mock<IScopes>()
5455
var scopesInside: IScopes? = null
55-
val flux = ReactorUtils.withSentryScopes(
56+
val flux = SentryReactorUtils.withSentryScopes(
5657
Flux.just("hello")
5758
.publishOn(Schedulers.boundedElastic())
5859
.map { it ->
@@ -101,7 +102,7 @@ class ReactorUtilsTest {
101102
val mockScopes = mock<IScopes>()
102103
whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock<IScopes>())
103104
Sentry.setCurrentScopes(mockScopes)
104-
ReactorUtils.withSentry(Mono.just("hello")).block()
105+
SentryReactorUtils.withSentry(Mono.just("hello")).block()
105106

106107
verify(mockScopes).forkedCurrentScope(any())
107108
}
@@ -111,7 +112,7 @@ class ReactorUtilsTest {
111112
val mockScopes = mock<IScopes>()
112113
whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock<IScopes>())
113114
Sentry.setCurrentScopes(mockScopes)
114-
ReactorUtils.withSentry(Flux.just("hello")).blockFirst()
115+
SentryReactorUtils.withSentry(Flux.just("hello")).blockFirst()
115116

116117
verify(mockScopes).forkedCurrentScope(any())
117118
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import io.opentelemetry.context.Scope;
66
import io.sentry.ISpan;
77
import io.sentry.Sentry;
8-
import io.sentry.spring.jakarta.webflux.ReactorUtils;
8+
import io.sentry.reactor.SentryReactorUtils;
99
import org.jetbrains.annotations.NotNull;
1010
import org.springframework.web.bind.annotation.GetMapping;
1111
import org.springframework.web.bind.annotation.PathVariable;
@@ -51,7 +51,7 @@ Todo todo(@PathVariable Long id) {
5151
@GetMapping("/todo-webclient/{id}")
5252
Todo todoWebClient(@PathVariable Long id) {
5353
Hooks.enableAutomaticContextPropagation();
54-
return ReactorUtils.withSentry(
54+
return SentryReactorUtils.withSentry(
5555
Mono.just(true)
5656
.publishOn(Schedulers.boundedElastic())
5757
.flatMap(

0 commit comments

Comments
 (0)