diff --git a/.craft.yml b/.craft.yml index 57e92b7995a..8caca1d0b9a 100644 --- a/.craft.yml +++ b/.craft.yml @@ -48,6 +48,7 @@ targets: maven:io.sentry:sentry-apollo: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: +# maven:io.sentry:sentry-quartz: maven:io.sentry:sentry-android-navigation: maven:io.sentry:sentry-compose: maven:io.sentry:sentry-compose-android: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index c71b3fb4947..429fbdee73a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -27,6 +27,7 @@ body: - sentry-logback - sentry-log4j2 - sentry-graphql + - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 - other diff --git a/CHANGELOG.md b/CHANGELOG.md index 29490479ab9..2fb41cc5c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add `sendModules` option for disable sending modules ([#2926](https://github.com/getsentry/sentry-java/pull/2926)) - Send `db.system` and `db.name` in span data for androidx.sqlite spans ([#2928](https://github.com/getsentry/sentry-java/pull/2928)) - Add API for sending checkins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) +- Support check-ins for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) ### Fixes diff --git a/README.md b/README.md index d10da65505c..afc81b60bd1 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Sentry SDK for Java and Android | 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) | | 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) | | 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) | +| 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) | | 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) | | 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) | | 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) | diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index eda3873d817..af4d49a3bdb 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -75,6 +75,7 @@ object Config { val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion" val springBootStarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBootVersion" + val springBootStarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBootVersion" val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion" val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion" val springBootStarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion" @@ -85,6 +86,7 @@ object Config { val springBoot3Starter = "org.springframework.boot:spring-boot-starter:$springBoot3Version" val springBoot3StarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBoot3Version" + val springBoot3StarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBoot3Version" val springBoot3StarterTest = "org.springframework.boot:spring-boot-starter-test:$springBoot3Version" val springBoot3StarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBoot3Version" val springBoot3StarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBoot3Version" @@ -128,6 +130,8 @@ object Config { val graphQlJava = "com.graphql-java:graphql-java:17.3" + val quartz = "org.quartz-scheduler:quartz:2.3.0" + val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib" @@ -227,6 +231,7 @@ object Config { val SENTRY_APOLLO3_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo3" val SENTRY_APOLLO_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo" val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql" + val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" diff --git a/sentry-quartz/api/sentry-quartz.api b/sentry-quartz/api/sentry-quartz.api new file mode 100644 index 00000000000..34a71295845 --- /dev/null +++ b/sentry-quartz/api/sentry-quartz.api @@ -0,0 +1,15 @@ +public final class io/sentry/quartz/BuildConfig { + public static final field SENTRY_QUARTZ_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/quartz/SentryJobListener : org/quartz/JobListener { + public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; + public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String; + public fun ()V + public fun getName ()Ljava/lang/String; + public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V + public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V + public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V +} + diff --git a/sentry-quartz/build.gradle.kts b/sentry-quartz/build.gradle.kts new file mode 100644 index 00000000000..8731f6a40b8 --- /dev/null +++ b/sentry-quartz/build.gradle.kts @@ -0,0 +1,83 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + compileOnly(Config.Libs.quartz) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.quartz") + buildConfigField("String", "SENTRY_QUARTZ_SDK_NAME", "\"${Config.Sentry.SENTRY_QUARTZ_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java new file mode 100644 index 00000000000..68d5afceaae --- /dev/null +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -0,0 +1,206 @@ +package io.sentry.quartz; + +import io.sentry.BuildConfig; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.MonitorConfig; +import io.sentry.MonitorSchedule; +import io.sentry.MonitorScheduleUnit; +import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import java.util.List; +import java.util.TimeZone; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronTrigger; +import org.quartz.DateBuilder; +import org.quartz.Job; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.JobListener; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; + +public final class SentryJobListener implements JobListener { + + public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; + public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug"; + + public SentryJobListener() { + SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz"); + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME); + } + + @Override + public String getName() { + return "sentry-job-listener"; + } + + @Override + public void jobToBeExecuted(JobExecutionContext context) { + try { + final @NotNull String slug = getSlug(context.getJobDetail()); + final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); + + final @Nullable MonitorConfig monitorConfig = extractMonitorConfig(context); + if (monitorConfig != null) { + checkIn.setMonitorConfig(monitorConfig); + } + + final @NotNull SentryId checkInId = Sentry.captureCheckIn(checkIn); + context.put(SENTRY_CHECK_IN_ID_KEY, checkInId); + context.put(SENTRY_CHECK_IN_SLUG_KEY, slug); + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to capture check-in in jobToBeExecuted.", t); + } + } + + private @NotNull String getSlug(final @Nullable JobDetail jobDetail) { + if (jobDetail == null) { + return "fallback"; + } + final @NotNull StringBuilder slugBuilder = new StringBuilder(); + + final @Nullable JobKey key = jobDetail.getKey(); + if (key != null) { + slugBuilder.append(key.getName()); + slugBuilder.append("__"); + } + + final @Nullable Class jobClass = jobDetail.getJobClass(); + if (jobClass != null) { + slugBuilder.append(jobClass.getCanonicalName()); + } + + return slugBuilder.toString(); + } + + private @Nullable MonitorConfig extractMonitorConfig(final @NotNull JobExecutionContext context) { + @Nullable MonitorSchedule schedule = null; + @Nullable String cronExpression = null; + @Nullable TimeZone timeZone = TimeZone.getDefault(); + @Nullable Integer repeatInterval = null; + @Nullable MonitorScheduleUnit timeUnit = null; + + try { + List triggersOfJob = + context.getScheduler().getTriggersOfJob(context.getTrigger().getJobKey()); + for (Trigger trigger : triggersOfJob) { + if (trigger instanceof CronTrigger) { + final CronTrigger cronTrigger = (CronTrigger) trigger; + cronExpression = cronTrigger.getCronExpression(); + timeZone = cronTrigger.getTimeZone(); + } else if (trigger instanceof SimpleTrigger) { + final SimpleTrigger simpleTrigger = (SimpleTrigger) trigger; + long tmpRepeatInterval = simpleTrigger.getRepeatInterval(); + repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else if (trigger instanceof CalendarIntervalTrigger) { + final CalendarIntervalTrigger calendarIntervalTrigger = (CalendarIntervalTrigger) trigger; + DateBuilder.IntervalUnit repeatIntervalUnit = + calendarIntervalTrigger.getRepeatIntervalUnit(); + int tmpRepeatInterval = calendarIntervalTrigger.getRepeatInterval(); + if (DateBuilder.IntervalUnit.SECOND.equals(repeatIntervalUnit)) { + repeatInterval = secondsToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else if (DateBuilder.IntervalUnit.MILLISECOND.equals(repeatIntervalUnit)) { + repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else { + repeatInterval = tmpRepeatInterval; + timeUnit = convertUnit(repeatIntervalUnit); + } + } + } + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to extract monitor config for check-in.", t); + } + if (cronExpression != null) { + schedule = MonitorSchedule.crontab(cronExpression); + } else if (repeatInterval != null && timeUnit != null) { + schedule = MonitorSchedule.interval(repeatInterval.intValue(), timeUnit); + } + + if (schedule != null) { + final @Nullable MonitorConfig monitorConfig = new MonitorConfig(schedule); + if (timeZone != null) { + monitorConfig.setTimezone(timeZone.getID()); + } + return monitorConfig; + } else { + return null; + } + } + + private @Nullable Integer millisToMinutes(final @NotNull Double milis) { + return Double.valueOf((milis / 1000.0) / 60.0).intValue(); + } + + private @Nullable Integer secondsToMinutes(final @NotNull Double seconds) { + return Double.valueOf(seconds / 60.0).intValue(); + } + + private @Nullable MonitorScheduleUnit convertUnit( + final @Nullable DateBuilder.IntervalUnit intervalUnit) { + if (intervalUnit == null) { + return null; + } + + if (DateBuilder.IntervalUnit.MINUTE.equals(intervalUnit)) { + return MonitorScheduleUnit.MINUTE; + } else if (DateBuilder.IntervalUnit.HOUR.equals(intervalUnit)) { + return MonitorScheduleUnit.HOUR; + } else if (DateBuilder.IntervalUnit.DAY.equals(intervalUnit)) { + return MonitorScheduleUnit.DAY; + } else if (DateBuilder.IntervalUnit.WEEK.equals(intervalUnit)) { + return MonitorScheduleUnit.WEEK; + } else if (DateBuilder.IntervalUnit.MONTH.equals(intervalUnit)) { + return MonitorScheduleUnit.MONTH; + } else if (DateBuilder.IntervalUnit.YEAR.equals(intervalUnit)) { + return MonitorScheduleUnit.YEAR; + } + + return null; + } + + @Override + public void jobExecutionVetoed(JobExecutionContext context) { + // do nothing + } + + @Override + public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { + try { + final @Nullable Object checkInIdObjectFromContext = context.get(SENTRY_CHECK_IN_ID_KEY); + final @Nullable Object slugObjectFromContext = context.get(SENTRY_CHECK_IN_SLUG_KEY); + final @NotNull SentryId checkInId = + checkInIdObjectFromContext == null + ? new SentryId() + : (SentryId) checkInIdObjectFromContext; + final @Nullable String slug = + slugObjectFromContext == null ? null : (String) slugObjectFromContext; + if (slug != null) { + final boolean isFailed = jobException != null; + final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK; + Sentry.captureCheckIn(new CheckIn(checkInId, slug, status)); + } + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index c326142f555..68a9937be55 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(Config.Libs.springBoot3StarterWeb) implementation(Config.Libs.springBoot3StarterWebsocket) implementation(Config.Libs.springBoot3StarterGraphql) + implementation(Config.Libs.springBoot3StarterQuartz) implementation(Config.Libs.springBoot3StarterWebflux) implementation(Config.Libs.springBoot3StarterAop) implementation(Config.Libs.aspectj) @@ -32,6 +33,7 @@ dependencies { implementation(projects.sentrySpringBootStarterJakarta) implementation(projects.sentryLogback) implementation(projects.sentryGraphql) + implementation(projects.sentryQuartz) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java index 4bc9c817e2b..994203b4140 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -31,7 +31,6 @@ void execute() throws InterruptedException { try { LOGGER.info("Executing scheduled job"); Thread.sleep(2000L); - Sentry.captureCheckIn(new CheckIn(checkInId, "my_monitor_slug", CheckInStatus.OK)); } catch (Throwable t) { didError = true; throw t; diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 6d91b889a64..d9e4a7cdb5e 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -1,10 +1,15 @@ package io.sentry.samples.spring.boot.jakarta; +import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import org.quartz.JobDetail; +import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -24,4 +29,31 @@ RestTemplate restTemplate(RestTemplateBuilder builder) { WebClient webClient(WebClient.Builder builder) { return builder.build(); } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setName("hello there 123"); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDescription("Invoke Sample Job service..."); + jobDetailFactory.setDurability(true); + return jobDetailFactory; + } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + return trigger; + } + + // @Bean + // public CronTriggerFactoryBean trigger(JobDetail job) { + // CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + // trigger.setJobDetail(job); + // trigger.setCronExpression("0 /5 * ? * *"); + // return trigger; + // } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java new file mode 100644 index 00000000000..d0f0973c864 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.jakarta.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.stereotype.Component; + +@Component +public class SampleJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("running job"); + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 37c9f988fe5..6a386aca64f 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -25,3 +25,5 @@ spring.datasource.username=sa spring.datasource.password= spring.graphql.graphiql.enabled=true spring.graphql.websocket.path=/graphql +spring.quartz.job-store-type=memory + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties new file mode 100644 index 00000000000..6e302ce765a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties @@ -0,0 +1 @@ +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index e5506d42294..e43de9d2b05 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(Config.Libs.springBootStarterWebsocket) implementation(Config.Libs.springBootStarterWebflux) implementation(Config.Libs.springBootStarterGraphql) + implementation(Config.Libs.springBootStarterQuartz) implementation(Config.Libs.springBootStarterAop) implementation(Config.Libs.aspectj) implementation(Config.Libs.springBootStarter) @@ -32,6 +33,7 @@ dependencies { implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) implementation(projects.sentryGraphql) + implementation(projects.sentryQuartz) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java index 18afe2838b5..ff02e3d01c2 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java @@ -1,10 +1,15 @@ package io.sentry.samples.spring.boot; +import io.sentry.samples.spring.boot.quartz.SampleJob; +import org.quartz.JobDetail; +import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -24,4 +29,31 @@ RestTemplate restTemplate(RestTemplateBuilder builder) { WebClient webClient(WebClient.Builder builder) { return builder.build(); } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setName("hello_spring_boot_2"); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDescription("Invoke Sample Job service..."); + jobDetailFactory.setDurability(true); + return jobDetailFactory; + } + + // @Bean + // public CronTriggerFactoryBean trigger(JobDetail job) { + // CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + // trigger.setJobDetail(job); + // trigger.setCronExpression("0 * * ? * *"); + // return trigger; + // } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + return trigger; + } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java new file mode 100644 index 00000000000..d238e026811 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.stereotype.Component; + +@Component +public class SampleJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("running job"); + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 0b2292d06ec..e216a11452d 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -30,12 +30,14 @@ dependencies { compileOnly(Config.Libs.springBoot3Starter) compileOnly(platform(SpringBootPlugin.BOM_COORDINATES)) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) compileOnly(Config.Libs.springWeb) compileOnly(Config.Libs.springWebflux) compileOnly(Config.Libs.servletApiJakarta) compileOnly(Config.Libs.springBoot3StarterAop) compileOnly(Config.Libs.springBoot3StarterSecurity) compileOnly(Config.Libs.springBoot3StarterGraphql) + compileOnly(Config.Libs.springBoot3StarterQuartz) compileOnly(Config.Libs.reactorCore) compileOnly(Config.Libs.contextPropagation) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index d3b071c1e8a..4a489adb04d 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -13,6 +13,7 @@ import io.sentry.graphql.SentryGraphqlExceptionHandler; import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; import io.sentry.protocol.SdkVersion; +import io.sentry.quartz.SentryJobListener; import io.sentry.spring.jakarta.ContextTagsEventProcessor; import io.sentry.spring.jakarta.SentryExceptionResolver; import io.sentry.spring.jakarta.SentryRequestResolver; @@ -21,6 +22,7 @@ import io.sentry.spring.jakarta.SentryUserProvider; import io.sentry.spring.jakarta.SentryWebConfiguration; import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; +import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration; import io.sentry.spring.jakarta.graphql.SentryGraphqlConfiguration; import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration; @@ -35,6 +37,7 @@ import java.util.Optional; import org.aspectj.lang.ProceedingJoinPoint; import org.jetbrains.annotations.NotNull; +import org.quartz.core.QuartzScheduler; import org.slf4j.MDC; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -56,6 +59,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -167,6 +171,16 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { }) static class GraphqlConfiguration {} + @Configuration(proxyBeanMethods = false) + @Import(SentryQuartzConfiguration.class) + @Open + @ConditionalOnClass({ + SentryJobListener.class, + QuartzScheduler.class, + SchedulerFactoryBean.class + }) + static class QuartzConfiguration {} + /** Registers beans specific to Spring MVC. */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 3d144730a59..20b1d4a9427 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -35,9 +35,11 @@ dependencies { compileOnly(Config.Libs.springBootStarterAop) compileOnly(Config.Libs.springBootStarterSecurity) compileOnly(Config.Libs.springBootStarterGraphql) + compileOnly(Config.Libs.springBootStarterQuartz) compileOnly(Config.Libs.reactorCore) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES)) annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure) diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 0b51b22d605..aa01874b18d 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -13,6 +13,7 @@ import io.sentry.graphql.SentryGraphqlExceptionHandler; import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; import io.sentry.protocol.SdkVersion; +import io.sentry.quartz.SentryJobListener; import io.sentry.spring.ContextTagsEventProcessor; import io.sentry.spring.SentryExceptionResolver; import io.sentry.spring.SentryRequestResolver; @@ -21,6 +22,7 @@ import io.sentry.spring.SentryUserProvider; import io.sentry.spring.SentryWebConfiguration; import io.sentry.spring.SpringSecuritySentryUserProvider; +import io.sentry.spring.checkin.SentryQuartzConfiguration; import io.sentry.spring.graphql.SentryGraphqlConfiguration; import io.sentry.spring.tracing.SentryAdviceConfiguration; import io.sentry.spring.tracing.SentrySpanPointcutConfiguration; @@ -35,6 +37,7 @@ import javax.servlet.http.HttpServletRequest; import org.aspectj.lang.ProceedingJoinPoint; import org.jetbrains.annotations.NotNull; +import org.quartz.core.QuartzScheduler; import org.slf4j.MDC; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -56,6 +59,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -167,6 +171,16 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { }) static class GraphqlConfiguration {} + @Configuration(proxyBeanMethods = false) + @Import(SentryQuartzConfiguration.class) + @Open + @ConditionalOnClass({ + SentryJobListener.class, + QuartzScheduler.class, + SchedulerFactoryBean.class + }) + static class QuartzConfiguration {} + /** Registers beans specific to Spring MVC. */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 8f35b6075f2..0a229caaf32 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -88,6 +88,16 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i public fun provideUser ()Lio/sentry/protocol/User; } +public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { + public fun ()V + public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; +} + +public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer { + public fun ()V + public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V +} + public final class io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry : org/springframework/graphql/execution/BatchLoaderRegistry { public fun forName (Ljava/lang/String;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; public fun forTypePair (Ljava/lang/Class;Ljava/lang/Class;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 6f16d29a086..be3c00583e3 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { compileOnly(Config.Libs.springAop) compileOnly(Config.Libs.springSecurityWeb) compileOnly(Config.Libs.springBoot3StarterGraphql) + compileOnly(Config.Libs.springBoot3StarterQuartz) compileOnly(Config.Libs.aspectj) compileOnly(Config.Libs.servletApiJakarta) compileOnly(Config.Libs.slf4jApi) @@ -43,6 +44,7 @@ dependencies { errorprone(Config.CompileOnly.errorProneNullAway) compileOnly(Config.CompileOnly.jetbrainsAnnotations) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) // tests testImplementation(projects.sentryTestSupport) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java new file mode 100644 index 00000000000..8819e246e5e --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -0,0 +1,16 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryQuartzConfiguration { + + @Bean + public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { + return new SentrySchedulerFactoryBeanCustomizer(); + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java new file mode 100644 index 00000000000..70dafd24e24 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -0,0 +1,12 @@ +package io.sentry.spring.jakarta.checkin; + +import io.sentry.quartz.SentryJobListener; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setGlobalJobListeners(new SentryJobListener()); + } +} diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 7efeb59f569..05a59c477f9 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -88,6 +88,16 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry public fun provideUser ()Lio/sentry/protocol/User; } +public class io/sentry/spring/checkin/SentryQuartzConfiguration { + public fun ()V + public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; +} + +public final class io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer { + public fun ()V + public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V +} + public final class io/sentry/spring/graphql/SentryBatchLoaderRegistry : org/springframework/graphql/execution/BatchLoaderRegistry { public fun forName (Ljava/lang/String;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; public fun forTypePair (Ljava/lang/Class;Ljava/lang/Class;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index 75a33d0767b..ac444c25ca3 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { compileOnly(Config.Libs.springWebflux) compileOnly(Config.Libs.springBootStarterGraphql) compileOnly(projects.sentryGraphql) + compileOnly(Config.Libs.springBootStarterQuartz) + compileOnly(projects.sentryQuartz) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java new file mode 100644 index 00000000000..842011cb1f1 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java @@ -0,0 +1,16 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryQuartzConfiguration { + + @Bean + public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { + return new SentrySchedulerFactoryBeanCustomizer(); + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java new file mode 100644 index 00000000000..f266d478f1b --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -0,0 +1,12 @@ +package io.sentry.spring.checkin; + +import io.sentry.quartz.SentryJobListener; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setGlobalJobListeners(new SentryJobListener()); + } +} diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index 7e6822971f3..4a72d8b5d09 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -9,7 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Adds additional information about what happened to an event. */ +/** A check-in for a monitor (CRON). */ public final class CheckIn implements JsonUnknown, JsonSerializable { private final @NotNull SentryId checkInId; @@ -29,10 +29,10 @@ public CheckIn(final @NotNull String monitorSlug, final @NotNull CheckInStatus s } public CheckIn( - final @NotNull SentryId checkInId, + final @NotNull SentryId id, final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { - this(checkInId, monitorSlug, status.apiName()); + this(id, monitorSlug, status.apiName()); } @ApiStatus.Internal diff --git a/settings.gradle.kts b/settings.gradle.kts index 45b562da809..f87b9e01263 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-core", "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", "sentry-opentelemetry:sentry-opentelemetry-agent", + "sentry-quartz", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul",