Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- 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))

## 8.59.0
Expand Down
1 change: 1 addition & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public final class io/sentry/spring7/cache/SentryCacheWrapper : org/springframew
public abstract interface annotation class io/sentry/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation {
public abstract fun heartbeat ()Z
public abstract fun monitorSlug ()Ljava/lang/String;
public abstract fun upsertMonitorConfig ()Z
public abstract fun value ()Ljava/lang/String;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
*/
boolean heartbeat() default false;

/**
* Whether to send the schedule and zone from the method's {@code @Scheduled} with check-ins, so
* Sentry creates or updates the monitor. On by default. Set to false to manage the monitor's
* schedule in Sentry instead.
*
* <p>Heartbeat check-ins never send a monitor config.
*
* @return true to send a monitor config, true by default
*/
boolean upsertMonitorConfig() default true;

/**
* Monitor slug. If not set, no check-in will be sent.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,32 @@
import io.sentry.DateUtils;
import io.sentry.IScopes;
import io.sentry.ISentryLifecycleToken;
import io.sentry.MonitorConfig;
import io.sentry.ScopesAdapter;
import io.sentry.SentryLevel;
import io.sentry.protocol.SentryId;
import io.sentry.time.Stopwatch;
import io.sentry.util.MonitorConfigUtils;
import io.sentry.util.Objects;
import io.sentry.util.TracingUtils;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringValueResolver;

Expand All @@ -31,10 +42,19 @@
@ApiStatus.Internal
@Open
public class SentryCheckInAdvice implements MethodInterceptor, EmbeddedValueResolverAware {
// Spring before 5.3 parses crons with CronSequenceGenerator
private static final boolean LEGACY_CRON_PARSER =
!ClassUtils.isPresent(
"org.springframework.scheduling.support.CronExpression",
SentryCheckInAdvice.class.getClassLoader());

private final @NotNull IScopes scopes;

private @Nullable StringValueResolver resolver;

private final @NotNull Map<Method, CachedMonitorConfig> monitorConfigs =
new ConcurrentHashMap<>();

public SentryCheckInAdvice() {
this(ScopesAdapter.getInstance());
}
Expand Down Expand Up @@ -87,6 +107,11 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
return invocation.proceed();
}

final @Nullable MonitorConfig monitorConfig =
!isHeartbeatOnly && checkInAnnotation.upsertMonitorConfig()
? monitorConfig(mostSpecificMethod)
: null;

try (final @NotNull ISentryLifecycleToken ignored =
scopes.forkedScopes("SentryCheckInAdvice").makeCurrent()) {
TracingUtils.startNewTrace(scopes);
Expand All @@ -98,7 +123,9 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl

try {
if (!isHeartbeatOnly) {
checkInId = scopes.captureCheckIn(new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS));
final @NotNull CheckIn inProgress = new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS);
inProgress.setMonitorConfig(monitorConfig);
checkInId = scopes.captureCheckIn(inProgress);
}
return invocation.proceed();
} catch (Throwable e) {
Expand All @@ -113,6 +140,96 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
}
}

private @Nullable MonitorConfig monitorConfig(final @NotNull Method method) {
return monitorConfigs.computeIfAbsent(
method, key -> new CachedMonitorConfig(monitorConfigFromScheduled(key)))
.config;
}

private @Nullable MonitorConfig monitorConfigFromScheduled(final @NotNull Method method) {
try {
final @NotNull Set<Scheduled> schedules =
AnnotatedElementUtils.findMergedRepeatableAnnotations(method, Scheduled.class);
if (schedules.size() != 1) {
return null;
}
final @NotNull Scheduled scheduled = schedules.iterator().next();
// timeUnit only exists from Spring 5.3.10, so read it as an attribute
final @Nullable Object timeUnitAttribute =
AnnotationUtils.getAnnotationAttributes(scheduled).get("timeUnit");
final @NotNull TimeUnit timeUnit =
timeUnitAttribute instanceof TimeUnit
? (TimeUnit) timeUnitAttribute
: TimeUnit.MILLISECONDS;
final @Nullable String cron = resolve(scheduled.cron());
// Spring only reads the zone for a cron
final @Nullable String zone =
cron == null || cron.isEmpty() ? null : resolve(scheduled.zone());
return MonitorConfigUtils.fromSpringScheduled(
cron,
zone,
periodMillis(scheduled.fixedRate(), scheduled.fixedRateString(), timeUnit),
periodMillis(scheduled.fixedDelay(), scheduled.fixedDelayString(), timeUnit),
LEGACY_CRON_PARSER);
} catch (RuntimeException e) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.WARNING,
"Could not derive a monitor config from @Scheduled for method annotated with @SentryCheckIn.",
e);
return null;
}
Comment thread
wedamija marked this conversation as resolved.
}

private @Nullable Long periodMillis(
final long value, final @NotNull String valueString, final @NotNull TimeUnit timeUnit) {
if (value >= 0) {
return timeUnit.toMillis(value);
}
final @Nullable String resolved = resolve(valueString);
if (resolved == null || resolved.isEmpty()) {
return null;
}
final @NotNull String trimmed = resolved.trim();
@Nullable Long millis = null;
if (trimmed.startsWith("P") || trimmed.startsWith("p")) {
try {
millis = Duration.parse(trimmed).toMillis();
} catch (DateTimeParseException | ArithmeticException e) {
// logged below
}
} else {
millis = MonitorConfigUtils.parsePeriodMillis(trimmed, timeUnit);
}
if (millis == null) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG,
"Not sending a monitor config for @SentryCheckIn because the @Scheduled period '%s' could not be parsed.",
trimmed);
}
return millis;
}

private @Nullable String resolve(final @NotNull String value) {
if (resolver == null || value.isEmpty()) {
return value;
}
return resolver.resolveStringValue(value);
}

private static final class CachedMonitorConfig {
private final @Nullable MonitorConfig config;

private CachedMonitorConfig(final @Nullable MonitorConfig config) {
this.config = config;
}
}

@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.resolver = resolver;
Expand Down
Loading
Loading