From 41cf8428d758588d1158698f07be705b97021d0a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 25 Sep 2023 14:25:31 +0200 Subject: [PATCH 1/8] Support check-ins for Quartz (#2940) --- .craft.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + CHANGELOG.md | 1 + README.md | 1 + buildSrc/src/main/java/Config.kt | 5 + sentry-quartz/api/sentry-quartz.api | 15 ++ sentry-quartz/build.gradle.kts | 83 +++++++ .../io/sentry/quartz/SentryJobListener.java | 206 ++++++++++++++++++ .../build.gradle.kts | 2 + .../spring/boot/jakarta/CustomJob.java | 1 - .../boot/jakarta/SentryDemoApplication.java | 32 +++ .../spring/boot/jakarta/quartz/SampleJob.java | 19 ++ .../src/main/resources/application.properties | 2 + .../src/main/resources/quartz.properties | 1 + .../build.gradle.kts | 2 + .../spring/boot/SentryDemoApplication.java | 32 +++ .../samples/spring/boot/quartz/SampleJob.java | 19 ++ sentry-spring-boot-jakarta/build.gradle.kts | 2 + .../boot/jakarta/SentryAutoConfiguration.java | 14 ++ sentry-spring-boot/build.gradle.kts | 2 + .../spring/boot/SentryAutoConfiguration.java | 14 ++ .../api/sentry-spring-jakarta.api | 10 + sentry-spring-jakarta/build.gradle.kts | 2 + .../checkin/SentryQuartzConfiguration.java | 16 ++ .../SentrySchedulerFactoryBeanCustomizer.java | 12 + sentry-spring/api/sentry-spring.api | 10 + sentry-spring/build.gradle.kts | 2 + .../checkin/SentryQuartzConfiguration.java | 16 ++ .../SentrySchedulerFactoryBeanCustomizer.java | 12 + sentry/src/main/java/io/sentry/CheckIn.java | 6 +- settings.gradle.kts | 1 + 31 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 sentry-quartz/api/sentry-quartz.api create mode 100644 sentry-quartz/build.gradle.kts create mode 100644 sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties create mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java 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", From 7da0892315b477f25e3b6c80dd2ca2f013b3ddc4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 25 Sep 2023 14:32:51 +0200 Subject: [PATCH 2/8] Add option for ignoring certain monitor slugs (#2943) --- CHANGELOG.md | 3 +- .../src/main/resources/application.properties | 2 + .../src/main/resources/application.properties | 2 + sentry-spring-boot-jakarta/build.gradle.kts | 2 + .../jakarta/SentryAutoConfigurationTest.kt | 61 ++++++++++++++++++- sentry-spring-boot/build.gradle.kts | 2 + .../boot/SentryAutoConfigurationTest.kt | 61 ++++++++++++++++++- sentry/api/sentry.api | 13 ++++ .../main/java/io/sentry/ExternalOptions.java | 24 ++++++++ .../src/main/java/io/sentry/SentryClient.java | 15 +++++ .../main/java/io/sentry/SentryOptions.java | 51 ++++++++++++++++ .../java/io/sentry/util/CheckInUtils.java | 34 +++++++++++ .../java/io/sentry/ExternalOptionsTest.kt | 14 +++++ .../test/java/io/sentry/SentryClientTest.kt | 38 ++++++++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 5 ++ .../java/io/sentry/util/CheckInUtilsTest.kt | 38 ++++++++++++ 16 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/util/CheckInUtils.java create mode 100644 sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb41cc5c7e..08c8000f8ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - 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)) +- Support check-ins (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) +- Add option for ignoring certain monitor slugs ([#2943](https://github.com/getsentry/sentry-java/pull/2943)) ### Fixes 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 6a386aca64f..ba90224f37c 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 @@ -10,6 +10,8 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true +sentry.enable-automatic-checkins=true +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index 8151eacc6cf..ef8e5484b94 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -10,6 +10,8 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true +sentry.enable-automatic-checkins=true +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index e216a11452d..473a8b5de3e 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { // tests testImplementation(projects.sentryLogback) + testImplementation(projects.sentryQuartz) testImplementation(projects.sentryApacheHttpClient5) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) @@ -69,6 +70,7 @@ dependencies { testImplementation(Config.Libs.springBoot3StarterWebflux) testImplementation(Config.Libs.springBoot3StarterSecurity) testImplementation(Config.Libs.springBoot3StarterAop) + testImplementation(Config.Libs.springBoot3StarterQuartz) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) testImplementation(Config.Libs.contextPropagation) } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 1c4af4f91de..015b22cfac3 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -18,6 +18,7 @@ import io.sentry.checkEvent import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.quartz.SentryJobListener import io.sentry.spring.jakarta.ContextTagsEventProcessor import io.sentry.spring.jakarta.HttpServletRequestSentryUserProvider import io.sentry.spring.jakarta.SentryExceptionResolver @@ -36,9 +37,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.quartz.core.QuartzScheduler import org.slf4j.MDC import org.springframework.aop.support.NameMatchMethodPointcut import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.info.GitProperties @@ -51,6 +54,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.Ordered import org.springframework.core.annotation.Order +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 @@ -157,7 +161,9 @@ class SentryAutoConfigurationTest { "sentry.ignored-exceptions-for-type=java.lang.RuntimeException,java.lang.IllegalStateException,io.sentry.Sentry", "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", - "sentry.send-modules=false" + "sentry.send-modules=false", + "sentry.enable-automatic-checkins=true", + "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -188,6 +194,8 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) + assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) + assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -724,6 +732,57 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when auto checkins is enabled, creates quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .run { + assertThat(it).hasSingleBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(QuartzScheduler::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if spring-quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(SchedulerFactoryBean::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if sentry-quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is disabled, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins option is skipped, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomOptionsConfigurationConfiguration { diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 20b1d4a9427..548b02face5 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { // tests testImplementation(projects.sentryLogback) + testImplementation(projects.sentryQuartz) testImplementation(projects.sentryApacheHttpClient5) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) @@ -66,6 +67,7 @@ dependencies { testImplementation(Config.Libs.springBootStarterWebflux) testImplementation(Config.Libs.springBootStarterSecurity) testImplementation(Config.Libs.springBootStarterAop) + testImplementation(Config.Libs.springBootStarterQuartz) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index b6c6ec380e4..0336b2f6d25 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -18,6 +18,7 @@ import io.sentry.checkEvent import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.quartz.SentryJobListener import io.sentry.spring.ContextTagsEventProcessor import io.sentry.spring.HttpServletRequestSentryUserProvider import io.sentry.spring.SentryExceptionResolver @@ -35,9 +36,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.quartz.core.QuartzScheduler import org.slf4j.MDC import org.springframework.aop.support.NameMatchMethodPointcut import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.info.GitProperties @@ -50,6 +53,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.Ordered import org.springframework.core.annotation.Order +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 @@ -157,7 +161,9 @@ class SentryAutoConfigurationTest { "sentry.ignored-exceptions-for-type=java.lang.RuntimeException,java.lang.IllegalStateException,io.sentry.Sentry", "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", - "sentry.send-modules=false" + "sentry.send-modules=false", + "sentry.enable-automatic-checkins=true", + "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -188,6 +194,8 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) + assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) + assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -724,6 +732,57 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when auto checkins is enabled, creates quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .run { + assertThat(it).hasSingleBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(QuartzScheduler::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if spring-quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(SchedulerFactoryBean::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is enabled, does not create quartz config if sentry-quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins is disabled, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins option is skipped, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomOptionsConfigurationConfiguration { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8f44d415e13..40efd78815b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -320,6 +320,7 @@ public final class io/sentry/ExternalOptions { public fun getEnableUncaughtExceptionHandler ()Ljava/lang/Boolean; public fun getEnvironment ()Ljava/lang/String; public fun getIdleTimeout ()Ljava/lang/Long; + public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; @@ -335,12 +336,14 @@ public final class io/sentry/ExternalOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracingOrigins ()Ljava/util/List; + public fun isEnableAutomaticCheckIns ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V + public fun setEnableAutomaticCheckIns (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableTracing (Ljava/lang/Boolean;)V @@ -348,6 +351,7 @@ public final class io/sentry/ExternalOptions { public fun setEnabled (Ljava/lang/Boolean;)V public fun setEnvironment (Ljava/lang/String;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V @@ -1929,6 +1933,7 @@ public class io/sentry/SentryOptions { public fun getGestureTargetLocators ()Ljava/util/List; public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIdleTimeout ()Ljava/lang/Long; + public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; @@ -1979,6 +1984,7 @@ public class io/sentry/SentryOptions { public fun isAttachThreads ()Z public fun isDebug ()Z public fun isEnableAutoSessionTracking ()Z + public fun isEnableAutomaticCheckIns ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z public fun isEnableNdk ()Z @@ -2015,6 +2021,7 @@ public class io/sentry/SentryOptions { public fun setDistinctId (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V public fun setEnableAutoSessionTracking (Z)V + public fun setEnableAutomaticCheckIns (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V public fun setEnableNdk (Z)V @@ -2035,6 +2042,7 @@ public class io/sentry/SentryOptions { public fun setGestureTargetLocators (Ljava/util/List;)V public fun setHostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setLogger (Lio/sentry/ILogger;)V public fun setMainThreadChecker (Lio/sentry/util/thread/IMainThreadChecker;)V @@ -4310,6 +4318,11 @@ public abstract class io/sentry/transport/TransportResult { public static fun success ()Lio/sentry/transport/TransportResult; } +public final class io/sentry/util/CheckInUtils { + public fun ()V + public static fun isIgnored (Ljava/util/List;Ljava/lang/String;)Z +} + public final class io/sentry/util/ClassLoaderUtils { public fun ()V public static fun classLoaderOrDefault (Ljava/lang/ClassLoader;)Ljava/lang/ClassLoader; diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 0f2e241f621..e139cd2cc4a 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -45,6 +45,9 @@ public final class ExternalOptions { private @Nullable Boolean enabled; private @Nullable Boolean enablePrettySerializationOutput; + private @Nullable Boolean enableAutomaticCheckIns; + private @Nullable List ignoredCheckIns; + private @Nullable Boolean sendModules; @SuppressWarnings("unchecked") @@ -126,6 +129,11 @@ public final class ExternalOptions { options.setSendModules(propertiesProvider.getBooleanProperty("send-modules")); + options.setEnableAutomaticCheckIns( + propertiesProvider.getBooleanProperty("enable-automatic-checkins")); + + options.setIgnoredCheckIns(propertiesProvider.getList("ignored-checkins")); + for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { try { @@ -383,4 +391,20 @@ public void setEnablePrettySerializationOutput( public void setSendModules(final @Nullable Boolean sendModules) { this.sendModules = sendModules; } + + public @Nullable Boolean isEnableAutomaticCheckIns() { + return enableAutomaticCheckIns; + } + + public void setEnableAutomaticCheckIns(final @Nullable Boolean enableAutomaticCheckIns) { + this.enableAutomaticCheckIns = enableAutomaticCheckIns; + } + + public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { + this.ignoredCheckIns = ignoredCheckIns; + } + + public @Nullable List getIgnoredCheckIns() { + return ignoredCheckIns; + } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 8640a3985c3..19b0bb66276 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -9,6 +9,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; +import io.sentry.util.CheckInUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; @@ -689,6 +690,20 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint checkIn = applyScope(checkIn, scope); } + if (CheckInUtils.isIgnored(options.getIgnoredCheckIns(), checkIn.getMonitorSlug())) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Check-in was dropped as slug %s is ignored", + checkIn.getMonitorSlug()); + // TODO in a follow up PR with DataCategory.Monitor + // options + // .getClientReportRecorder() + // .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); + return SentryId.EMPTY_ID; + } + options.getLogger().log(SentryLevel.DEBUG, "Capturing check-in: %s", checkIn.getCheckInId()); SentryId sentryId = checkIn.getCheckInId(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 4305c79e1e3..7265b2feda7 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -443,6 +443,12 @@ public class SentryOptions { /** Whether to send modules containing information about versions. */ private boolean sendModules = true; + /** Whether to automatically send check-ins for monitors (CRONS). */ + private boolean enableAutomaticCheckIns = false; + + /** Contains a list of monitor slugs for which check-ins should not be sent. */ + private @Nullable List ignoredCheckIns = null; + /** * Adds an event processor * @@ -2141,6 +2147,15 @@ public boolean isSendModules() { return sendModules; } + /** + * Whether to send check-ins for monitors (CRONS) automatically. + * + * @return true if check-ins should be sent automatically. + */ + public boolean isEnableAutomaticCheckIns() { + return enableAutomaticCheckIns; + } + /** * Whether to format serialized data, e.g. events logged to console in debug mode * @@ -2159,6 +2174,34 @@ public void setSendModules(boolean sendModules) { this.sendModules = sendModules; } + /** + * Whether to send check-ins for monitors (CRONS) automatically. + * + * @param enableAutomaticCheckIns true if check-ins should be sent automatically. + */ + public void setEnableAutomaticCheckIns(boolean enableAutomaticCheckIns) { + this.enableAutomaticCheckIns = enableAutomaticCheckIns; + } + + public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { + if (ignoredCheckIns == null) { + this.ignoredCheckIns = null; + } else { + @NotNull final List filteredIgnoredCheckIns = new ArrayList<>(); + for (String slug : ignoredCheckIns) { + if (!slug.isEmpty()) { + filteredIgnoredCheckIns.add(slug); + } + } + + this.ignoredCheckIns = filteredIgnoredCheckIns; + } + } + + public @Nullable List getIgnoredCheckIns() { + return ignoredCheckIns; + } + /** Returns the current {@link SentryDateProvider} that is used to retrieve the current date. */ @ApiStatus.Internal public @NotNull SentryDateProvider getDateProvider() { @@ -2407,6 +2450,14 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isSendModules() != null) { setSendModules(options.isSendModules()); } + + if (options.isEnableAutomaticCheckIns() != null) { + setEnableAutomaticCheckIns(options.isEnableAutomaticCheckIns()); + } + if (options.getIgnoredCheckIns() != null) { + final List ignoredCheckIns = new ArrayList<>(options.getIgnoredCheckIns()); + setIgnoredCheckIns(ignoredCheckIns); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/main/java/io/sentry/util/CheckInUtils.java b/sentry/src/main/java/io/sentry/util/CheckInUtils.java new file mode 100644 index 00000000000..41ba19499e5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/CheckInUtils.java @@ -0,0 +1,34 @@ +package io.sentry.util; + +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Checks if a check-in for a monitor (CRON) has been ignored. */ +@ApiStatus.Internal +public final class CheckInUtils { + + public static boolean isIgnored( + final @Nullable List ignoredSlugs, final @NotNull String slug) { + if (ignoredSlugs == null || ignoredSlugs.isEmpty()) { + return false; + } + + for (final String ignoredSlug : ignoredSlugs) { + if (ignoredSlug.equalsIgnoreCase(slug)) { + return true; + } + + try { + if (slug.matches(ignoredSlug)) { + return true; + } + } catch (Throwable t) { + // ignore invalid regex + } + } + + return false; + } +} diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 9d57b694c34..fc95bed9e7c 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -261,6 +261,20 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with enableAutomaticCheckIns set to true`() { + withPropertiesFile("enable-automatic-checkins=true") { options -> + assertTrue(options.isEnableAutomaticCheckIns == true) + } + } + + @Test + fun `creates options with ignoredCheckIns`() { + withPropertiesFile("ignored-checkins=slugA,slug2") { options -> + assertTrue(options.ignoredCheckIns!!.containsAll(listOf("slugA", "slug2"))) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6f259743c16..ccc749b76a3 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -557,6 +557,44 @@ class SentryClientTest { ) } + @Test + fun `when captureCheckIn, envelope is sent if ignored slug does not match`() { + val sut = fixture.getSut { options -> + options.ignoredCheckIns = listOf("non_matching_slug") + } + + sut.captureCheckIn(checkIn, null, null) + + verify(fixture.transport).send( + check { actual -> + assertEquals(checkIn.checkInId, actual.header.eventId) + assertEquals(fixture.sentryOptions.sdkVersion, actual.header.sdkVersion) + + assertEquals(1, actual.items.count()) + val item = actual.items.first() + assertEquals(SentryItemType.CheckIn, item.header.type) + assertEquals("application/json", item.header.contentType) + + assertEnvelopeItemDataForCheckIn(item) + }, + any() + ) + } + + @Test + fun `when captureCheckIn, envelope is not sent if slug is ignored`() { + val sut = fixture.getSut { options -> + options.ignoredCheckIns = listOf("some_slug") + } + + sut.captureCheckIn(checkIn, null, null) + + verify(fixture.transport, never()).send( + any(), + any() + ) + } + private fun assertEnvelopeItemDataForCheckIn(item: SentryEnvelopeItem) { val stream = ByteArrayOutputStream() val writer = stream.bufferedWriter(Charset.forName("UTF-8")) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 32278d531d3..2b13764916f 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -371,6 +371,9 @@ class SentryOptionsTest { externalOptions.isEnabled = false externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false + externalOptions.isEnableAutomaticCheckIns = true + externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") + val options = SentryOptions() options.merge(externalOptions) @@ -398,6 +401,8 @@ class SentryOptionsTest { assertFalse(options.isEnabled) assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) + assertTrue(options.isEnableAutomaticCheckIns) + assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) } @Test diff --git a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt new file mode 100644 index 00000000000..2475504fe36 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt @@ -0,0 +1,38 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CheckInUtilsTest { + + @Test + fun `ignores exact match`() { + assertTrue(CheckInUtils.isIgnored(listOf("slugA"), "slugA")) + } + + @Test + fun `ignores regex match`() { + assertTrue(CheckInUtils.isIgnored(listOf("slug-.*"), "slug-A")) + } + + @Test + fun `does not ignore if ignored list is null`() { + assertFalse(CheckInUtils.isIgnored(null, "slugA")) + } + + @Test + fun `does not ignore if ignored list is empty`() { + assertFalse(CheckInUtils.isIgnored(emptyList(), "slugA")) + } + + @Test + fun `does not ignore if slug is not in ignored list`() { + assertFalse(CheckInUtils.isIgnored(listOf("slugB"), "slugA")) + } + + @Test + fun `does not ignore if slug is does not match ignored list`() { + assertFalse(CheckInUtils.isIgnored(listOf("slug-.*"), "slugA")) + } +} From 7ef79c24c9adb9c4006da974fdb5b9b04e78a41f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 25 Sep 2023 14:41:13 +0200 Subject: [PATCH 3/8] Mark CRONS feature experimental (#2944) --- .../src/main/java/io/sentry/quartz/SentryJobListener.java | 2 ++ .../spring/jakarta/checkin/SentryQuartzConfiguration.java | 2 ++ .../checkin/SentrySchedulerFactoryBeanCustomizer.java | 2 ++ .../sentry/spring/checkin/SentryQuartzConfiguration.java | 2 ++ .../checkin/SentrySchedulerFactoryBeanCustomizer.java | 2 ++ sentry/src/main/java/io/sentry/CheckIn.java | 1 + sentry/src/main/java/io/sentry/CheckInStatus.java | 2 ++ sentry/src/main/java/io/sentry/ExternalOptions.java | 5 +++++ sentry/src/main/java/io/sentry/Hub.java | 1 + sentry/src/main/java/io/sentry/HubAdapter.java | 1 + sentry/src/main/java/io/sentry/IHub.java | 1 + sentry/src/main/java/io/sentry/ISentryClient.java | 1 + sentry/src/main/java/io/sentry/MonitorConfig.java | 2 ++ sentry/src/main/java/io/sentry/MonitorContexts.java | 2 ++ sentry/src/main/java/io/sentry/MonitorSchedule.java | 1 + sentry/src/main/java/io/sentry/MonitorScheduleType.java | 2 ++ sentry/src/main/java/io/sentry/MonitorScheduleUnit.java | 2 ++ sentry/src/main/java/io/sentry/NoOpHub.java | 2 ++ sentry/src/main/java/io/sentry/NoOpSentryClient.java | 2 ++ sentry/src/main/java/io/sentry/Sentry.java | 1 + sentry/src/main/java/io/sentry/SentryClient.java | 1 + sentry/src/main/java/io/sentry/SentryOptions.java | 8 ++++++-- 22 files changed, 43 insertions(+), 2 deletions(-) diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 68d5afceaae..96f5014988f 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -12,6 +12,7 @@ import io.sentry.protocol.SentryId; import java.util.List; import java.util.TimeZone; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.quartz.CalendarIntervalTrigger; @@ -26,6 +27,7 @@ import org.quartz.SimpleTrigger; import org.quartz.Trigger; +@ApiStatus.Experimental public final class SentryJobListener implements JobListener { public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; 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 index 8819e246e5e..eb396c9692b 100644 --- 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 @@ -1,12 +1,14 @@ package io.sentry.spring.jakarta.checkin; import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @Open +@ApiStatus.Experimental public class SentryQuartzConfiguration { @Bean 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 index 70dafd24e24..e37f5ac4674 100644 --- 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 @@ -1,9 +1,11 @@ package io.sentry.spring.jakarta.checkin; import io.sentry.quartz.SentryJobListener; +import org.jetbrains.annotations.ApiStatus; import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; import org.springframework.scheduling.quartz.SchedulerFactoryBean; +@ApiStatus.Experimental public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { @Override public void customize(SchedulerFactoryBean schedulerFactoryBean) { 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 index 842011cb1f1..640507dd7c2 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java @@ -1,12 +1,14 @@ package io.sentry.spring.checkin; import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @Open +@ApiStatus.Experimental public class SentryQuartzConfiguration { @Bean 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 index f266d478f1b..d0f68c6712e 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -1,9 +1,11 @@ package io.sentry.spring.checkin; import io.sentry.quartz.SentryJobListener; +import org.jetbrains.annotations.ApiStatus; import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; import org.springframework.scheduling.quartz.SchedulerFactoryBean; +@ApiStatus.Experimental public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { @Override public void customize(SchedulerFactoryBean schedulerFactoryBean) { diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index 4a72d8b5d09..e8ad8d57c64 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental /** A check-in for a monitor (CRON). */ public final class CheckIn implements JsonUnknown, JsonSerializable { diff --git a/sentry/src/main/java/io/sentry/CheckInStatus.java b/sentry/src/main/java/io/sentry/CheckInStatus.java index 42ed7bac549..0f0b47b4a4d 100644 --- a/sentry/src/main/java/io/sentry/CheckInStatus.java +++ b/sentry/src/main/java/io/sentry/CheckInStatus.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Status of a CheckIn */ +@ApiStatus.Experimental public enum CheckInStatus { IN_PROGRESS, OK, diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index e139cd2cc4a..604a001da99 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -392,18 +393,22 @@ public void setSendModules(final @Nullable Boolean sendModules) { this.sendModules = sendModules; } + @ApiStatus.Experimental public @Nullable Boolean isEnableAutomaticCheckIns() { return enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setEnableAutomaticCheckIns(final @Nullable Boolean enableAutomaticCheckIns) { this.enableAutomaticCheckIns = enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { this.ignoredCheckIns = ignoredCheckIns; } + @ApiStatus.Experimental public @Nullable List getIgnoredCheckIns() { return ignoredCheckIns; } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 58103900692..0ce59cc05c2 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -864,6 +864,7 @@ private Scope buildLocalScope( } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { SentryId sentryId = SentryId.EMPTY_ID; if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 05f348cb157..88445b5232c 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -253,6 +253,7 @@ public void reportFullyDisplayed() { } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return Sentry.captureCheckIn(checkIn); } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 71bbb0f7310..8ff69727b06 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -627,6 +627,7 @@ TransactionContext continueTrace( @Nullable BaggageHeader getBaggage(); + @ApiStatus.Experimental @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn); } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 28ce8111d0a..6eb8d7d1d05 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -266,5 +266,6 @@ SentryId captureTransaction( } @NotNull + @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint); } diff --git a/sentry/src/main/java/io/sentry/MonitorConfig.java b/sentry/src/main/java/io/sentry/MonitorConfig.java index b6ad81ee8f8..c25884c2bde 100644 --- a/sentry/src/main/java/io/sentry/MonitorConfig.java +++ b/sentry/src/main/java/io/sentry/MonitorConfig.java @@ -4,9 +4,11 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorConfig implements JsonUnknown, JsonSerializable { private @NotNull MonitorSchedule schedule; diff --git a/sentry/src/main/java/io/sentry/MonitorContexts.java b/sentry/src/main/java/io/sentry/MonitorContexts.java index 461a4d549aa..3a15aa4113c 100644 --- a/sentry/src/main/java/io/sentry/MonitorContexts.java +++ b/sentry/src/main/java/io/sentry/MonitorContexts.java @@ -6,9 +6,11 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorContexts extends ConcurrentHashMap implements JsonSerializable { private static final long serialVersionUID = 3987329379811822556L; diff --git a/sentry/src/main/java/io/sentry/MonitorSchedule.java b/sentry/src/main/java/io/sentry/MonitorSchedule.java index 904e84aeac1..23740d45ba2 100644 --- a/sentry/src/main/java/io/sentry/MonitorSchedule.java +++ b/sentry/src/main/java/io/sentry/MonitorSchedule.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorSchedule implements JsonUnknown, JsonSerializable { public static @NotNull MonitorSchedule crontab(final @NotNull String value) { diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleType.java b/sentry/src/main/java/io/sentry/MonitorScheduleType.java index ac168e38455..a1ec20c97ed 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleType.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleType.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Type of a monitor schedule */ +@ApiStatus.Experimental public enum MonitorScheduleType { CRONTAB, INTERVAL; diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java index adfb0771fe7..994d9c4be17 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Time unit of a monitor schedule. */ +@ApiStatus.Experimental public enum MonitorScheduleUnit { MINUTE, HOUR, diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 4d40efe976b..ec6b45e3891 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -4,6 +4,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import java.util.List; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -209,6 +210,7 @@ public void reportFullyDisplayed() {} } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return SentryId.EMPTY_ID; } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index adeb63e3563..c0a71c27fc4 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -2,6 +2,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,6 +55,7 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( @NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint) { return SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 822822340f2..71b588d55b9 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1012,6 +1012,7 @@ public interface OptionsConfiguration { return getCurrentHub().getBaggage(); } + @ApiStatus.Experimental public static @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return getCurrentHub().captureCheckIn(checkIn); } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 19b0bb66276..5ff5e3f35f6 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -672,6 +672,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( @NotNull CheckIn checkIn, final @Nullable Scope scope, @Nullable Hint hint) { if (hint == null) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7265b2feda7..d14b3150a2d 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -444,10 +444,10 @@ public class SentryOptions { private boolean sendModules = true; /** Whether to automatically send check-ins for monitors (CRONS). */ - private boolean enableAutomaticCheckIns = false; + @ApiStatus.Experimental private boolean enableAutomaticCheckIns = false; /** Contains a list of monitor slugs for which check-ins should not be sent. */ - private @Nullable List ignoredCheckIns = null; + @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; /** * Adds an event processor @@ -2152,6 +2152,7 @@ public boolean isSendModules() { * * @return true if check-ins should be sent automatically. */ + @ApiStatus.Experimental public boolean isEnableAutomaticCheckIns() { return enableAutomaticCheckIns; } @@ -2179,10 +2180,12 @@ public void setSendModules(boolean sendModules) { * * @param enableAutomaticCheckIns true if check-ins should be sent automatically. */ + @ApiStatus.Experimental public void setEnableAutomaticCheckIns(boolean enableAutomaticCheckIns) { this.enableAutomaticCheckIns = enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { if (ignoredCheckIns == null) { this.ignoredCheckIns = null; @@ -2198,6 +2201,7 @@ public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { } } + @ApiStatus.Experimental public @Nullable List getIgnoredCheckIns() { return ignoredCheckIns; } From c65470b7403786f097b3f611cfb3a5b576ffe614 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 25 Sep 2023 14:54:26 +0200 Subject: [PATCH 4/8] No longer send schedule for Quartz check-ins, add a way to configure the monitor slug per quartz trigger (#2948) --- sentry-quartz/api/sentry-quartz.api | 3 +- .../io/sentry/quartz/SentryJobListener.java | 166 ++++-------------- .../boot/jakarta/SentryDemoApplication.java | 26 +-- .../src/main/resources/application.properties | 1 - .../spring/boot/SentryDemoApplication.java | 28 +-- .../src/main/resources/application.properties | 1 - .../jakarta/SentryAutoConfigurationTest.kt | 26 +-- .../boot/SentryAutoConfigurationTest.kt | 34 +--- sentry/api/sentry.api | 4 - .../main/java/io/sentry/ExternalOptions.java | 14 -- .../main/java/io/sentry/SentryOptions.java | 27 --- .../java/io/sentry/ExternalOptionsTest.kt | 7 - .../test/java/io/sentry/SentryOptionsTest.kt | 2 - 13 files changed, 77 insertions(+), 262 deletions(-) diff --git a/sentry-quartz/api/sentry-quartz.api b/sentry-quartz/api/sentry-quartz.api index 34a71295845..ff32280dc5b 100644 --- a/sentry-quartz/api/sentry-quartz.api +++ b/sentry-quartz/api/sentry-quartz.api @@ -5,8 +5,9 @@ public final class io/sentry/quartz/BuildConfig { 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 static final field SENTRY_SLUG_KEY Ljava/lang/String; public fun ()V + public fun (Lio/sentry/IHub;)V public fun getName ()Ljava/lang/String; public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 96f5014988f..84e499fca2e 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -3,37 +3,34 @@ 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.HubAdapter; +import io.sentry.IHub; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; -import java.util.List; -import java.util.TimeZone; +import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; 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.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; -import org.quartz.JobKey; import org.quartz.JobListener; -import org.quartz.SimpleTrigger; -import org.quartz.Trigger; @ApiStatus.Experimental 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 static final String SENTRY_SLUG_KEY = "sentry-slug"; + + private final @NotNull IHub hub; public SentryJobListener() { + this(HubAdapter.getInstance()); + } + + public SentryJobListener(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz"); SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME); @@ -45,133 +42,31 @@ public String getName() { } @Override - public void jobToBeExecuted(JobExecutionContext context) { + public void jobToBeExecuted(final @NotNull 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 @Nullable String maybeSlug = getSlug(context); + if (maybeSlug == null) { + return; } - - final @NotNull SentryId checkInId = Sentry.captureCheckIn(checkIn); + final @NotNull String slug = maybeSlug; + final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); + final @NotNull SentryId checkInId = hub.captureCheckIn(checkIn); context.put(SENTRY_CHECK_IN_ID_KEY, checkInId); - context.put(SENTRY_CHECK_IN_SLUG_KEY, slug); + context.put(SENTRY_SLUG_KEY, slug); } catch (Throwable t) { - Sentry.getCurrentHub() - .getOptions() + hub.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()); + private @Nullable String getSlug(final @NotNull JobExecutionContext context) { + final @Nullable JobDataMap jobDataMap = context.getMergedJobDataMap(); + if (jobDataMap != null) { + final @Nullable Object o = jobDataMap.get(SENTRY_SLUG_KEY); + if (o != null) { + return o.toString(); } - 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; @@ -186,7 +81,7 @@ public void jobExecutionVetoed(JobExecutionContext context) { 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 @Nullable Object slugObjectFromContext = context.get(SENTRY_SLUG_KEY); final @NotNull SentryId checkInId = checkInIdObjectFromContext == null ? new SentryId() @@ -196,11 +91,10 @@ public void jobWasExecuted(JobExecutionContext context, JobExecutionException jo if (slug != null) { final boolean isFailed = jobException != null; final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK; - Sentry.captureCheckIn(new CheckIn(checkInId, slug, status)); + hub.captureCheckIn(new CheckIn(checkInId, slug, status)); } } catch (Throwable t) { - Sentry.getCurrentHub() - .getOptions() + hub.getOptions() .getLogger() .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", 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 d9e4a7cdb5e..74b1ac3f71b 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,6 +1,9 @@ package io.sentry.samples.spring.boot.jakarta; +import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; + import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import java.util.Collections; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; @@ -8,6 +11,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; import org.springframework.scheduling.quartz.JobDetailFactoryBean; import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import org.springframework.web.client.RestTemplate; @@ -33,10 +37,10 @@ WebClient webClient(WebClient.Builder builder) { @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); + jobDetailFactory.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail")); return jobDetailFactory; } @@ -44,16 +48,18 @@ public JobDetailFactoryBean jobDetail() { public SimpleTriggerFactoryBean trigger(JobDetail job) { SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); trigger.setJobDetail(job); - trigger.setRepeatInterval(2 * 60 * 1000); + trigger.setRepeatInterval(2 * 60 * 1000); // every two minutes trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + trigger.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger")); return trigger; } - // @Bean - // public CronTriggerFactoryBean trigger(JobDetail job) { - // CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); - // trigger.setJobDetail(job); - // trigger.setCronExpression("0 /5 * ? * *"); - // return trigger; - // } + @Bean + public CronTriggerFactoryBean cronTrigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes + return trigger; + } } 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 ba90224f37c..d06f7e0878b 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 @@ -10,7 +10,6 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true -sentry.enable-automatic-checkins=true sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" 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 ff02e3d01c2..b4f46260997 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,6 +1,9 @@ package io.sentry.samples.spring.boot; +import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; + import io.sentry.samples.spring.boot.quartz.SampleJob; +import java.util.Collections; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; @@ -8,6 +11,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; import org.springframework.scheduling.quartz.JobDetailFactoryBean; import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import org.springframework.web.client.RestTemplate; @@ -33,27 +37,29 @@ WebClient webClient(WebClient.Builder builder) { @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); + jobDetailFactory.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail")); 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.setRepeatInterval(2 * 60 * 1000); // every two minutes trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + trigger.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger")); + return trigger; + } + + @Bean + public CronTriggerFactoryBean cronTrigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes return trigger; } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index ef8e5484b94..31989d2dfea 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -10,7 +10,6 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true -sentry.enable-automatic-checkins=true sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 015b22cfac3..d4c90e0b058 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -162,7 +162,6 @@ class SentryAutoConfigurationTest { "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", "sentry.send-modules=false", - "sentry.enable-automatic-checkins=true", "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) @@ -194,7 +193,6 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) - assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -733,7 +731,7 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, creates quartz config`() { + fun `creates quartz config`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") .run { assertThat(it).hasSingleBean(SchedulerFactoryBeanCustomizer::class.java) @@ -741,7 +739,7 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, does not create quartz config if quartz lib missing`() { + fun `does not create quartz config if quartz lib missing`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") .withClassLoader(FilteredClassLoader(QuartzScheduler::class.java)) .run { @@ -750,7 +748,7 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, does not create quartz config if spring-quartz lib missing`() { + fun `does not create quartz config if spring-quartz lib missing`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") .withClassLoader(FilteredClassLoader(SchedulerFactoryBean::class.java)) .run { @@ -759,7 +757,7 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, does not create quartz config if sentry-quartz lib missing`() { + fun `does not create quartz config if sentry-quartz lib missing`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) .run { @@ -767,22 +765,6 @@ class SentryAutoConfigurationTest { } } - @Test - fun `when auto checkins is disabled, does not create quartz config`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") - .run { - assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) - } - } - - @Test - fun `when auto checkins option is skipped, does not create quartz config`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") - .run { - assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) - } - } - @Configuration(proxyBeanMethods = false) open class CustomOptionsConfigurationConfiguration { diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 0336b2f6d25..44f280e6cbe 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -162,7 +162,6 @@ class SentryAutoConfigurationTest { "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", "sentry.send-modules=false", - "sentry.enable-automatic-checkins=true", "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) @@ -194,7 +193,6 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) - assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -733,16 +731,16 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, creates quartz config`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + fun `creates quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") .run { assertThat(it).hasSingleBean(SchedulerFactoryBeanCustomizer::class.java) } } @Test - fun `when auto checkins is enabled, does not create quartz config if quartz lib missing`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + fun `does not create quartz config if quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") .withClassLoader(FilteredClassLoader(QuartzScheduler::class.java)) .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) @@ -750,8 +748,8 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, does not create quartz config if spring-quartz lib missing`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") + fun `does not create quartz config if spring-quartz lib missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") .withClassLoader(FilteredClassLoader(SchedulerFactoryBean::class.java)) .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) @@ -759,25 +757,9 @@ class SentryAutoConfigurationTest { } @Test - fun `when auto checkins is enabled, does not create quartz config if sentry-quartz lib missing`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=true") - .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) - .run { - assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) - } - } - - @Test - fun `when auto checkins is disabled, does not create quartz config`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") - .run { - assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) - } - } - - @Test - fun `when auto checkins option is skipped, does not create quartz config`() { + fun `does not create quartz config if sentry-quartz lib missing`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 40efd78815b..b629b7993eb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -336,14 +336,12 @@ public final class io/sentry/ExternalOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracingOrigins ()Ljava/util/List; - public fun isEnableAutomaticCheckIns ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V - public fun setEnableAutomaticCheckIns (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableTracing (Ljava/lang/Boolean;)V @@ -1984,7 +1982,6 @@ public class io/sentry/SentryOptions { public fun isAttachThreads ()Z public fun isDebug ()Z public fun isEnableAutoSessionTracking ()Z - public fun isEnableAutomaticCheckIns ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z public fun isEnableNdk ()Z @@ -2021,7 +2018,6 @@ public class io/sentry/SentryOptions { public fun setDistinctId (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V public fun setEnableAutoSessionTracking (Z)V - public fun setEnableAutomaticCheckIns (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V public fun setEnableNdk (Z)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 604a001da99..adb25811e0e 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -46,7 +46,6 @@ public final class ExternalOptions { private @Nullable Boolean enabled; private @Nullable Boolean enablePrettySerializationOutput; - private @Nullable Boolean enableAutomaticCheckIns; private @Nullable List ignoredCheckIns; private @Nullable Boolean sendModules; @@ -130,9 +129,6 @@ public final class ExternalOptions { options.setSendModules(propertiesProvider.getBooleanProperty("send-modules")); - options.setEnableAutomaticCheckIns( - propertiesProvider.getBooleanProperty("enable-automatic-checkins")); - options.setIgnoredCheckIns(propertiesProvider.getList("ignored-checkins")); for (final String ignoredExceptionType : @@ -393,16 +389,6 @@ public void setSendModules(final @Nullable Boolean sendModules) { this.sendModules = sendModules; } - @ApiStatus.Experimental - public @Nullable Boolean isEnableAutomaticCheckIns() { - return enableAutomaticCheckIns; - } - - @ApiStatus.Experimental - public void setEnableAutomaticCheckIns(final @Nullable Boolean enableAutomaticCheckIns) { - this.enableAutomaticCheckIns = enableAutomaticCheckIns; - } - @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { this.ignoredCheckIns = ignoredCheckIns; diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d14b3150a2d..7b33347cbcb 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -443,9 +443,6 @@ public class SentryOptions { /** Whether to send modules containing information about versions. */ private boolean sendModules = true; - /** Whether to automatically send check-ins for monitors (CRONS). */ - @ApiStatus.Experimental private boolean enableAutomaticCheckIns = false; - /** Contains a list of monitor slugs for which check-ins should not be sent. */ @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; @@ -2147,16 +2144,6 @@ public boolean isSendModules() { return sendModules; } - /** - * Whether to send check-ins for monitors (CRONS) automatically. - * - * @return true if check-ins should be sent automatically. - */ - @ApiStatus.Experimental - public boolean isEnableAutomaticCheckIns() { - return enableAutomaticCheckIns; - } - /** * Whether to format serialized data, e.g. events logged to console in debug mode * @@ -2175,16 +2162,6 @@ public void setSendModules(boolean sendModules) { this.sendModules = sendModules; } - /** - * Whether to send check-ins for monitors (CRONS) automatically. - * - * @param enableAutomaticCheckIns true if check-ins should be sent automatically. - */ - @ApiStatus.Experimental - public void setEnableAutomaticCheckIns(boolean enableAutomaticCheckIns) { - this.enableAutomaticCheckIns = enableAutomaticCheckIns; - } - @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { if (ignoredCheckIns == null) { @@ -2454,10 +2431,6 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isSendModules() != null) { setSendModules(options.isSendModules()); } - - if (options.isEnableAutomaticCheckIns() != null) { - setEnableAutomaticCheckIns(options.isEnableAutomaticCheckIns()); - } if (options.getIgnoredCheckIns() != null) { final List ignoredCheckIns = new ArrayList<>(options.getIgnoredCheckIns()); setIgnoredCheckIns(ignoredCheckIns); diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index fc95bed9e7c..929db8ad06c 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -261,13 +261,6 @@ class ExternalOptionsTest { } } - @Test - fun `creates options with enableAutomaticCheckIns set to true`() { - withPropertiesFile("enable-automatic-checkins=true") { options -> - assertTrue(options.isEnableAutomaticCheckIns == true) - } - } - @Test fun `creates options with ignoredCheckIns`() { withPropertiesFile("ignored-checkins=slugA,slug2") { options -> diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 2b13764916f..ce211c9414a 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -371,7 +371,6 @@ class SentryOptionsTest { externalOptions.isEnabled = false externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false - externalOptions.isEnableAutomaticCheckIns = true externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") val options = SentryOptions() @@ -401,7 +400,6 @@ class SentryOptionsTest { assertFalse(options.isEnabled) assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) - assertTrue(options.isEnableAutomaticCheckIns) assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) } From d639a334eaf13c935e83e068ac839b57aa7e791a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 08:55:45 +0200 Subject: [PATCH 5/8] SentryCheckIn annotation and advice config for Spring (#2946) Co-authored-by: Lukas Bloder --- CHANGELOG.md | 1 + .../spring/boot/jakarta/CustomJob.java | 26 +-- .../sentry/samples/spring/boot/CustomJob.java | 4 +- .../boot/jakarta/SentryAutoConfiguration.java | 18 ++ .../spring/boot/SentryAutoConfiguration.java | 18 ++ .../api/sentry-spring-jakarta.api | 22 +++ .../spring/jakarta/checkin/SentryCheckIn.java | 41 +++++ .../jakarta/checkin/SentryCheckInAdvice.java | 78 ++++++++ .../SentryCheckInAdviceConfiguration.java | 31 ++++ .../SentryCheckInPointcutConfiguration.java | 29 +++ .../spring/jakarta/SentryCheckInAdviceTest.kt | 173 ++++++++++++++++++ sentry-spring/api/sentry-spring.api | 22 +++ .../sentry/spring/checkin/SentryCheckIn.java | 41 +++++ .../spring/checkin/SentryCheckInAdvice.java | 78 ++++++++ .../SentryCheckInAdviceConfiguration.java | 31 ++++ .../SentryCheckInPointcutConfiguration.java | 29 +++ .../sentry/spring/SentryCheckInAdviceTest.kt | 173 ++++++++++++++++++ sentry/src/main/java/io/sentry/CheckIn.java | 8 +- 18 files changed, 796 insertions(+), 27 deletions(-) create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java create mode 100644 sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c8000f8ca..667662b8291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Add API for sending checkins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) - Support check-ins (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) - Add option for ignoring certain monitor slugs ([#2943](https://github.com/getsentry/sentry-java/pull/2943)) +- `@SentryCheckIn` annotation and advice config for Spring ([#2946](https://github.com/getsentry/sentry-java/pull/2946)) ### Fixes 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 994203b4140..cac83e6d797 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 @@ -1,12 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; -import io.sentry.CheckIn; -import io.sentry.CheckInStatus; -import io.sentry.DateUtils; -import io.sentry.Sentry; -import io.sentry.protocol.SentryId; +import io.sentry.spring.jakarta.checkin.SentryCheckIn; import io.sentry.spring.jakarta.tracing.SentryTransaction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -22,23 +17,10 @@ public class CustomJob { private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); + @SentryCheckIn("monitor_slug_1") @Scheduled(fixedRate = 3 * 60 * 1000L) void execute() throws InterruptedException { - final @NotNull SentryId checkInId = - Sentry.captureCheckIn(new CheckIn("my_monitor_slug", CheckInStatus.IN_PROGRESS)); - final long startTime = System.currentTimeMillis(); - boolean didError = false; - try { - LOGGER.info("Executing scheduled job"); - Thread.sleep(2000L); - } catch (Throwable t) { - didError = true; - throw t; - } finally { - final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; - CheckIn checkIn = new CheckIn(checkInId, "my_monitor_slug", status); - checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); - Sentry.captureCheckIn(checkIn); - } + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java index cdebaeac1bd..67a726ed424 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java @@ -1,5 +1,6 @@ package io.sentry.samples.spring.boot; +import io.sentry.spring.checkin.SentryCheckIn; import io.sentry.spring.tracing.SentryTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +17,8 @@ public class CustomJob { private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); - @Scheduled(fixedRate = 3 * 1000L) + @Scheduled(fixedRate = 3 * 60 * 1000L) + @SentryCheckIn("monitor_slug_2") void execute() throws InterruptedException { LOGGER.info("Executing scheduled job"); Thread.sleep(2000L); 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 4a489adb04d..3ebcfc8bb9f 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 @@ -22,6 +22,8 @@ import io.sentry.spring.jakarta.SentryUserProvider; import io.sentry.spring.jakarta.SentryWebConfiguration; import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; +import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration; +import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration; import io.sentry.spring.jakarta.graphql.SentryGraphqlConfiguration; import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; @@ -181,6 +183,22 @@ static class GraphqlConfiguration {} }) static class QuartzConfiguration {} + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ProceedingJoinPoint.class) + @ConditionalOnProperty( + value = "sentry.enable-aot-compatibility", + havingValue = "false", + matchIfMissing = true) + @Import(SentryCheckInAdviceConfiguration.class) + @Open + static class SentryCheckInAspectsConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(name = "sentryCheckInPointcut") + @Import(SentryCheckInPointcutConfiguration.class) + @Open + static class SentryCheckInPointcutAutoConfiguration {} + } + /** Registers beans specific to Spring MVC. */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 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 aa01874b18d..131be4a1ecb 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 @@ -22,6 +22,8 @@ import io.sentry.spring.SentryUserProvider; import io.sentry.spring.SentryWebConfiguration; import io.sentry.spring.SpringSecuritySentryUserProvider; +import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration; +import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring.checkin.SentryQuartzConfiguration; import io.sentry.spring.graphql.SentryGraphqlConfiguration; import io.sentry.spring.tracing.SentryAdviceConfiguration; @@ -181,6 +183,22 @@ static class GraphqlConfiguration {} }) static class QuartzConfiguration {} + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ProceedingJoinPoint.class) + @ConditionalOnProperty( + value = "sentry.enable-aot-compatibility", + havingValue = "false", + matchIfMissing = true) + @Import(SentryCheckInAdviceConfiguration.class) + @Open + static class SentryCheckInAspectsConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(name = "sentryCheckInPointcut") + @Import(SentryCheckInPointcutConfiguration.class) + @Open + static class SentryCheckInPointcutAutoConfiguration {} + } + /** 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 0a229caaf32..d30cda5c602 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -88,6 +88,28 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i public fun provideUser ()Lio/sentry/protocol/User; } +public abstract interface annotation class io/sentry/spring/jakarta/checkin/SentryCheckIn : java/lang/annotation/Annotation { + public abstract fun heartbeat ()Z + public abstract fun monitorSlug ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor { + public fun (Lio/sentry/IHub;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration { + public fun ()V + public fun sentryCheckInAdvice (Lio/sentry/IHub;)Lorg/aopalliance/aop/Advice; + public fun sentryCheckInAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration { + public fun ()V + public fun sentryCheckInPointcut ()Lorg/springframework/aop/Pointcut; +} + public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { public fun ()V public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java new file mode 100644 index 00000000000..a2f53da955c --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java @@ -0,0 +1,41 @@ +package io.sentry.spring.jakarta.checkin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.jetbrains.annotations.ApiStatus; +import org.springframework.core.annotation.AliasFor; + +/** Sends a {@link io.sentry.CheckIn} for the annotated method. */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@ApiStatus.Experimental +public @interface SentryCheckIn { + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("value") + String monitorSlug() default ""; + + /** + * Whether to send only send heartbeat events. + * + *

A hearbeat check-in means there's no separate IN_PROGRESS check-in at the start of the jobs + * execution. Only the check-in after finishing the job will be sent. + * + * @return true if only heartbeat check-ins should be sent. + */ + boolean heartbeat() default false; + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("monitorSlug") + String value() default ""; +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java new file mode 100644 index 00000000000..c7805bf6071 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -0,0 +1,78 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.DateUtils; +import io.sentry.IHub; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import io.sentry.util.Objects; +import java.lang.reflect.Method; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; + +/** + * Reports execution of every bean method annotated with {@link SentryCheckIn} as a monitor + * check-in. + */ +@ApiStatus.Internal +@ApiStatus.Experimental +@Open +public class SentryCheckInAdvice implements MethodInterceptor { + private final @NotNull IHub hub; + + public SentryCheckInAdvice(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); + } + + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryCheckIn checkInAnnotation = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class); + if (checkInAnnotation == null) { + return invocation.proceed(); + } + + final boolean isHeartbeatOnly = checkInAnnotation.heartbeat(); + final @Nullable String monitorSlug = checkInAnnotation.value(); + + if (ObjectUtils.isEmpty(monitorSlug)) { + hub.getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Not capturing check-in for method annotated with @SentryCheckIn because it does not specify a monitor slug."); + return invocation.proceed(); + } + + @Nullable SentryId checkInId = null; + final long startTime = System.currentTimeMillis(); + boolean didError = false; + + try { + if (!isHeartbeatOnly) { + checkInId = hub.captureCheckIn(new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS)); + } + return invocation.proceed(); + } catch (Throwable e) { + didError = true; + throw e; + } finally { + final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; + CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); + checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); + hub.captureCheckIn(checkIn); + } + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java new file mode 100644 index 00000000000..505c08f77af --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IHub; +import org.aopalliance.aop.Advice; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.aop.Advisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInAdviceConfiguration { + + @Bean + public @NotNull Advice sentryCheckInAdvice(final @NotNull IHub hub) { + return new SentryCheckInAdvice(hub); + } + + @Bean + public @NotNull Advisor sentryCheckInAdvisor( + final @NotNull @Qualifier("sentryCheckInPointcut") Pointcut sentryCheckInPointcut, + final @NotNull @Qualifier("sentryCheckInAdvice") Advice sentryCheckInAdvice) { + return new DefaultPointcutAdvisor(sentryCheckInPointcut, sentryCheckInAdvice); + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java new file mode 100644 index 00000000000..8371d2e684d --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java @@ -0,0 +1,29 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationClassFilter; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** AOP pointcut configuration for {@link SentryCheckIn}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInPointcutConfiguration { + + /** + * Pointcut around which check-ins are created. + * + * @return pointcut used by {@link SentryCheckInAdvice}. + */ + @Bean + public @NotNull Pointcut sentryCheckInPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryCheckIn.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCheckIn.class)); + } +} diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt new file mode 100644 index 00000000000..06f1da81a13 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt @@ -0,0 +1,173 @@ +package io.sentry.spring.jakarta + +import io.sentry.CheckIn +import io.sentry.CheckInStatus +import io.sentry.IHub +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId +import io.sentry.spring.jakarta.checkin.SentryCheckIn +import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration +import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner +import kotlin.RuntimeException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCheckInAdviceTest.Config::class) +class SentryCheckInAdviceTest { + + @Autowired + lateinit var sampleService: SampleService + + @Autowired + lateinit var sampleServiceNoSlug: SampleServiceNoSlug + + @Autowired + lateinit var sampleServiceHeartbeat: SampleServiceHeartbeat + + @Autowired + lateinit var hub: IHub + + @BeforeTest + fun setup() { + whenever(hub.options).thenReturn(SentryOptions()) + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleService.hello() + assertEquals(1, result) + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { + sampleService.oops() + } + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1e", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceHeartbeat.hello() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end with error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { + sampleServiceHeartbeat.oops() + } + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + } + + @Test + fun `when method is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceNoSlug.hello() + assertEquals(1, result) + assertEquals(0, checkInCaptor.allValues.size) + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryCheckInAdviceConfiguration::class, SentryCheckInPointcutConfiguration::class) + open class Config { + + @Bean + open fun sampleService() = SampleService() + + @Bean + open fun sampleServiceNoSlug() = SampleServiceNoSlug() + + @Bean + open fun sampleServiceHeartbeat() = SampleServiceHeartbeat() + + @Bean + open fun hub() = mock() + } + + open class SampleService { + + @SentryCheckIn("monitor_slug_1") + open fun hello() = 1 + + @SentryCheckIn("monitor_slug_1e") + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } + + open class SampleServiceNoSlug { + + @SentryCheckIn + open fun hello() = 1 + } + + open class SampleServiceHeartbeat { + + @SentryCheckIn(monitorSlug = "monitor_slug_2", heartbeat = true) + open fun hello() = 1 + + @SentryCheckIn(monitorSlug = "monitor_slug_2e", heartbeat = true) + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } +} diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 05a59c477f9..ac4111e2083 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -88,6 +88,28 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry public fun provideUser ()Lio/sentry/protocol/User; } +public abstract interface annotation class io/sentry/spring/checkin/SentryCheckIn : java/lang/annotation/Annotation { + public abstract fun heartbeat ()Z + public abstract fun monitorSlug ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public class io/sentry/spring/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor { + public fun (Lio/sentry/IHub;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/checkin/SentryCheckInAdviceConfiguration { + public fun ()V + public fun sentryCheckInAdvice (Lio/sentry/IHub;)Lorg/aopalliance/aop/Advice; + public fun sentryCheckInAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; +} + +public class io/sentry/spring/checkin/SentryCheckInPointcutConfiguration { + public fun ()V + public fun sentryCheckInPointcut ()Lorg/springframework/aop/Pointcut; +} + public class io/sentry/spring/checkin/SentryQuartzConfiguration { public fun ()V public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java new file mode 100644 index 00000000000..4662bc531c7 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java @@ -0,0 +1,41 @@ +package io.sentry.spring.checkin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.jetbrains.annotations.ApiStatus; +import org.springframework.core.annotation.AliasFor; + +/** Sends a {@link io.sentry.CheckIn} for the annotated method. */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@ApiStatus.Experimental +public @interface SentryCheckIn { + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("value") + String monitorSlug() default ""; + + /** + * Whether to send only send heartbeat events. + * + *

A hearbeat check-in means there's no separate IN_PROGRESS check-in at the start of the jobs + * execution. Only the check-in after finishing the job will be sent. + * + * @return true if only heartbeat check-ins should be sent. + */ + boolean heartbeat() default false; + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("monitorSlug") + String value() default ""; +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java new file mode 100644 index 00000000000..1f66ebae284 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java @@ -0,0 +1,78 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.DateUtils; +import io.sentry.IHub; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import io.sentry.util.Objects; +import java.lang.reflect.Method; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; + +/** + * Reports execution of every bean method annotated with {@link SentryCheckIn} as a monitor + * check-in. + */ +@ApiStatus.Internal +@ApiStatus.Experimental +@Open +public class SentryCheckInAdvice implements MethodInterceptor { + private final @NotNull IHub hub; + + public SentryCheckInAdvice(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); + } + + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryCheckIn checkInAnnotation = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class); + if (checkInAnnotation == null) { + return invocation.proceed(); + } + + final boolean isHeartbeatOnly = checkInAnnotation.heartbeat(); + final @Nullable String monitorSlug = checkInAnnotation.value(); + + if (ObjectUtils.isEmpty(monitorSlug)) { + hub.getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Not capturing check-in for method annotated with @SentryCheckIn because it does not specify a monitor slug."); + return invocation.proceed(); + } + + @Nullable SentryId checkInId = null; + final long startTime = System.currentTimeMillis(); + boolean didError = false; + + try { + if (!isHeartbeatOnly) { + checkInId = hub.captureCheckIn(new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS)); + } + return invocation.proceed(); + } catch (Throwable e) { + didError = true; + throw e; + } finally { + final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; + CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); + checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); + hub.captureCheckIn(checkIn); + } + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java new file mode 100644 index 00000000000..ec6433cee6f --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IHub; +import org.aopalliance.aop.Advice; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.aop.Advisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInAdviceConfiguration { + + @Bean + public @NotNull Advice sentryCheckInAdvice(final @NotNull IHub hub) { + return new SentryCheckInAdvice(hub); + } + + @Bean + public @NotNull Advisor sentryCheckInAdvisor( + final @NotNull @Qualifier("sentryCheckInPointcut") Pointcut sentryCheckInPointcut, + final @NotNull @Qualifier("sentryCheckInAdvice") Advice sentryCheckInAdvice) { + return new DefaultPointcutAdvisor(sentryCheckInPointcut, sentryCheckInAdvice); + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java new file mode 100644 index 00000000000..4d18ec48672 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java @@ -0,0 +1,29 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationClassFilter; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** AOP pointcut configuration for {@link SentryCheckIn}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInPointcutConfiguration { + + /** + * Pointcut around which check-ins are created. + * + * @return pointcut used by {@link SentryCheckInAdvice}. + */ + @Bean + public @NotNull Pointcut sentryCheckInPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryCheckIn.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCheckIn.class)); + } +} diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt new file mode 100644 index 00000000000..d71ed0d61bb --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt @@ -0,0 +1,173 @@ +package io.sentry.spring + +import io.sentry.CheckIn +import io.sentry.CheckInStatus +import io.sentry.IHub +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId +import io.sentry.spring.checkin.SentryCheckIn +import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration +import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner +import kotlin.RuntimeException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCheckInAdviceTest.Config::class) +class SentryCheckInAdviceTest { + + @Autowired + lateinit var sampleService: SampleService + + @Autowired + lateinit var sampleServiceNoSlug: SampleServiceNoSlug + + @Autowired + lateinit var sampleServiceHeartbeat: SampleServiceHeartbeat + + @Autowired + lateinit var hub: IHub + + @BeforeTest + fun setup() { + whenever(hub.options).thenReturn(SentryOptions()) + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleService.hello() + assertEquals(1, result) + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { + sampleService.oops() + } + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1e", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceHeartbeat.hello() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end with error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { + sampleServiceHeartbeat.oops() + } + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + } + + @Test + fun `when method is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceNoSlug.hello() + assertEquals(1, result) + assertEquals(0, checkInCaptor.allValues.size) + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryCheckInAdviceConfiguration::class, SentryCheckInPointcutConfiguration::class) + open class Config { + + @Bean + open fun sampleService() = SampleService() + + @Bean + open fun sampleServiceNoSlug() = SampleServiceNoSlug() + + @Bean + open fun sampleServiceHeartbeat() = SampleServiceHeartbeat() + + @Bean + open fun hub() = mock() + } + + open class SampleService { + + @SentryCheckIn("monitor_slug_1") + open fun hello() = 1 + + @SentryCheckIn("monitor_slug_1e") + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } + + open class SampleServiceNoSlug { + + @SentryCheckIn + open fun hello() = 1 + } + + open class SampleServiceHeartbeat { + + @SentryCheckIn(monitorSlug = "monitor_slug_2", heartbeat = true) + open fun hello() = 1 + + @SentryCheckIn(monitorSlug = "monitor_slug_2e", heartbeat = true) + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } +} diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index e8ad8d57c64..4c83771324f 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -26,11 +26,11 @@ public final class CheckIn implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; public CheckIn(final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { - this(new SentryId(), monitorSlug, status.apiName()); + this(null, monitorSlug, status.apiName()); } public CheckIn( - final @NotNull SentryId id, + final @Nullable SentryId id, final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { this(id, monitorSlug, status.apiName()); @@ -38,10 +38,10 @@ public CheckIn( @ApiStatus.Internal public CheckIn( - final @NotNull SentryId checkInId, + final @Nullable SentryId checkInId, final @NotNull String monitorSlug, final @NotNull String status) { - this.checkInId = checkInId; + this.checkInId = checkInId == null ? new SentryId() : checkInId; this.monitorSlug = monitorSlug; this.status = status; } From 92285dd7d0ebf652ebe66c1859a733380b94eef1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 08:58:14 +0200 Subject: [PATCH 6/8] group crons changelog entries --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667662b8291..3697b31dda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ - 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 (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) -- Add option for ignoring certain monitor slugs ([#2943](https://github.com/getsentry/sentry-java/pull/2943)) -- `@SentryCheckIn` annotation and advice config for Spring ([#2946](https://github.com/getsentry/sentry-java/pull/2946)) +- Check-ins (CRONS) support + - Add API for sending check-ins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) + - Support check-ins (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) + - `@SentryCheckIn` annotation and advice config for Spring ([#2946](https://github.com/getsentry/sentry-java/pull/2946)) + - Add option for ignoring certain monitor slugs ([#2943](https://github.com/getsentry/sentry-java/pull/2943)) ### Fixes From 6babb27997725d61486c3457c762d1b573449690 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 09:37:13 +0200 Subject: [PATCH 7/8] link PR in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3697b31dda4..0be89ba894f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,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)) -- Check-ins (CRONS) support +- Check-ins (CRONS) support ([#2952](https://github.com/getsentry/sentry-java/pull/2952)) - Add API for sending check-ins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) - Support check-ins (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) - `@SentryCheckIn` annotation and advice config for Spring ([#2946](https://github.com/getsentry/sentry-java/pull/2946)) From ac4897a491c4c229ff58349c4bdc62e4a656e00b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 12:24:17 +0200 Subject: [PATCH 8/8] CRONS pushScope, new PropagationContext, popScope (#2956) --- .../io/sentry/quartz/SentryJobListener.java | 5 +++ .../jakarta/checkin/SentryCheckInAdvice.java | 5 +++ .../spring/jakarta/SentryCheckInAdviceTest.kt | 31 +++++++++++++++++++ .../spring/checkin/SentryCheckInAdvice.java | 5 +++ .../sentry/spring/SentryCheckInAdviceTest.kt | 31 +++++++++++++++++++ 5 files changed, 77 insertions(+) diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 84e499fca2e..28a0e512005 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -9,6 +9,7 @@ import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; import io.sentry.util.Objects; +import io.sentry.util.TracingUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -48,6 +49,8 @@ public void jobToBeExecuted(final @NotNull JobExecutionContext context) { if (maybeSlug == null) { return; } + hub.pushScope(); + TracingUtils.startNewTrace(hub); final @NotNull String slug = maybeSlug; final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); final @NotNull SentryId checkInId = hub.captureCheckIn(checkIn); @@ -97,6 +100,8 @@ public void jobWasExecuted(JobExecutionContext context, JobExecutionException jo hub.getOptions() .getLogger() .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t); + } finally { + hub.popScope(); } } } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java index c7805bf6071..fca5200575c 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -8,6 +8,7 @@ import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; import io.sentry.util.Objects; +import io.sentry.util.TracingUtils; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -56,6 +57,9 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl return invocation.proceed(); } + hub.pushScope(); + TracingUtils.startNewTrace(hub); + @Nullable SentryId checkInId = null; final long startTime = System.currentTimeMillis(); boolean didError = false; @@ -73,6 +77,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); hub.captureCheckIn(checkIn); + hub.popScope(); } } } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt index 06f1da81a13..e15254398a7 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt @@ -10,8 +10,14 @@ import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration import org.junit.jupiter.api.assertThrows import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean @@ -44,6 +50,7 @@ class SentryCheckInAdviceTest { @BeforeTest fun setup() { + reset(hub) whenever(hub.options).thenReturn(SentryOptions()) } @@ -63,6 +70,11 @@ class SentryCheckInAdviceTest { val doneCheckIn = checkInCaptor.lastValue assertEquals("monitor_slug_1", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub, times(2)).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -82,6 +94,11 @@ class SentryCheckInAdviceTest { val doneCheckIn = checkInCaptor.lastValue assertEquals("monitor_slug_1e", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub, times(2)).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -97,6 +114,11 @@ class SentryCheckInAdviceTest { assertEquals("monitor_slug_2", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) assertNotNull(doneCheckIn.duration) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -113,6 +135,11 @@ class SentryCheckInAdviceTest { assertEquals("monitor_slug_2e", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) assertNotNull(doneCheckIn.duration) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -123,6 +150,10 @@ class SentryCheckInAdviceTest { val result = sampleServiceNoSlug.hello() assertEquals(1, result) assertEquals(0, checkInCaptor.allValues.size) + + verify(hub, never()).pushScope() + verify(hub, never()).captureCheckIn(any()) + verify(hub, never()).popScope() } @Configuration diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java index 1f66ebae284..e452cc35003 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java @@ -8,6 +8,7 @@ import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; import io.sentry.util.Objects; +import io.sentry.util.TracingUtils; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -56,6 +57,9 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl return invocation.proceed(); } + hub.pushScope(); + TracingUtils.startNewTrace(hub); + @Nullable SentryId checkInId = null; final long startTime = System.currentTimeMillis(); boolean didError = false; @@ -73,6 +77,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); hub.captureCheckIn(checkIn); + hub.popScope(); } } } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt index d71ed0d61bb..a31453b0ed1 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt @@ -10,8 +10,14 @@ import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration import org.junit.jupiter.api.assertThrows import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean @@ -44,6 +50,7 @@ class SentryCheckInAdviceTest { @BeforeTest fun setup() { + reset(hub) whenever(hub.options).thenReturn(SentryOptions()) } @@ -63,6 +70,11 @@ class SentryCheckInAdviceTest { val doneCheckIn = checkInCaptor.lastValue assertEquals("monitor_slug_1", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub, times(2)).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -82,6 +94,11 @@ class SentryCheckInAdviceTest { val doneCheckIn = checkInCaptor.lastValue assertEquals("monitor_slug_1e", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub, times(2)).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -97,6 +114,11 @@ class SentryCheckInAdviceTest { assertEquals("monitor_slug_2", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) assertNotNull(doneCheckIn.duration) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -113,6 +135,11 @@ class SentryCheckInAdviceTest { assertEquals("monitor_slug_2e", doneCheckIn.monitorSlug) assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) assertNotNull(doneCheckIn.duration) + + val order = inOrder(hub) + order.verify(hub).pushScope() + order.verify(hub).captureCheckIn(any()) + order.verify(hub).popScope() } @Test @@ -123,6 +150,10 @@ class SentryCheckInAdviceTest { val result = sampleServiceNoSlug.hello() assertEquals(1, result) assertEquals(0, checkInCaptor.allValues.size) + + verify(hub, never()).pushScope() + verify(hub, never()).captureCheckIn(any()) + verify(hub, never()).popScope() } @Configuration