Skip to content

Commit e29145a

Browse files
wedamijaclaude
andauthored
feat(spring): Add an option to monitor all @scheduled methods in Spring Boot 2 (#6276)
* ref(core): Add MonitorConfigUtils to convert schedules to monitor configs Framework-free conversion of a 6 field cron expression, fixed rate or fixed delay into a MonitorConfig, plus period parsing, so the Spring and Quartz integrations can share it. * fix(core): Only send monitor configs Sentry reads the same way Return no config when day of month and day of week are both set, for cron syntax Sentry rejects, and for zones that are not IANA region IDs. Whole-hour fixed offsets map to Etc/GMT zones. Intervals use hours or days when they divide evenly. * ref(core): Add MonitorConfigUtils.fromSpringScheduled Move the Spring day-of-week rewrite and the JVM default zone rule into core so the three Spring modules share them. * fix(core): Return no config for stepped single value ranges Spring and Quartz run 10-10/2 only at 10, cronsim steps to the field max. * fix(core): Read Spring crons per parser and skip fixed delays Spring before 5.3 steps day of month */n from 0 and can run at midnight when both day fields are set; read its crons that way. Spring 5.3 runs #5 in months without a fifth weekday, so return null for it. A fixed delay drifts from any interval, so it gets no config. * feat(spring): Send monitor config from @scheduled with @SentryCheckIn Co-Authored-By: Claude <noreply@anthropic.com> * feat(spring): Send @scheduled monitor config by default Co-Authored-By: Claude <noreply@anthropic.com> * ref(spring): Shorten upsertMonitorConfig Javadoc Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): Keep Sunday-only day of week ranges SUN-SUN was rewritten to 0-7, which crontab reads as every day. Co-Authored-By: Claude <noreply@anthropic.com> * fix(spring): Only catch runtime exceptions when deriving monitor config Co-Authored-By: Claude <noreply@anthropic.com> * ref(spring): Shorten upsertMonitorConfig Javadoc Co-Authored-By: Claude <noreply@anthropic.com> * feat(spring-jakarta): Monitor all @scheduled methods with sentry.monitor-scheduled-tasks Adds sentry.monitor-scheduled-tasks and sentry.exclude-scheduled-tasks to Spring Boot 3. Methods without @SentryCheckIn get a slug from the class and method name and send their @scheduled schedule as the monitor config. Co-Authored-By: Claude <noreply@anthropic.com> * changelog Co-Authored-By: Claude <noreply@anthropic.com> * feat(spring): Monitor all @scheduled methods in Spring Boot 2 Co-Authored-By: Claude <noreply@anthropic.com> * docs(changelog): Link Spring Boot 2 PR Co-Authored-By: Claude <noreply@anthropic.com> * fix(spring-jakarta): Skip reactive @scheduled methods when monitoring all tasks Spring calls reactive and suspend @scheduled methods once at startup and subscribes to the result on each run, so check-ins around the call would only be sent once. Co-Authored-By: Claude <noreply@anthropic.com> * fix(spring): Skip reactive @scheduled methods when monitoring all tasks Same fix as the Spring Boot 3 copy. Co-Authored-By: Claude <noreply@anthropic.com> * fix(spring): Skip jobs with unsupported schedules when monitoring all jobs * fix(spring): Skip jobs with unsupported schedules when monitoring all jobs * ref(spring): Use pattern-matching instanceof for the @scheduled time unit * fix(core): Use a 2 minute check-in margin for crons with fixed seconds Dropping a non-zero seconds field makes check-ins arrive up to 59s after the expected minute, which a 1 minute margin barely covers. * fix(spring): Default upsertMonitorConfig to false and read @scheduled like Spring Existing @SentryCheckIn users shouldn't have their monitors changed on upgrade, so sending the schedule is opt-in. Find @scheduled with Spring's own lookup so an overriding method doesn't inherit a parent's schedule. * fix(spring): Find @scheduled tasks to monitor the way Spring does An overriding method without @scheduled isn't scheduled by Spring, so don't monitor it either. * fix(spring): Find @scheduled tasks to monitor the way Spring does An overriding method without @scheduled isn't scheduled by Spring, so don't monitor it either. * feat(spring): Warn when two @scheduled methods share a monitor slug Derived slugs use only the short class name and method name, so different methods can map to the same slug and share one monitor. Log a warning the first time that happens so users can set an explicit slug. * feat(spring): Warn when two @scheduled methods share a monitor slug Derived slugs use only the short class name and method name, so different methods can map to the same slug and share one monitor. Log a warning the first time that happens so users can set an explicit slug. * ref(spring): Reuse the target class for the slug collision check Use the same class for the task name and the collision check so both see one value. * ref(spring): Reuse the target class for the slug collision check Use the same class for the task name and the collision check so both see one value. * fix(spring): Skip private and static @scheduled methods when monitoring all tasks Spring invokes private and static scheduled methods on the target, so the proxy never sees them. Don't match them, so they aren't proxied for nothing. * fix(spring): Skip private and static @scheduled methods when monitoring all tasks Spring invokes private and static scheduled methods on the target, so the proxy never sees them. Don't match them, so they aren't proxied for nothing. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5e69b7 commit e29145a

9 files changed

Lines changed: 657 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
### Features
1111

12-
- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275))
12+
- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275), [#6276](https://github.com/getsentry/sentry-java/pull/6276))
1313
- Quartz `SentryJobListener` can send a monitor config derived from the trigger with check-ins, so Sentry creates or updates the monitor from code. Set the `sentry-upsert-monitor-config` job or trigger data key to `"true"` to send the schedule ([#6216](https://github.com/getsentry/sentry-java/pull/6216))
1414
- Send a monitor config derived from Spring `@Scheduled` with `@SentryCheckIn` check-ins, so Sentry creates or updates the monitor from code. Set `@SentryCheckIn(upsertMonitorConfig = true)` to send the schedule ([#6215](https://github.com/getsentry/sentry-java/pull/6215))
1515

‎sentry-spring-boot/api/sentry-spring-boot.api‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,20 @@ public class io/sentry/spring/boot/SentryProperties : io/sentry/SentryOptions {
3232
public fun <init> ()V
3333
public fun getClients ()Lio/sentry/spring/boot/SentryProperties$Clients;
3434
public fun getExceptionResolverOrder ()I
35+
public fun getExcludeScheduledTasks ()Ljava/util/List;
3536
public fun getGraphql ()Lio/sentry/spring/boot/SentryProperties$Graphql;
3637
public fun getLogging ()Lio/sentry/spring/boot/SentryProperties$Logging;
3738
public fun getUserFilterOrder ()Ljava/lang/Integer;
3839
public fun isKeepTransactionsOpenForAsyncResponses ()Z
40+
public fun isMonitorScheduledTasks ()Z
3941
public fun isUseGitCommitIdAsRelease ()Z
4042
public fun setClients (Lio/sentry/spring/boot/SentryProperties$Clients;)V
4143
public fun setExceptionResolverOrder (I)V
44+
public fun setExcludeScheduledTasks (Ljava/util/List;)V
4245
public fun setGraphql (Lio/sentry/spring/boot/SentryProperties$Graphql;)V
4346
public fun setKeepTransactionsOpenForAsyncResponses (Z)V
4447
public fun setLogging (Lio/sentry/spring/boot/SentryProperties$Logging;)V
48+
public fun setMonitorScheduledTasks (Z)V
4549
public fun setUseGitCommitIdAsRelease (Z)V
4650
public fun setUserFilterOrder (Ljava/lang/Integer;)V
4751
}

‎sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import io.sentry.spring.SpringSecuritySentryUserProvider;
2525
import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration;
2626
import io.sentry.spring.cache.SentryCacheBeanPostProcessor;
27+
import io.sentry.spring.checkin.SentryCheckInAdvice;
2728
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
2829
import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration;
2930
import io.sentry.spring.checkin.SentryQuartzConfiguration;
31+
import io.sentry.spring.checkin.SentryScheduledCheckInPointcut;
3032
import io.sentry.spring.exception.SentryCaptureExceptionParameterPointcutConfiguration;
3133
import io.sentry.spring.exception.SentryExceptionParameterAdviceConfiguration;
3234
import io.sentry.spring.kafka.SentryKafkaConsumerBeanPostProcessor;
@@ -47,11 +49,16 @@
4749
import java.util.List;
4850
import java.util.Optional;
4951
import javax.servlet.http.HttpServletRequest;
52+
import org.aopalliance.aop.Advice;
5053
import org.aspectj.lang.ProceedingJoinPoint;
5154
import org.jetbrains.annotations.NotNull;
5255
import org.quartz.core.QuartzScheduler;
5356
import org.slf4j.MDC;
57+
import org.springframework.aop.Advisor;
58+
import org.springframework.aop.support.DefaultPointcutAdvisor;
5459
import org.springframework.beans.factory.ObjectProvider;
60+
import org.springframework.beans.factory.annotation.Qualifier;
61+
import org.springframework.beans.factory.config.BeanDefinition;
5562
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
5663
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
5764
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -70,6 +77,7 @@
7077
import org.springframework.context.annotation.Conditional;
7178
import org.springframework.context.annotation.Configuration;
7279
import org.springframework.context.annotation.Import;
80+
import org.springframework.context.annotation.Role;
7381
import org.springframework.core.Ordered;
7482
import org.springframework.core.annotation.Order;
7583
import org.springframework.core.env.Environment;
@@ -292,6 +300,27 @@ static class SentryCheckInAspectsConfiguration {
292300
@Import(SentryCheckInPointcutConfiguration.class)
293301
@Open
294302
static class SentryCheckInPointcutAutoConfiguration {}
303+
304+
@Configuration(proxyBeanMethods = false)
305+
@ConditionalOnProperty(name = "sentry.monitor-scheduled-tasks", havingValue = "true")
306+
@Open
307+
static class SentryScheduledCheckInConfiguration {
308+
@Bean
309+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
310+
public @NotNull Advice sentryScheduledCheckInAdvice() {
311+
return new SentryCheckInAdvice(ScopesAdapter.getInstance(), true);
312+
}
313+
314+
@Bean
315+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
316+
public @NotNull Advisor sentryScheduledCheckInAdvisor(
317+
final @NotNull @Qualifier("sentryScheduledCheckInAdvice") Advice advice,
318+
final @NotNull SentryProperties sentryProperties) {
319+
return new DefaultPointcutAdvisor(
320+
new SentryScheduledCheckInPointcut(sentryProperties.getExcludeScheduledTasks()),
321+
advice);
322+
}
323+
}
295324
}
296325

297326
/** Registers beans specific to Spring MVC. */

‎sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProperties.java‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ public class SentryProperties extends SentryOptions {
4040
/** Clients integration properties. */
4141
private @NotNull Clients clients = new Clients();
4242

43+
/** Monitor every {@code @Scheduled} method with Sentry Crons. */
44+
private boolean monitorScheduledTasks = false;
45+
46+
/**
47+
* {@code @Scheduled} methods not to monitor when {@code monitorScheduledTasks} is on, as {@code
48+
* ClassName.methodName} or a regex matching it.
49+
*/
50+
private @NotNull List<String> excludeScheduledTasks = new ArrayList<>();
51+
4352
public boolean isUseGitCommitIdAsRelease() {
4453
return useGitCommitIdAsRelease;
4554
}
@@ -109,6 +118,22 @@ public void setClients(@NotNull Clients clients) {
109118
this.clients = clients;
110119
}
111120

121+
public boolean isMonitorScheduledTasks() {
122+
return monitorScheduledTasks;
123+
}
124+
125+
public void setMonitorScheduledTasks(boolean monitorScheduledTasks) {
126+
this.monitorScheduledTasks = monitorScheduledTasks;
127+
}
128+
129+
public @NotNull List<String> getExcludeScheduledTasks() {
130+
return excludeScheduledTasks;
131+
}
132+
133+
public void setExcludeScheduledTasks(@NotNull List<String> excludeScheduledTasks) {
134+
this.excludeScheduledTasks = excludeScheduledTasks;
135+
}
136+
112137
@Open
113138
public static class Clients {
114139
/** Enable automatic instrumentation of Spring-managed OkHttp clients. Disabled by default. */

‎sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.sentry.NoOpTransportFactory
2020
import io.sentry.ProfileLifecycle
2121
import io.sentry.SamplingContext
2222
import io.sentry.Sentry
23+
import io.sentry.SentryEnvelope
2324
import io.sentry.SentryEvent
2425
import io.sentry.SentryIntegrationPackageStorage
2526
import io.sentry.SentryLevel
@@ -50,6 +51,7 @@ import io.sentry.transport.ITransportGate
5051
import io.sentry.transport.apache.ApacheHttpClientTransportFactory
5152
import java.io.File
5253
import java.lang.RuntimeException
54+
import java.util.concurrent.TimeUnit
5355
import javax.servlet.Filter
5456
import kotlin.test.Test
5557
import kotlin.test.assertEquals
@@ -58,9 +60,12 @@ import kotlin.test.assertTrue
5860
import org.aspectj.lang.ProceedingJoinPoint
5961
import org.assertj.core.api.Assertions.assertThat
6062
import org.mockito.internal.util.MockUtil.isMock
63+
import org.mockito.kotlin.after
6164
import org.mockito.kotlin.any
6265
import org.mockito.kotlin.anyOrNull
66+
import org.mockito.kotlin.argumentCaptor
6367
import org.mockito.kotlin.mock
68+
import org.mockito.kotlin.timeout
6469
import org.mockito.kotlin.verify
6570
import org.mockito.kotlin.whenever
6671
import org.quartz.JobExecutionContext
@@ -71,6 +76,7 @@ import org.quartz.core.QuartzScheduler
7176
import org.slf4j.MDC
7277
import org.springframework.aop.support.NameMatchMethodPointcut
7378
import org.springframework.boot.autoconfigure.AutoConfigurations
79+
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
7480
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration
7581
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer
7682
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
@@ -85,6 +91,8 @@ import org.springframework.context.annotation.Bean
8591
import org.springframework.context.annotation.Configuration
8692
import org.springframework.core.Ordered
8793
import org.springframework.core.annotation.Order
94+
import org.springframework.scheduling.annotation.EnableScheduling
95+
import org.springframework.scheduling.annotation.Scheduled
8896
import org.springframework.scheduling.quartz.SchedulerFactoryBean
8997
import org.springframework.security.core.context.SecurityContextHolder
9098
import org.springframework.web.client.RestTemplate
@@ -1126,6 +1134,44 @@ class SentryAutoConfigurationTest {
11261134
.run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) }
11271135
}
11281136

1137+
@Test
1138+
fun `does not monitor @Scheduled methods by default`() {
1139+
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run {
1140+
assertThat(it).doesNotHaveBean("sentryScheduledCheckInAdvisor")
1141+
}
1142+
}
1143+
1144+
@Test
1145+
fun `monitors @Scheduled methods when monitor-scheduled-tasks is enabled`() {
1146+
baseContextRunner
1147+
.withPropertyValues(
1148+
"sentry.dsn=http://key@localhost/proj",
1149+
"sentry.monitor-scheduled-tasks=true",
1150+
"sentry.exclude-scheduled-tasks[0]=SentryAutoConfigurationTest.ScheduledJobs.excluded",
1151+
)
1152+
.withConfiguration(AutoConfigurations.of(AopAutoConfiguration::class.java))
1153+
.withUserConfiguration(
1154+
MockTransportConfiguration::class.java,
1155+
ScheduledJobsConfiguration::class.java,
1156+
)
1157+
.run {
1158+
assertThat(it).hasBean("sentryScheduledCheckInAdvisor")
1159+
val transport = it.getBean(ITransport::class.java)
1160+
val captor = argumentCaptor<SentryEnvelope>()
1161+
verify(transport, timeout(5000).times(2)).send(any(), anyOrNull())
1162+
verify(transport, after(500).times(2)).send(captor.capture(), anyOrNull())
1163+
val payloads =
1164+
captor.allValues.map { envelope -> String(envelope.items.first().data, Charsets.UTF_8) }
1165+
assertThat(payloads).allMatch { payload ->
1166+
payload.contains(
1167+
"\"monitor_slug\":\"sentry-auto-configuration-test-scheduled-jobs-sync\""
1168+
)
1169+
}
1170+
assertThat(payloads[0]).contains("\"value\":1,\"unit\":\"hour\"")
1171+
assertThat(payloads[1]).contains("\"status\":\"ok\"")
1172+
}
1173+
}
1174+
11291175
@Test
11301176
fun `Sentry quartz job listener is added`() {
11311177
contextRunner
@@ -1241,6 +1287,18 @@ class SentryAutoConfigurationTest {
12411287
@Bean open fun sentryTransport() = transport
12421288
}
12431289

1290+
@Configuration(proxyBeanMethods = false)
1291+
@EnableScheduling
1292+
open class ScheduledJobsConfiguration {
1293+
@Bean open fun scheduledJobs() = ScheduledJobs()
1294+
}
1295+
1296+
open class ScheduledJobs {
1297+
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) open fun sync() {}
1298+
1299+
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) open fun excluded() {}
1300+
}
1301+
12441302
@Configuration(proxyBeanMethods = false)
12451303
open class NoOpTransportConfiguration {
12461304

‎sentry-spring/api/sentry-spring.api‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public abstract interface annotation class io/sentry/spring/checkin/SentryCheckI
146146
public class io/sentry/spring/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware {
147147
public fun <init> ()V
148148
public fun <init> (Lio/sentry/IScopes;)V
149+
public fun <init> (Lio/sentry/IScopes;Z)V
149150
public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object;
150151
public fun setEmbeddedValueResolver (Lorg/springframework/util/StringValueResolver;)V
151152
}
@@ -166,6 +167,11 @@ public class io/sentry/spring/checkin/SentryQuartzConfiguration {
166167
public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer;
167168
}
168169

170+
public final class io/sentry/spring/checkin/SentryScheduledCheckInPointcut : org/springframework/aop/support/StaticMethodMatcherPointcut {
171+
public fun <init> (Ljava/util/List;)V
172+
public fun matches (Ljava/lang/reflect/Method;Ljava/lang/Class;)Z
173+
}
174+
169175
public final class io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer {
170176
public fun <init> ()V
171177
public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V

0 commit comments

Comments
 (0)