Skip to content

Commit 41cf842

Browse files
authored
Support check-ins for Quartz (#2940)
1 parent 8d29d97 commit 41cf842

File tree

31 files changed

+538
-4
lines changed

31 files changed

+538
-4
lines changed

.craft.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ targets:
4848
maven:io.sentry:sentry-apollo:
4949
maven:io.sentry:sentry-jdbc:
5050
maven:io.sentry:sentry-graphql:
51+
# maven:io.sentry:sentry-quartz:
5152
maven:io.sentry:sentry-android-navigation:
5253
maven:io.sentry:sentry-compose:
5354
maven:io.sentry:sentry-compose-android:

.github/ISSUE_TEMPLATE/bug_report_java.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ body:
2727
- sentry-logback
2828
- sentry-log4j2
2929
- sentry-graphql
30+
- sentry-quartz
3031
- sentry-openfeign
3132
- sentry-apache-http-client-5
3233
- other

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Add `sendModules` option for disable sending modules ([#2926](https://github.com/getsentry/sentry-java/pull/2926))
88
- Send `db.system` and `db.name` in span data for androidx.sqlite spans ([#2928](https://github.com/getsentry/sentry-java/pull/2928))
99
- Add API for sending checkins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935))
10+
- Support check-ins for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940))
1011

1112
### Fixes
1213

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Sentry SDK for Java and Android
4848
| sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) |
4949
| sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) |
5050
| sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) |
51+
| sentry-quartz | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) |
5152
| sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) |
5253
| sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) |
5354
| sentry-opentelemetry-agentcustomization | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization) |

