Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Features

- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275))
- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. 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))
- Send a monitor config derived from Spring `@Scheduled` with `@SentryCheckIn` check-ins by default, so Sentry creates or updates the monitor from code. Set `@SentryCheckIn(upsertMonitorConfig = false)` to turn this off ([#6215](https://github.com/getsentry/sentry-java/pull/6215))
- Report the cellular network technology generation in `device.connection_effective_type`, for example `4g` or `5g` ([#6146](https://github.com/getsentry/sentry-java/pull/6146))

Expand Down
4 changes: 4 additions & 0 deletions sentry-spring-boot/api/sentry-spring-boot.api
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@ public class io/sentry/spring/boot/SentryProperties : io/sentry/SentryOptions {
public fun <init> ()V
public fun getClients ()Lio/sentry/spring/boot/SentryProperties$Clients;
public fun getExceptionResolverOrder ()I
public fun getExcludeScheduledTasks ()Ljava/util/List;
public fun getGraphql ()Lio/sentry/spring/boot/SentryProperties$Graphql;
public fun getLogging ()Lio/sentry/spring/boot/SentryProperties$Logging;
public fun getUserFilterOrder ()Ljava/lang/Integer;
public fun isKeepTransactionsOpenForAsyncResponses ()Z
public fun isMonitorScheduledTasks ()Z
public fun isUseGitCommitIdAsRelease ()Z
public fun setClients (Lio/sentry/spring/boot/SentryProperties$Clients;)V
public fun setExceptionResolverOrder (I)V
public fun setExcludeScheduledTasks (Ljava/util/List;)V
public fun setGraphql (Lio/sentry/spring/boot/SentryProperties$Graphql;)V
public fun setKeepTransactionsOpenForAsyncResponses (Z)V
public fun setLogging (Lio/sentry/spring/boot/SentryProperties$Logging;)V
public fun setMonitorScheduledTasks (Z)V
public fun setUseGitCommitIdAsRelease (Z)V
public fun setUserFilterOrder (Ljava/lang/Integer;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
import io.sentry.spring.SpringSecuritySentryUserProvider;
import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration;
import io.sentry.spring.cache.SentryCacheBeanPostProcessor;
import io.sentry.spring.checkin.SentryCheckInAdvice;
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration;
import io.sentry.spring.checkin.SentryQuartzConfiguration;
import io.sentry.spring.checkin.SentryScheduledCheckInPointcut;
import io.sentry.spring.exception.SentryCaptureExceptionParameterPointcutConfiguration;
import io.sentry.spring.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.kafka.SentryKafkaConsumerBeanPostProcessor;
Expand All @@ -47,11 +49,16 @@
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.aopalliance.aop.Advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.jetbrains.annotations.NotNull;
import org.quartz.core.QuartzScheduler;
import org.slf4j.MDC;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand All @@ -70,6 +77,7 @@
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
Expand Down Expand Up @@ -292,6 +300,27 @@ static class SentryCheckInAspectsConfiguration {
@Import(SentryCheckInPointcutConfiguration.class)
@Open
static class SentryCheckInPointcutAutoConfiguration {}

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "sentry.monitor-scheduled-tasks", havingValue = "true")
@Open
static class SentryScheduledCheckInConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public @NotNull Advice sentryScheduledCheckInAdvice() {
return new SentryCheckInAdvice(ScopesAdapter.getInstance(), true);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public @NotNull Advisor sentryScheduledCheckInAdvisor(
final @NotNull @Qualifier("sentryScheduledCheckInAdvice") Advice advice,
final @NotNull SentryProperties sentryProperties) {
return new DefaultPointcutAdvisor(
new SentryScheduledCheckInPointcut(sentryProperties.getExcludeScheduledTasks()),
advice);
}
}
}

/** Registers beans specific to Spring MVC. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public class SentryProperties extends SentryOptions {
/** Clients integration properties. */
private @NotNull Clients clients = new Clients();

/** Monitor every {@code @Scheduled} method with Sentry Crons. */
private boolean monitorScheduledTasks = false;

/**
* {@code @Scheduled} methods not to monitor when {@code monitorScheduledTasks} is on, as {@code
* ClassName.methodName} or a regex matching it.
*/
private @NotNull List<String> excludeScheduledTasks = new ArrayList<>();

public boolean isUseGitCommitIdAsRelease() {
return useGitCommitIdAsRelease;
}
Expand Down Expand Up @@ -109,6 +118,22 @@ public void setClients(@NotNull Clients clients) {
this.clients = clients;
}

public boolean isMonitorScheduledTasks() {
return monitorScheduledTasks;
}

public void setMonitorScheduledTasks(boolean monitorScheduledTasks) {
this.monitorScheduledTasks = monitorScheduledTasks;
}

public @NotNull List<String> getExcludeScheduledTasks() {
return excludeScheduledTasks;
}

public void setExcludeScheduledTasks(@NotNull List<String> excludeScheduledTasks) {
this.excludeScheduledTasks = excludeScheduledTasks;
}

@Open
public static class Clients {
/** Enable automatic instrumentation of Spring-managed OkHttp clients. Disabled by default. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.sentry.NoOpTransportFactory
import io.sentry.ProfileLifecycle
import io.sentry.SamplingContext
import io.sentry.Sentry
import io.sentry.SentryEnvelope
import io.sentry.SentryEvent
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
Expand Down Expand Up @@ -50,6 +51,7 @@ import io.sentry.transport.ITransportGate
import io.sentry.transport.apache.ApacheHttpClientTransportFactory
import java.io.File
import java.lang.RuntimeException
import java.util.concurrent.TimeUnit
import javax.servlet.Filter
import kotlin.test.Test
import kotlin.test.assertEquals
Expand All @@ -58,9 +60,12 @@ import kotlin.test.assertTrue
import org.aspectj.lang.ProceedingJoinPoint
import org.assertj.core.api.Assertions.assertThat
import org.mockito.internal.util.MockUtil.isMock
import org.mockito.kotlin.after
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.timeout
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.quartz.JobExecutionContext
Expand All @@ -71,6 +76,7 @@ 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.aop.AopAutoConfiguration
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
Expand All @@ -85,6 +91,8 @@ 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.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.scheduling.quartz.SchedulerFactoryBean
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.client.RestTemplate
Expand Down Expand Up @@ -1126,6 +1134,44 @@ class SentryAutoConfigurationTest {
.run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) }
}

@Test
fun `does not monitor @Scheduled methods by default`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run {
assertThat(it).doesNotHaveBean("sentryScheduledCheckInAdvisor")
}
}

@Test
fun `monitors @Scheduled methods when monitor-scheduled-tasks is enabled`() {
baseContextRunner
.withPropertyValues(
"sentry.dsn=http://key@localhost/proj",
"sentry.monitor-scheduled-tasks=true",
"sentry.exclude-scheduled-tasks[0]=SentryAutoConfigurationTest.ScheduledJobs.excluded",
)
.withConfiguration(AutoConfigurations.of(AopAutoConfiguration::class.java))
.withUserConfiguration(
MockTransportConfiguration::class.java,
ScheduledJobsConfiguration::class.java,
)
.run {
assertThat(it).hasBean("sentryScheduledCheckInAdvisor")
val transport = it.getBean(ITransport::class.java)
val captor = argumentCaptor<SentryEnvelope>()
verify(transport, timeout(5000).times(2)).send(any(), anyOrNull())
verify(transport, after(500).times(2)).send(captor.capture(), anyOrNull())
val payloads =
captor.allValues.map { envelope -> String(envelope.items.first().data, Charsets.UTF_8) }
assertThat(payloads).allMatch { payload ->
payload.contains(
"\"monitor_slug\":\"sentry-auto-configuration-test-scheduled-jobs-sync\""
)
}
assertThat(payloads[0]).contains("\"value\":1,\"unit\":\"hour\"")
assertThat(payloads[1]).contains("\"status\":\"ok\"")
}
}

@Test
fun `Sentry quartz job listener is added`() {
contextRunner
Expand Down Expand Up @@ -1241,6 +1287,18 @@ class SentryAutoConfigurationTest {
@Bean open fun sentryTransport() = transport
}

@Configuration(proxyBeanMethods = false)
@EnableScheduling
open class ScheduledJobsConfiguration {
@Bean open fun scheduledJobs() = ScheduledJobs()
}

open class ScheduledJobs {
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) open fun sync() {}

@Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) open fun excluded() {}
}

@Configuration(proxyBeanMethods = false)
open class NoOpTransportConfiguration {

Expand Down
6 changes: 6 additions & 0 deletions sentry-spring/api/sentry-spring.api
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public abstract interface annotation class io/sentry/spring/checkin/SentryCheckI
public class io/sentry/spring/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware {
public fun <init> ()V
public fun <init> (Lio/sentry/IScopes;)V
public fun <init> (Lio/sentry/IScopes;Z)V
public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object;
public fun setEmbeddedValueResolver (Lorg/springframework/util/StringValueResolver;)V
}
Expand All @@ -161,6 +162,11 @@ public class io/sentry/spring/checkin/SentryQuartzConfiguration {
public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer;
}

public final class io/sentry/spring/checkin/SentryScheduledCheckInPointcut : org/springframework/aop/support/StaticMethodMatcherPointcut {
public fun <init> (Ljava/util/List;)V
public fun matches (Ljava/lang/reflect/Method;Ljava/lang/Class;)Z
}

public final class io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer {
public fun <init> ()V
public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
import io.sentry.util.Objects;
import io.sentry.util.TracingUtils;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.ApiStatus;
Expand Down Expand Up @@ -48,8 +52,14 @@ public class SentryCheckInAdvice implements MethodInterceptor, EmbeddedValueReso
"org.springframework.scheduling.support.CronExpression",
SentryCheckInAdvice.class.getClassLoader());

private static final @NotNull Pattern WORD_BOUNDARY =
Pattern.compile("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])");
private static final int MAX_SLUG_LENGTH = 50;

private final @NotNull IScopes scopes;

private final boolean monitorScheduledTasks;

private @Nullable StringValueResolver resolver;

private final @NotNull Map<Method, CachedMonitorConfig> monitorConfigs =
Expand All @@ -60,7 +70,16 @@ public SentryCheckInAdvice() {
}

public SentryCheckInAdvice(final @NotNull IScopes scopes) {
this(scopes, false);
}

/**
* @param monitorScheduledTasks whether to also monitor {@code @Scheduled} methods without {@link
* SentryCheckIn}, using a slug derived from the class and method name
*/
public SentryCheckInAdvice(final @NotNull IScopes scopes, final boolean monitorScheduledTasks) {
this.scopes = Objects.requireNonNull(scopes, "scopes are required");
this.monitorScheduledTasks = monitorScheduledTasks;
}

@Override
Expand All @@ -72,6 +91,14 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
SentryCheckIn checkInAnnotation =
AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class);
if (checkInAnnotation == null) {
if (monitorScheduledTasks && isScheduled(mostSpecificMethod)) {
return captureCheckIns(
invocation,
scheduledTaskSlug(
scheduledTaskName(invocation.getThis().getClass(), mostSpecificMethod)),
false,
monitorConfig(mostSpecificMethod));
}
return invocation.proceed();
}

Expand Down Expand Up @@ -114,7 +141,15 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
!isHeartbeatOnly && checkInAnnotation.upsertMonitorConfig()
? monitorConfig(mostSpecificMethod)
: null;
return captureCheckIns(invocation, monitorSlug, isHeartbeatOnly, monitorConfig);
}

private Object captureCheckIns(
final @NotNull MethodInvocation invocation,
final @NotNull String monitorSlug,
final boolean isHeartbeatOnly,
final @Nullable MonitorConfig monitorConfig)
throws Throwable {
try (final @NotNull ISentryLifecycleToken ignored =
scopes.forkedScopes("SentryCheckInAdvice").makeCurrent()) {
TracingUtils.startNewTrace(scopes);
Expand Down Expand Up @@ -143,6 +178,37 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
}
}

static boolean isScheduled(final @NotNull Method method) {
return !AnnotatedElementUtils.findMergedRepeatableAnnotations(method, Scheduled.class)
.isEmpty();
}

/** {@code ClassName.methodName}, with nested classes as {@code Outer.Inner}. */
static @NotNull String scheduledTaskName(
final @NotNull Class<?> targetClass, final @NotNull Method method) {
return ClassUtils.getShortName(ClassUtils.getUserClass(targetClass)) + "." + method.getName();
}

static @NotNull String scheduledTaskSlug(final @NotNull String taskName) {
final @NotNull String slug =
WORD_BOUNDARY
.matcher(taskName)
.replaceAll("-")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "");
if (slug.length() <= MAX_SLUG_LENGTH) {
return slug;
}
// keep truncated slugs unique
final @NotNull CRC32 crc = new CRC32();
crc.update(slug.getBytes(StandardCharsets.UTF_8));
final @NotNull String hash = String.format(Locale.ROOT, "%08x", crc.getValue());
return slug.substring(0, MAX_SLUG_LENGTH - hash.length() - 1).replaceAll("-+$", "")
+ "-"
+ hash;
}

private @Nullable MonitorConfig monitorConfig(final @NotNull Method method) {
return monitorConfigs.computeIfAbsent(
method, key -> new CachedMonitorConfig(monitorConfigFromScheduled(key)))
Expand Down
Loading
Loading