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), [#6276](https://github.com/getsentry/sentry-java/pull/6276))
- 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), [#6277](https://github.com/getsentry/sentry-java/pull/6277))
- 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
6 changes: 6 additions & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public abstract interface annotation class io/sentry/spring7/checkin/SentryCheck
public class io/sentry/spring7/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 @@ -163,6 +164,11 @@ public class io/sentry/spring7/checkin/SentryQuartzConfiguration {
public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer;
}

public final class io/sentry/spring7/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/spring7/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/quartz/autoconfigure/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 @@ -111,7 +138,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 @@ -140,6 +175,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.sentry.spring7.checkin;

import io.sentry.FilterString;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ClassUtils;

/**
* Matches {@code @Scheduled} methods without {@link SentryCheckIn}, except excluded ones. Excludes
* match the task name ({@code ClassName.methodName}) exactly or as a regex.
*/
@ApiStatus.Experimental
public final class SentryScheduledCheckInPointcut extends StaticMethodMatcherPointcut {
private final @NotNull List<FilterString> excludedScheduledTasks = new ArrayList<>();

public SentryScheduledCheckInPointcut(final @Nullable List<String> excludedScheduledTasks) {
if (excludedScheduledTasks != null) {
for (final @NotNull String task : excludedScheduledTasks) {
if (!task.trim().isEmpty()) {
this.excludedScheduledTasks.add(new FilterString(task.trim()));
}
}
}
}

@Override
public boolean matches(final @NotNull Method method, final @NotNull Class<?> targetClass) {
final @NotNull Class<?> userClass = ClassUtils.getUserClass(targetClass);
// CGLIB can't intercept these
if (Modifier.isFinal(userClass.getModifiers()) || Modifier.isFinal(method.getModifiers())) {
return false;
}
final @NotNull Method specificMethod = AopUtils.getMostSpecificMethod(method, userClass);
if (!SentryCheckInAdvice.isScheduled(specificMethod)
|| AnnotationUtils.findAnnotation(specificMethod, SentryCheckIn.class) != null) {
return false;
}
final @NotNull String taskName =
SentryCheckInAdvice.scheduledTaskName(userClass, specificMethod);
for (final @NotNull FilterString excluded : excludedScheduledTasks) {
if (excluded.getFilterString().equals(taskName) || excluded.matches(taskName)) {
return false;
}
}
return true;
}
}
Loading
Loading