buildSrc/src/main/java/Config.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ object Config {
7575

7676
val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion"
7777
val springBootStarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBootVersion"
78+
val springBootStarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBootVersion"
7879
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
7980
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
8081
val springBootStarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion"
@@ -85,6 +86,7 @@ object Config {
8586

8687
val springBoot3Starter = "org.springframework.boot:spring-boot-starter:$springBoot3Version"
8788
val springBoot3StarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBoot3Version"
89+
val springBoot3StarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBoot3Version"
8890
val springBoot3StarterTest = "org.springframework.boot:spring-boot-starter-test:$springBoot3Version"
8991
val springBoot3StarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBoot3Version"
9092
val springBoot3StarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBoot3Version"
@@ -128,6 +130,8 @@ object Config {
128130

129131
val graphQlJava = "com.graphql-java:graphql-java:17.3"
130132

133+
val quartz = "org.quartz-scheduler:quartz:2.3.0"
134+
131135
val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect"
132136
val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib"
133137

@@ -227,6 +231,7 @@ object Config {
227231
val SENTRY_APOLLO3_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo3"
228232
val SENTRY_APOLLO_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo"
229233
val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql"
234+
val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz"
230235
val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc"
231236
val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet"
232237
val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public final class io/sentry/quartz/BuildConfig {
2+
public static final field SENTRY_QUARTZ_SDK_NAME Ljava/lang/String;
3+
public static final field VERSION_NAME Ljava/lang/String;
4+
}
5+
6+
public final class io/sentry/quartz/SentryJobListener : org/quartz/JobListener {
7+
public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String;
8+
public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String;
9+
public fun <init> ()V
10+
public fun getName ()Ljava/lang/String;
11+
public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V
12+
public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V
13+
public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V
14+
}
15+

sentry-quartz/build.gradle.kts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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_1_8
15+
targetCompatibility = JavaVersion.VERSION_1_8
16+
}
17+
18+
tasks.withType<KotlinCompile>().configureEach {
19+
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
20+
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
21+
}
22+
23+
dependencies {
24+
api(projects.sentry)
25+
compileOnly(Config.Libs.quartz)
26+
27+
compileOnly(Config.CompileOnly.nopen)
28+
errorprone(Config.CompileOnly.nopenChecker)
29+
errorprone(Config.CompileOnly.errorprone)
30+
errorprone(Config.CompileOnly.errorProneNullAway)
31+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
32+
33+
// tests
34+
testImplementation(projects.sentry)
35+
testImplementation(projects.sentryTestSupport)
36+
testImplementation(kotlin(Config.kotlinStdLib))
37+
testImplementation(Config.TestLibs.kotlinTestJunit)
38+
testImplementation(Config.TestLibs.mockitoKotlin)
39+
testImplementation(Config.TestLibs.mockitoInline)
40+
}
41+
42+
configure<SourceSetContainer> {
43+
test {
44+
java.srcDir("src/test/java")
45+
}
46+
}
47+
48+
jacoco {
49+
toolVersion = Config.QualityPlugins.Jacoco.version
50+
}
51+
52+
tasks.jacocoTestReport {
53+
reports {
54+
xml.required.set(true)
55+
html.required.set(false)
56+
}
57+
}
58+
59+
tasks {
60+
jacocoTestCoverageVerification {
61+
violationRules {
62+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
63+
}
64+
}
65+
check {
66+
dependsOn(jacocoTestCoverageVerification)
67+
dependsOn(jacocoTestReport)
68+
}
69+
}
70+
71+
tasks.withType<JavaCompile>().configureEach {
72+
options.errorprone {
73+
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
74+
option("NullAway:AnnotatedPackages", "io.sentry")
75+
}
76+
}
77+
78+
buildConfig {
79+
useJavaOutput()
80+
packageName("io.sentry.quartz")
81+
buildConfigField("String", "SENTRY_QUARTZ_SDK_NAME", "\"${Config.Sentry.SENTRY_QUARTZ_SDK_NAME}\"")
82+
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
83+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package io.sentry.quartz;
2+
3+
import io.sentry.BuildConfig;
4+
import io.sentry.CheckIn;
5+
import io.sentry.CheckInStatus;
6+
import io.sentry.MonitorConfig;
7+
import io.sentry.MonitorSchedule;
8+
import io.sentry.MonitorScheduleUnit;
9+
import io.sentry.Sentry;
10+
import io.sentry.SentryIntegrationPackageStorage;
11+
import io.sentry.SentryLevel;
12+
import io.sentry.protocol.SentryId;
13+
import java.util.List;
14+
import java.util.TimeZone;
15+
import org.jetbrains.annotations.NotNull;
16+
import org.jetbrains.annotations.Nullable;
17+
import org.quartz.CalendarIntervalTrigger;
18+
import org.quartz.CronTrigger;
19+
import org.quartz.DateBuilder;
20+
import org.quartz.Job;
21+
import org.quartz.JobDetail;
22+
import org.quartz.JobExecutionContext;
23+
import org.quartz.JobExecutionException;
24+
import org.quartz.JobKey;
25+
import org.quartz.JobListener;
26+
import org.quartz.SimpleTrigger;
27+
import org.quartz.Trigger;
28+
29+
public final class SentryJobListener implements JobListener {
30+
31+
public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id";
32+
public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug";
33+
34+
public SentryJobListener() {
35+
SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz");
36+
SentryIntegrationPackageStorage.getInstance()
37+
.addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME);
38+
}
39+
40+
@Override
41+
public String getName() {
42+
return "sentry-job-listener";
43+
}
44+
45+
@Override
46+
public void jobToBeExecuted(JobExecutionContext context) {
47+
try {
48+
final @NotNull String slug = getSlug(context.getJobDetail());
49+
final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS);
50+
51+
final @Nullable MonitorConfig monitorConfig = extractMonitorConfig(context);
52+
if (monitorConfig != null) {
53+
checkIn.setMonitorConfig(monitorConfig);
54+
}
55+
56+
final @NotNull SentryId checkInId = Sentry.captureCheckIn(checkIn);
57+
context.put(SENTRY_CHECK_IN_ID_KEY, checkInId);
58+
context.put(SENTRY_CHECK_IN_SLUG_KEY, slug);
59+
} catch (Throwable t) {
60+
Sentry.getCurrentHub()
61+
.getOptions()
62+
.getLogger()
63+
.log(SentryLevel.ERROR, "Unable to capture check-in in jobToBeExecuted.", t);
64+
}
65+
}
66+
67+
private @NotNull String getSlug(final @Nullable JobDetail jobDetail) {
68+
if (jobDetail == null) {
69+
return "fallback";
70+
}
71+
final @NotNull StringBuilder slugBuilder = new StringBuilder();
72+
73+
final @Nullable JobKey key = jobDetail.getKey();
74+
if (key != null) {
75+
slugBuilder.append(key.getName());
76+
slugBuilder.append("__");
77+
}
78+
79+
final @Nullable Class<? extends Job> jobClass = jobDetail.getJobClass();
80+
if (jobClass != null) {
81+
slugBuilder.append(jobClass.getCanonicalName());
82+
}
83+
84+
return slugBuilder.toString();
85+
}
86+
87+
private @Nullable MonitorConfig extractMonitorConfig(final @NotNull JobExecutionContext context) {
88+
@Nullable MonitorSchedule schedule = null;
89+
@Nullable String cronExpression = null;
90+
@Nullable TimeZone timeZone = TimeZone.getDefault();
91+
@Nullable Integer repeatInterval = null;
92+
@Nullable MonitorScheduleUnit timeUnit = null;
93+
94+
try {
95+
List<? extends Trigger> triggersOfJob =
96+
context.getScheduler().getTriggersOfJob(context.getTrigger().getJobKey());
97+
for (Trigger trigger : triggersOfJob) {
98+
if (trigger instanceof CronTrigger) {
99+
final CronTrigger cronTrigger = (CronTrigger) trigger;
100+
cronExpression = cronTrigger.getCronExpression();
101+
timeZone = cronTrigger.getTimeZone();
102+
} else if (trigger instanceof SimpleTrigger) {
103+
final SimpleTrigger simpleTrigger = (SimpleTrigger) trigger;
104+
long tmpRepeatInterval = simpleTrigger.getRepeatInterval();
105+
repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval));
106+
timeUnit = MonitorScheduleUnit.MINUTE;
107+
} else if (trigger instanceof CalendarIntervalTrigger) {
108+
final CalendarIntervalTrigger calendarIntervalTrigger = (CalendarIntervalTrigger) trigger;
109+
DateBuilder.IntervalUnit repeatIntervalUnit =
110+
calendarIntervalTrigger.getRepeatIntervalUnit();
111+
int tmpRepeatInterval = calendarIntervalTrigger.getRepeatInterval();
112+
if (DateBuilder.IntervalUnit.SECOND.equals(repeatIntervalUnit)) {
113+
repeatInterval = secondsToMinutes(Double.valueOf(tmpRepeatInterval));
114+
timeUnit = MonitorScheduleUnit.MINUTE;
115+
} else if (DateBuilder.IntervalUnit.MILLISECOND.equals(repeatIntervalUnit)) {
116+
repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval));
117+
timeUnit = MonitorScheduleUnit.MINUTE;
118+
} else {
119+
repeatInterval = tmpRepeatInterval;
120+
timeUnit = convertUnit(repeatIntervalUnit);
121+
}
122+
}
123+
}
124+
} catch (Throwable t) {
125+
Sentry.getCurrentHub()
126+
.getOptions()
127+
.getLogger()
128+
.log(SentryLevel.ERROR, "Unable to extract monitor config for check-in.", t);
129+
}
130+
if (cronExpression != null) {
131+
schedule = MonitorSchedule.crontab(cronExpression);
132+
} else if (repeatInterval != null && timeUnit != null) {
133+
schedule = MonitorSchedule.interval(repeatInterval.intValue(), timeUnit);
134+
}
135+
136+
if (schedule != null) {
137+
final @Nullable MonitorConfig monitorConfig = new MonitorConfig(schedule);
138+
if (timeZone != null) {
139+
monitorConfig.setTimezone(timeZone.getID());
140+
}
141+
return monitorConfig;
142+
} else {
143+
return null;
144+
}
145+
}
146+
147+
private @Nullable Integer millisToMinutes(final @NotNull Double milis) {
148+
return Double.valueOf((milis / 1000.0) / 60.0).intValue();
149+
}
150+
151+
private @Nullable Integer secondsToMinutes(final @NotNull Double seconds) {
152+
return Double.valueOf(seconds / 60.0).intValue();
153+
}
154+
155+
private @Nullable MonitorScheduleUnit convertUnit(
156+
final @Nullable DateBuilder.IntervalUnit intervalUnit) {
157+
if (intervalUnit == null) {
158+
return null;
159+
}
160+
161+
if (DateBuilder.IntervalUnit.MINUTE.equals(intervalUnit)) {
162+
return MonitorScheduleUnit.MINUTE;
163+
} else if (DateBuilder.IntervalUnit.HOUR.equals(intervalUnit)) {
164+
return MonitorScheduleUnit.HOUR;
165+
} else if (DateBuilder.IntervalUnit.DAY.equals(intervalUnit)) {
166+
return MonitorScheduleUnit.DAY;
167+
} else if (DateBuilder.IntervalUnit.WEEK.equals(intervalUnit)) {
168+
return MonitorScheduleUnit.WEEK;
169+
} else if (DateBuilder.IntervalUnit.MONTH.equals(intervalUnit)) {
170+
return MonitorScheduleUnit.MONTH;
171+
} else if (DateBuilder.IntervalUnit.YEAR.equals(intervalUnit)) {
172+
return MonitorScheduleUnit.YEAR;
173+
}
174+
175+
return null;
176+
}
177+
178+
@Override
179+
public void jobExecutionVetoed(JobExecutionContext context) {
180+
// do nothing
181+
}
182+
183+
@Override
184+
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
185+
try {
186+
final @Nullable Object checkInIdObjectFromContext = context.get(SENTRY_CHECK_IN_ID_KEY);
187+
final @Nullable Object slugObjectFromContext = context.get(SENTRY_CHECK_IN_SLUG_KEY);
188+
final @NotNull SentryId checkInId =
189+
checkInIdObjectFromContext == null
190+
? new SentryId()
191+
: (SentryId) checkInIdObjectFromContext;
192+
final @Nullable String slug =
193+
slugObjectFromContext == null ? null : (String) slugObjectFromContext;
194+
if (slug != null) {
195+
final boolean isFailed = jobException != null;
196+
final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK;
197+
Sentry.captureCheckIn(new CheckIn(checkInId, slug, status));
198+
}
199+
} catch (Throwable t) {
200+
Sentry.getCurrentHub()
201+
.getOptions()
202+
.getLogger()
203+
.log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t);
204+
}
205+
}
206+
}

sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies {
2222
implementation(Config.Libs.springBoot3StarterWeb)
2323
implementation(Config.Libs.springBoot3StarterWebsocket)
2424
implementation(Config.Libs.springBoot3StarterGraphql)
25+
implementation(Config.Libs.springBoot3StarterQuartz)
2526
implementation(Config.Libs.springBoot3StarterWebflux)
2627
implementation(Config.Libs.springBoot3StarterAop)
2728
implementation(Config.Libs.aspectj)
@@ -32,6 +33,7 @@ dependencies {
3233
implementation(projects.sentrySpringBootStarterJakarta)
3334
implementation(projects.sentryLogback)
3435
implementation(projects.sentryGraphql)
36+
implementation(projects.sentryQuartz)
3537

3638
// database query tracing
3739
implementation(projects.sentryJdbc)

sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ void execute() throws InterruptedException {
3131
try {
3232
LOGGER.info("Executing scheduled job");
3333
Thread.sleep(2000L);
34-
Sentry.captureCheckIn(new CheckIn(checkInId, "my_monitor_slug", CheckInStatus.OK));
3534
} catch (Throwable t) {
3635
didError = true;
3736
throw t;

0 commit comments

Comments
 (0)