Skip to content

Commit c34b4fe

Browse files
authored
Merge 409827e into 8d29d97
2 parents 8d29d97 + 409827e commit c34b4fe

File tree

54 files changed

+920
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+920
-6
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+
- Automatic CRON checkins 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 <init> (Lio/sentry/IHub;)V
11+
public fun getName ()Ljava/lang/String;
12+
public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V
13+
public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V
14+
public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V
15+
}
16+

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: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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.HubAdapter;
7+
import io.sentry.IHub;
8+
import io.sentry.MonitorConfig;
9+
import io.sentry.MonitorSchedule;
10+
import io.sentry.MonitorScheduleUnit;
11+
import io.sentry.Sentry;
12+
import io.sentry.SentryIntegrationPackageStorage;
13+
import io.sentry.SentryLevel;
14+
import io.sentry.protocol.SentryId;
15+
import io.sentry.util.Objects;
16+
import java.util.List;
17+
import java.util.TimeZone;
18+
import org.jetbrains.annotations.ApiStatus;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.jetbrains.annotations.Nullable;
21+
import org.quartz.CalendarIntervalTrigger;
22+
import org.quartz.CronTrigger;
23+
import org.quartz.DateBuilder;
24+
import org.quartz.Job;
25+
import org.quartz.JobDetail;
26+
import org.quartz.JobExecutionContext;
27+
import org.quartz.JobExecutionException;
28+
import org.quartz.JobKey;
29+
import org.quartz.JobListener;
30+
import org.quartz.SimpleTrigger;
31+
import org.quartz.Trigger;
32+
33+
@ApiStatus.Experimental
34+
public final class SentryJobListener implements JobListener {
35+
36+
public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id";
37+
public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug";
38+
39+
private final @NotNull IHub hub;
40+
41+
public SentryJobListener() {
42+
this(HubAdapter.getInstance());
43+
}
44+
45+
public SentryJobListener(final @NotNull IHub hub) {
46+
this.hub = Objects.requireNonNull(hub, "hub is required");
47+
SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz");
48+
SentryIntegrationPackageStorage.getInstance()
49+
.addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME);
50+
}
51+
52+
@Override
53+
public String getName() {
54+
return "sentry-job-listener";
55+
}
56+
57+
@Override
58+
public void jobToBeExecuted(JobExecutionContext context) {
59+
try {
60+
if (isDisabled()) {
61+
return;
62+
}
63+
final @NotNull String slug = getSlug(context.getJobDetail());
64+
final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS);
65+
66+
final @Nullable MonitorConfig monitorConfig = extractMonitorConfig(context);
67+
if (monitorConfig != null) {
68+
checkIn.setMonitorConfig(monitorConfig);
69+
}
70+
71+
final @NotNull SentryId checkInId = Sentry.captureCheckIn(checkIn);
72+
context.put(SENTRY_CHECK_IN_ID_KEY, checkInId);
73+
context.put(SENTRY_CHECK_IN_SLUG_KEY, slug);
74+
} catch (Throwable t) {
75+
Sentry.getCurrentHub()
76+
.getOptions()
77+
.getLogger()
78+
.log(SentryLevel.ERROR, "Unable to capture check-in in jobToBeExecuted.", t);
79+
}
80+
}
81+
82+
private @NotNull String getSlug(final @Nullable JobDetail jobDetail) {
83+
if (jobDetail == null) {
84+
return "fallback";
85+
}
86+
final @NotNull StringBuilder slugBuilder = new StringBuilder();
87+
88+
final @Nullable JobKey key = jobDetail.getKey();
89+
if (key != null) {
90+
slugBuilder.append(key.getName());
91+
slugBuilder.append("__");
92+
}
93+
94+
final @Nullable Class<? extends Job> jobClass = jobDetail.getJobClass();
95+
if (jobClass != null) {
96+
slugBuilder.append(jobClass.getCanonicalName());
97+
}
98+
99+
return slugBuilder.toString();
100+
}
101+
102+
private @Nullable MonitorConfig extractMonitorConfig(final @NotNull JobExecutionContext context) {
103+
@Nullable MonitorSchedule schedule = null;
104+
@Nullable String cronExpression = null;
105+
@Nullable TimeZone timeZone = TimeZone.getDefault();
106+
@Nullable Integer repeatInterval = null;
107+
@Nullable MonitorScheduleUnit timeUnit = null;
108+
109+
try {
110+
List<? extends Trigger> triggersOfJob =
111+
context.getScheduler().getTriggersOfJob(context.getTrigger().getJobKey());
112+
for (Trigger trigger : triggersOfJob) {
113+
if (trigger instanceof CronTrigger) {
114+
final CronTrigger cronTrigger = (CronTrigger) trigger;
115+
cronExpression = cronTrigger.getCronExpression();
116+
timeZone = cronTrigger.getTimeZone();
117+
} else if (trigger instanceof SimpleTrigger) {
118+
final SimpleTrigger simpleTrigger = (SimpleTrigger) trigger;
119+
long tmpRepeatInterval = simpleTrigger.getRepeatInterval();
120+
repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval));
121+
timeUnit = MonitorScheduleUnit.MINUTE;
122+
} else if (trigger instanceof CalendarIntervalTrigger) {
123+
final CalendarIntervalTrigger calendarIntervalTrigger = (CalendarIntervalTrigger) trigger;
124+
DateBuilder.IntervalUnit repeatIntervalUnit =
125+
calendarIntervalTrigger.getRepeatIntervalUnit();
126+
int tmpRepeatInterval = calendarIntervalTrigger.getRepeatInterval();
127+
if (DateBuilder.IntervalUnit.SECOND.equals(repeatIntervalUnit)) {
128+
repeatInterval = secondsToMinutes(Double.valueOf(tmpRepeatInterval));
129+
timeUnit = MonitorScheduleUnit.MINUTE;
130+
} else if (DateBuilder.IntervalUnit.MILLISECOND.equals(repeatIntervalUnit)) {
131+
repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval));
132+
timeUnit = MonitorScheduleUnit.MINUTE;
133+
} else {
134+
repeatInterval = tmpRepeatInterval;
135+
timeUnit = convertUnit(repeatIntervalUnit);
136+
}
137+
}
138+
}
139+
} catch (Throwable t) {
140+
Sentry.getCurrentHub()
141+
.getOptions()
142+
.getLogger()
143+
.log(SentryLevel.ERROR, "Unable to extract monitor config for check-in.", t);
144+
}
145+
if (cronExpression != null) {
146+
schedule = MonitorSchedule.crontab(cronExpression);
147+
} else if (repeatInterval != null && timeUnit != null) {
148+
schedule = MonitorSchedule.interval(repeatInterval.intValue(), timeUnit);
149+
}
150+
151+
if (schedule != null) {
152+
final @Nullable MonitorConfig monitorConfig = new MonitorConfig(schedule);
153+
if (timeZone != null) {
154+
monitorConfig.setTimezone(timeZone.getID());
155+
}
156+
return monitorConfig;
157+
} else {
158+
return null;
159+
}
160+
}
161+
162+
private @Nullable Integer millisToMinutes(final @NotNull Double milis) {
163+
return Double.valueOf((milis / 1000.0) / 60.0).intValue();
164+
}
165+
166+
private @Nullable Integer secondsToMinutes(final @NotNull Double seconds) {
167+
return Double.valueOf(seconds / 60.0).intValue();
168+
}
169+
170+
private @Nullable MonitorScheduleUnit convertUnit(
171+
final @Nullable DateBuilder.IntervalUnit intervalUnit) {
172+
if (intervalUnit == null) {
173+
return null;
174+
}
175+
176+
if (DateBuilder.IntervalUnit.MINUTE.equals(intervalUnit)) {
177+
return MonitorScheduleUnit.MINUTE;
178+
} else if (DateBuilder.IntervalUnit.HOUR.equals(intervalUnit)) {
179+
return MonitorScheduleUnit.HOUR;
180+
} else if (DateBuilder.IntervalUnit.DAY.equals(intervalUnit)) {
181+
return MonitorScheduleUnit.DAY;
182+
} else if (DateBuilder.IntervalUnit.WEEK.equals(intervalUnit)) {
183+
return MonitorScheduleUnit.WEEK;
184+
} else if (DateBuilder.IntervalUnit.MONTH.equals(intervalUnit)) {
185+
return MonitorScheduleUnit.MONTH;
186+
} else if (DateBuilder.IntervalUnit.YEAR.equals(intervalUnit)) {
187+
return MonitorScheduleUnit.YEAR;
188+
}
189+
190+
return null;
191+
}
192+
193+
@Override
194+
public void jobExecutionVetoed(JobExecutionContext context) {
195+
// do nothing
196+
}
197+
198+
@Override
199+
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
200+
try {
201+
if (isDisabled()) {
202+
return;
203+
}
204+
205+
final @Nullable Object checkInIdObjectFromContext = context.get(SENTRY_CHECK_IN_ID_KEY);
206+
final @Nullable Object slugObjectFromContext = context.get(SENTRY_CHECK_IN_SLUG_KEY);
207+
final @NotNull SentryId checkInId =
208+
checkInIdObjectFromContext == null
209+
? new SentryId()
210+
: (SentryId) checkInIdObjectFromContext;
211+
final @Nullable String slug =
212+
slugObjectFromContext == null ? null : (String) slugObjectFromContext;
213+
if (slug != null) {
214+
final boolean isFailed = jobException != null;
215+
final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK;
216+
Sentry.captureCheckIn(new CheckIn(checkInId, slug, status));
217+
}
218+
} catch (Throwable t) {
219+
Sentry.getCurrentHub()
220+
.getOptions()
221+
.getLogger()
222+
.log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t);
223+
}
224+
}
225+
226+
private boolean isDisabled() {
227+
return !hub.getOptions().isEnableAutomaticCheckIns();
228+
}
229+
}

0 commit comments

Comments
 (0)