-
-
Notifications
You must be signed in to change notification settings - Fork 461
Support check-ins for Quartz #2940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b2debe
1de0ba7
3dd4fce
41cd16d
97b8e63
083e021
4828fa3
ef54e76
cc23d3c
6038dc5
44a3c75
b558570
0c29f12
24215e5
68931af
a709a40
66cb754
f4fb1dd
600a58c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <init> ()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 | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<JavaPluginExtension> { | ||
| sourceCompatibility = JavaVersion.VERSION_1_8 | ||
| targetCompatibility = JavaVersion.VERSION_1_8 | ||
| } | ||
|
|
||
| tasks.withType<KotlinCompile>().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<SourceSetContainer> { | ||
| 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<JavaCompile>().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}\"") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<? extends Job> 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<? extends Trigger> triggersOfJob = | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we use Small example (add to SentryDemoApplication in jakarta):
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the moment there's no support for multiple triggers in Sentry. I opted for a more consistent approach by getting the list of triggers so we don't update the monitor config on every invocation.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We no longer send the schedule and also get the slug from the job data map. We'll document how to handle multiple triggers. |
||
| 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); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.