55import io .sentry .CheckInStatus ;
66import io .sentry .IScopes ;
77import io .sentry .ISentryLifecycleToken ;
8+ import io .sentry .MonitorConfig ;
89import io .sentry .ScopesAdapter ;
910import io .sentry .SentryIntegrationPackageStorage ;
1011import io .sentry .SentryLevel ;
1112import io .sentry .protocol .SentryId ;
1213import io .sentry .util .LifecycleHelper ;
14+ import io .sentry .util .MonitorConfigUtils ;
1315import io .sentry .util .Objects ;
1416import io .sentry .util .TracingUtils ;
17+ import java .util .Arrays ;
18+ import java .util .List ;
19+ import java .util .Locale ;
20+ import java .util .TimeZone ;
21+ import java .util .regex .Pattern ;
1522import org .jetbrains .annotations .ApiStatus ;
1623import org .jetbrains .annotations .NotNull ;
1724import org .jetbrains .annotations .Nullable ;
25+ import org .quartz .CronTrigger ;
1826import org .quartz .JobDataMap ;
1927import org .quartz .JobExecutionContext ;
2028import org .quartz .JobExecutionException ;
2129import org .quartz .JobListener ;
30+ import org .quartz .SimpleTrigger ;
31+ import org .quartz .Trigger ;
2232
2333@ ApiStatus .Experimental
2434public final class SentryJobListener implements JobListener {
@@ -32,6 +42,16 @@ public final class SentryJobListener implements JobListener {
3242 public static final String SENTRY_SLUG_KEY = "sentry-slug" ;
3343 public static final String SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY = "sentry-scope-lifecycle" ;
3444
45+ /** Job data key; set to {@code true} to send a monitor config from the trigger. */
46+ public static final String SENTRY_UPSERT_MONITOR_CONFIG_KEY = "sentry-upsert-monitor-config" ;
47+
48+ private static final @ NotNull List <String > DAY_NAMES =
49+ Arrays .asList ("SUN" , "MON" , "TUE" , "WED" , "THU" , "FRI" , "SAT" );
50+
51+ // Quartz ignores a step after a name, so SUN/2 is every Sunday
52+ private static final @ NotNull Pattern NAME_STEP =
53+ Pattern .compile ("(^|,)[A-Za-z]{3}(-[A-Za-z]{3})?/" );
54+
3555 private final @ NotNull IScopes scopes ;
3656
3757 public SentryJobListener () {
@@ -55,15 +75,20 @@ public void jobToBeExecuted(final @NotNull JobExecutionContext context) {
5575 if (maybeSlug == null ) {
5676 return ;
5777 }
78+ final @ Nullable MonitorConfig monitorConfig =
79+ shouldUpsertMonitorConfig (context )
80+ ? monitorConfigFromTrigger (context .getTrigger ())
81+ : null ;
5882 final @ NotNull ISentryLifecycleToken lifecycleToken =
5983 scopes .forkedScopes ("SentryJobListener" ).makeCurrent ();
84+ context .put (SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY , lifecycleToken );
6085 TracingUtils .startNewTrace (scopes );
6186 final @ NotNull String slug = maybeSlug ;
6287 final @ NotNull CheckIn checkIn = new CheckIn (slug , CheckInStatus .IN_PROGRESS );
88+ checkIn .setMonitorConfig (monitorConfig );
6389 final @ NotNull SentryId checkInId = scopes .captureCheckIn (checkIn );
6490 context .put (SENTRY_CHECK_IN_ID_KEY , checkInId );
6591 context .put (SENTRY_SLUG_KEY , slug );
66- context .put (SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY , lifecycleToken );
6792 } catch (Throwable t ) {
6893 scopes
6994 .getOptions ()
@@ -84,6 +109,137 @@ public void jobToBeExecuted(final @NotNull JobExecutionContext context) {
84109 return null ;
85110 }
86111
112+ private boolean shouldUpsertMonitorConfig (final @ NotNull JobExecutionContext context ) {
113+ final @ Nullable JobDataMap jobDataMap = context .getMergedJobDataMap ();
114+ if (jobDataMap == null ) {
115+ return false ;
116+ }
117+ final @ Nullable Object o = jobDataMap .get (SENTRY_UPSERT_MONITOR_CONFIG_KEY );
118+ return o != null && "true" .equalsIgnoreCase (o .toString ());
119+ }
120+
121+ private @ Nullable MonitorConfig monitorConfigFromTrigger (final @ Nullable Trigger trigger ) {
122+ try {
123+ // Sentry can't express calendar exclusions
124+ if (trigger == null || trigger .getCalendarName () != null ) {
125+ return null ;
126+ }
127+ if (trigger instanceof CronTrigger ) {
128+ final @ NotNull CronTrigger cronTrigger = (CronTrigger ) trigger ;
129+ final @ Nullable String cron = toSixFieldCron (cronTrigger .getCronExpression ());
130+ if (cron == null ) {
131+ return null ;
132+ }
133+ final @ Nullable TimeZone timeZone = cronTrigger .getTimeZone ();
134+ return MonitorConfigUtils .fromSchedule (
135+ cron , timeZone == null ? null : timeZone .getID (), null );
136+ }
137+ if (trigger instanceof SimpleTrigger ) {
138+ final @ NotNull SimpleTrigger simpleTrigger = (SimpleTrigger ) trigger ;
139+ if (simpleTrigger .getRepeatCount () != SimpleTrigger .REPEAT_INDEFINITELY ) {
140+ return null ;
141+ }
142+ return MonitorConfigUtils .fromSchedule (null , null , simpleTrigger .getRepeatInterval ());
143+ }
144+ return null ;
145+ } catch (Throwable e ) {
146+ scopes
147+ .getOptions ()
148+ .getLogger ()
149+ .log (
150+ SentryLevel .WARNING , "Could not derive a monitor config from the Quartz trigger." , e );
151+ return null ;
152+ }
153+ }
154+
155+ /**
156+ * Quartz numbers days of week 1-7 from Sunday; crontab uses 0-6. Null if a year is set or a name
157+ * has a step.
158+ */
159+ static @ Nullable String toSixFieldCron (final @ Nullable String quartzCron ) {
160+ if (quartzCron == null ) {
161+ return null ;
162+ }
163+ final @ NotNull String [] fields = quartzCron .trim ().split ("\\ s+" , -1 );
164+ if (fields .length == 7 && !"*" .equals (fields [6 ])) {
165+ return null ;
166+ }
167+ if (fields .length != 6 && fields .length != 7 ) {
168+ return null ;
169+ }
170+ if (NAME_STEP .matcher (fields [4 ]).find () || NAME_STEP .matcher (fields [5 ]).find ()) {
171+ return null ;
172+ }
173+ final @ Nullable String dayOfWeek = toZeroBasedDayOfWeek (fields [5 ]);
174+ if (dayOfWeek == null ) {
175+ return null ;
176+ }
177+ final @ NotNull StringBuilder cron = new StringBuilder ();
178+ for (int i = 0 ; i < 5 ; i ++) {
179+ cron .append (fields [i ]).append (' ' );
180+ }
181+ return cron .append (dayOfWeek ).toString ();
182+ }
183+
184+ private static @ Nullable String toZeroBasedDayOfWeek (final @ NotNull String field ) {
185+ final @ NotNull StringBuilder result = new StringBuilder ();
186+ for (final @ NotNull String item : field .split ("," , -1 )) {
187+ final @ Nullable String converted = toZeroBasedDayOfWeekItem (item .toUpperCase (Locale .ROOT ));
188+ if (converted == null ) {
189+ return null ;
190+ }
191+ if (result .length () > 0 ) {
192+ result .append (',' );
193+ }
194+ result .append (converted );
195+ }
196+ return result .toString ();
197+ }
198+
199+ private static @ Nullable String toZeroBasedDayOfWeekItem (final @ NotNull String item ) {
200+ if (item .startsWith ("*" ) || item .startsWith ("?" )) {
201+ // '*' starts on Sunday in both
202+ return item ;
203+ }
204+ final int hash = item .indexOf ('#' );
205+ if (hash >= 0 ) {
206+ final @ Nullable Integer day = toZeroBasedDay (item .substring (0 , hash ));
207+ return day == null ? null : day + item .substring (hash );
208+ }
209+ if (item .length () > 1 && item .endsWith ("L" )) {
210+ final @ Nullable Integer day = toZeroBasedDay (item .substring (0 , item .length () - 1 ));
211+ return day == null ? null : day + "L" ;
212+ }
213+ final int slash = item .indexOf ('/' );
214+ final @ NotNull String days = slash >= 0 ? item .substring (0 , slash ) : item ;
215+ final @ NotNull String step = slash >= 0 ? item .substring (slash ) : "" ;
216+ final @ NotNull String [] range = days .split ("-" , -1 );
217+ if (range .length > 2 ) {
218+ return null ;
219+ }
220+ final @ Nullable Integer start = toZeroBasedDay (range [0 ]);
221+ if (start == null ) {
222+ return null ;
223+ }
224+ if (range .length == 2 ) {
225+ final @ Nullable Integer end = toZeroBasedDay (range [1 ]);
226+ return end == null ? null : start + "-" + end + step ;
227+ }
228+ if (slash < 0 || start == 6 ) {
229+ return String .valueOf (start );
230+ }
231+ // a step from a single day ends on Saturday in Quartz, but on Sunday (7) in crontab
232+ return start + "-6" + step ;
233+ }
234+
235+ private static @ Nullable Integer toZeroBasedDay (final @ NotNull String day ) {
236+ if (day .matches ("[1-7]" )) {
237+ return Integer .parseInt (day ) - 1 ;
238+ }
239+ final int index = DAY_NAMES .indexOf (day );
240+ return index >= 0 ? index : null ;
241+ }
242+
87243 @ Override
88244 public void jobExecutionVetoed (JobExecutionContext context ) {
89245 // do nothing
0 commit comments