Instrument Scheduled methods for observability

This commit enhances the `ScheduledAnnotationBeanPostProcessor` to
instrument `@Scheduled` methods declared on beans. This will create
`"tasks.scheduled.execution"` observations for each execution of a
scheduled method. This supports both blocking and reactive variants.

By default, observations are no-ops; developers must configure the
current `ObservationRegistry` on the `ScheduledTaskRegistrar` by using a
`SchedulingConfigurer`.

Closes gh-29883
This commit is contained in:
Brian Clozel
2023-06-19 08:55:08 +02:00
parent 842569c9e5
commit 09cb844421
14 changed files with 900 additions and 24 deletions

View File

@@ -414,6 +414,10 @@ public class ScheduledAnnotationBeanPostProcessor
* accordingly. The Runnable can represent either a synchronous method invocation
* (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous
* one (see {@link #processScheduledAsync(Scheduled, Method, Object)}).
* @param scheduled the {@code @Scheduled} annotation
* @param runnable the runnable to be scheduled
* @param method the method that the annotation has been declared on
* @param bean the target bean instance
*/
protected void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
try {
@@ -578,6 +582,7 @@ public class ScheduledAnnotationBeanPostProcessor
Runnable task;
try {
task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
this.registrar::getObservationRegistry,
this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
}
catch (IllegalArgumentException ex) {
@@ -598,7 +603,7 @@ public class ScheduledAnnotationBeanPostProcessor
protected Runnable createRunnable(Object target, Method method) {
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
return new ScheduledMethodRunnable(target, invocableMethod);
return new ScheduledMethodRunnable(target, invocableMethod, this.registrar::getObservationRegistry);
}
private static Duration toDuration(long value, TimeUnit timeUnit) {

View File

@@ -20,7 +20,11 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.function.Supplier;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
@@ -34,16 +38,22 @@ import org.springframework.core.KotlinDetector;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.config.DefaultScheduledTaskObservationConvention;
import org.springframework.scheduling.config.ScheduledTaskObservationContext;
import org.springframework.scheduling.config.ScheduledTaskObservationConvention;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import static org.springframework.scheduling.config.ScheduledTaskObservationDocumentation.TASKS_SCHEDULED_EXECUTION;
/**
* Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive
* cases without a dependency on optional classes.
*
* @author Simon Baslé
* @author Brian Clozel
* @since 6.1
*/
abstract class ScheduledAnnotationReactiveSupport {
@@ -157,11 +167,12 @@ abstract class ScheduledAnnotationReactiveSupport {
* delay is applied until the next iteration).
*/
static Runnable createSubscriptionRunnable(Method method, Object targetBean, Scheduled scheduled,
List<Runnable> subscriptionTrackerRegistry) {
Supplier<ObservationRegistry> observationRegistrySupplier, List<Runnable> subscriptionTrackerRegistry) {
boolean shouldBlock = (scheduled.fixedDelay() > 0 || StringUtils.hasText(scheduled.fixedDelayString()));
Publisher<?> publisher = getPublisherFor(method, targetBean);
return new SubscribingRunnable(publisher, shouldBlock, subscriptionTrackerRegistry);
Supplier<ScheduledTaskObservationContext> contextSupplier = () -> new ScheduledTaskObservationContext(targetBean, method);
return new SubscribingRunnable(publisher, shouldBlock, subscriptionTrackerRegistry, observationRegistrySupplier, contextSupplier);
}
@@ -173,23 +184,33 @@ abstract class ScheduledAnnotationReactiveSupport {
private final Publisher<?> publisher;
private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = new DefaultScheduledTaskObservationConvention();
final boolean shouldBlock;
private final List<Runnable> subscriptionTrackerRegistry;
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock, List<Runnable> subscriptionTrackerRegistry) {
final Supplier<ObservationRegistry> observationRegistrySupplier;
final Supplier<ScheduledTaskObservationContext> contextSupplier;
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock, List<Runnable> subscriptionTrackerRegistry,
Supplier<ObservationRegistry> observationRegistrySupplier, Supplier<ScheduledTaskObservationContext> contextSupplier) {
this.publisher = publisher;
this.shouldBlock = shouldBlock;
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
this.observationRegistrySupplier = observationRegistrySupplier;
this.contextSupplier = contextSupplier;
}
@Override
public void run() {
Observation observation = TASKS_SCHEDULED_EXECUTION.observation(null, DEFAULT_CONVENTION,
this.contextSupplier, this.observationRegistrySupplier.get());
if (this.shouldBlock) {
CountDownLatch latch = new CountDownLatch(1);
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, latch);
this.subscriptionTrackerRegistry.add(subscriber);
this.publisher.subscribe(subscriber);
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation, latch);
subscribe(subscriber, observation);
try {
latch.await();
}
@@ -198,8 +219,19 @@ abstract class ScheduledAnnotationReactiveSupport {
}
}
else {
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry);
this.subscriptionTrackerRegistry.add(subscriber);
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation);
subscribe(subscriber, observation);
}
}
private void subscribe(TrackingSubscriber subscriber, Observation observation) {
this.subscriptionTrackerRegistry.add(subscriber);
if (reactorPresent) {
Flux.from(this.publisher)
.contextWrite(context -> context.put(ObservationThreadLocalAccessor.KEY, observation))
.subscribe(subscriber);
}
else {
this.publisher.subscribe(subscriber);
}
}
@@ -215,6 +247,8 @@ abstract class ScheduledAnnotationReactiveSupport {
private final List<Runnable> subscriptionTrackerRegistry;
private final Observation observation;
@Nullable
private final CountDownLatch blockingLatch;
@@ -225,12 +259,13 @@ abstract class ScheduledAnnotationReactiveSupport {
@Nullable
private Subscription subscription;
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry) {
this(subscriptionTrackerRegistry, null);
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, Observation observation) {
this(subscriptionTrackerRegistry, observation, null);
}
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, @Nullable CountDownLatch latch) {
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, Observation observation, @Nullable CountDownLatch latch) {
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
this.observation = observation;
this.blockingLatch = latch;
}
@@ -238,6 +273,7 @@ abstract class ScheduledAnnotationReactiveSupport {
public void run() {
if (this.subscription != null) {
this.subscription.cancel();
this.observation.stop();
}
if (this.blockingLatch != null) {
this.blockingLatch.countDown();
@@ -247,6 +283,7 @@ abstract class ScheduledAnnotationReactiveSupport {
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
this.observation.start();
subscription.request(Integer.MAX_VALUE);
}
@@ -259,6 +296,8 @@ abstract class ScheduledAnnotationReactiveSupport {
public void onError(Throwable ex) {
this.subscriptionTrackerRegistry.remove(this);
logger.warn("Unexpected error occurred in scheduled reactive task", ex);
this.observation.error(ex);
this.observation.stop();
if (this.blockingLatch != null) {
this.blockingLatch.countDown();
}
@@ -267,6 +306,10 @@ abstract class ScheduledAnnotationReactiveSupport {
@Override
public void onComplete() {
this.subscriptionTrackerRegistry.remove(this);
if (this.observation.getContext() instanceof ScheduledTaskObservationContext context) {
context.setComplete(true);
}
this.observation.stop();
if (this.blockingLatch != null) {
this.blockingLatch.countDown();
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.config;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.util.StringUtils;
import static org.springframework.scheduling.config.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames;
/**
* Default implementation for {@link ScheduledTaskObservationConvention}.
* @author Brian Clozel
* @since 6.1.0
*/
public class DefaultScheduledTaskObservationConvention implements ScheduledTaskObservationConvention {
private static final String DEFAULT_NAME = "tasks.scheduled.execution";
private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE);
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "SUCCESS");
private static final KeyValue OUTCOME_ERROR = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "ERROR");
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
@Override
public String getName() {
return DEFAULT_NAME;
}
@Override
public String getContextualName(ScheduledTaskObservationContext context) {
return "task " + StringUtils.uncapitalize(context.getTargetClass().getSimpleName())
+ "." + context.getMethod().getName();
}
@Override
public KeyValues getLowCardinalityKeyValues(ScheduledTaskObservationContext context) {
return KeyValues.of(exception(context), methodName(context), outcome(context), targetType(context));
}
protected KeyValue exception(ScheduledTaskObservationContext context) {
if (context.getError() != null) {
return KeyValue.of(LowCardinalityKeyNames.EXCEPTION, context.getError().getClass().getSimpleName());
}
return EXCEPTION_NONE;
}
protected KeyValue methodName(ScheduledTaskObservationContext context) {
return KeyValue.of(LowCardinalityKeyNames.METHOD_NAME, context.getMethod().getName());
}
protected KeyValue outcome(ScheduledTaskObservationContext context) {
if (context.getError() != null) {
return OUTCOME_ERROR;
}
else if (!context.isComplete()) {
return OUTCOME_UNKNOWN;
}
return OUTCOME_SUCCESS;
}
protected KeyValue targetType(ScheduledTaskObservationContext context) {
return KeyValue.of(LowCardinalityKeyNames.TARGET_TYPE, context.getTargetClass().getSimpleName());
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.config;
import java.lang.reflect.Method;
import io.micrometer.observation.Observation;
import org.springframework.util.ClassUtils;
/**
* Context that holds information for observation metadata collection
* during the {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION execution of scheduled tasks}.
* @author Brian Clozel
* @since 6.1.0
*/
public class ScheduledTaskObservationContext extends Observation.Context {
private final Class<?> targetClass;
private final Method method;
private boolean complete;
/**
* Create a new observation context for a task, given the target object
* and the method to be called.
* @param target the target object that is called for task execution
* @param method the method that is called for task execution
*/
public ScheduledTaskObservationContext(Object target, Method method) {
this.targetClass = ClassUtils.getUserClass(target);
this.method = method;
}
/**
* Return the type of the target object.
*/
public Class<?> getTargetClass() {
return this.targetClass;
}
/**
* Return the method that is called for task execution.
*/
public Method getMethod() {
return this.method;
}
/**
* Return whether the task execution is complete.
* <p>If an observation has ended and the task is not complete, this means
* that an {@link #getError() error} was raised or that the task execution got cancelled
* during its execution.
*/
public boolean isComplete() {
return this.complete;
}
/**
* Set whether the task execution has completed.
*/
public void setComplete(boolean complete) {
this.complete = complete;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.config;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
/**
* Interface for an {@link ObservationConvention} for
* {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION scheduled task executions}.
* @author Brian Clozel
* @since 6.1.0
*/
public interface ScheduledTaskObservationConvention extends ObservationConvention<ScheduledTaskObservationContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof ScheduledTaskObservationContext;
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.config;
import io.micrometer.common.KeyValue;
import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.docs.ObservationDocumentation;
/**
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the observations on
* executions of {@link org.springframework.scheduling.annotation.Scheduled scheduled tasks}.
* <p>This class is used by automated tools to document KeyValues attached to the {@code @Scheduled} observations.
*
* @author Brian Clozel
* @since 6.1.0
*/
public enum ScheduledTaskObservationDocumentation implements ObservationDocumentation {
/**
* Observations on executions of {@link org.springframework.scheduling.annotation.Scheduled} tasks.
*/
TASKS_SCHEDULED_EXECUTION {
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultScheduledTaskObservationConvention.class;
}
@Override
public KeyName[] getLowCardinalityKeyNames() {
return LowCardinalityKeyNames.values();
}
@Override
public KeyName[] getHighCardinalityKeyNames() {
return new KeyName[] {};
}
};
public enum LowCardinalityKeyNames implements KeyName {
/**
* {@link Class#getSimpleName() Simple name} of the target type that owns the scheduled method.
*/
TARGET_TYPE {
@Override
public String asString() {
return "target.type";
}
},
/**
* Name of the method that is executed for the scheduled task.
*/
METHOD_NAME {
@Override
public String asString() {
return "method.name";
}
},
/**
* Name of the exception thrown during task execution, or {@value KeyValue#NONE_VALUE} if no exception was thrown.
*/
EXCEPTION {
@Override
public String asString() {
return "exception";
}
},
/**
* Outcome of the scheduled task execution.
*/
OUTCOME {
@Override
public String asString() {
return "outcome";
}
}
}
}

View File

@@ -28,6 +28,8 @@ import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
@@ -53,6 +55,7 @@ import org.springframework.util.CollectionUtils;
* @author Tobias Montagna-Hay
* @author Sam Brannen
* @author Arjen Poutsma
* @author Brian Clozel
* @since 3.0
* @see org.springframework.scheduling.annotation.EnableAsync
* @see org.springframework.scheduling.annotation.SchedulingConfigurer
@@ -77,6 +80,9 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
@Nullable
private ScheduledExecutorService localExecutor;
@Nullable
private ObservationRegistry observationRegistry;
@Nullable
private List<TriggerTask> triggerTasks;
@@ -130,6 +136,22 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
return this.taskScheduler;
}
/**
* Return the {@link ObservationRegistry} for this registrar.
* @since 6.1.0
*/
@Nullable
public ObservationRegistry getObservationRegistry() {
return this.observationRegistry;
}
/**
* Configure an {@link ObservationRegistry} to record observations for scheduled tasks.
* @since 6.1.0
*/
public void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
/**
* Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,15 @@ package org.springframework.scheduling.support;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.function.Supplier;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.scheduling.config.DefaultScheduledTaskObservationConvention;
import org.springframework.scheduling.config.ScheduledTaskObservationContext;
import org.springframework.scheduling.config.ScheduledTaskObservationConvention;
import org.springframework.scheduling.config.ScheduledTaskObservationDocumentation;
import org.springframework.util.ReflectionUtils;
/**
@@ -28,15 +36,33 @@ import org.springframework.util.ReflectionUtils;
* assuming that an error strategy for Runnables is in place.
*
* @author Juergen Hoeller
* @author Brian Clozel
* @since 3.0.6
* @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor
*/
public class ScheduledMethodRunnable implements Runnable {
private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = new DefaultScheduledTaskObservationConvention();
private final Object target;
private final Method method;
private final Supplier<ObservationRegistry> observationRegistrySupplier;
/**
* Create a {@code ScheduledMethodRunnable} for the given target instance,
* calling the specified method.
* @param target the target instance to call the method on
* @param method the target method to call
* @param observationRegistrySupplier a supplier for the observation registry to use
* @since 6.1.0
*/
public ScheduledMethodRunnable(Object target, Method method, Supplier<ObservationRegistry> observationRegistrySupplier) {
this.target = target;
this.method = method;
this.observationRegistrySupplier = observationRegistrySupplier;
}
/**
* Create a {@code ScheduledMethodRunnable} for the given target instance,
@@ -45,8 +71,7 @@ public class ScheduledMethodRunnable implements Runnable {
* @param method the target method to call
*/
public ScheduledMethodRunnable(Object target, Method method) {
this.target = target;
this.method = method;
this(target, method, () -> ObservationRegistry.NOOP);
}
/**
@@ -57,8 +82,7 @@ public class ScheduledMethodRunnable implements Runnable {
* @throws NoSuchMethodException if the specified method does not exist
*/
public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException {
this.target = target;
this.method = target.getClass().getMethod(methodName);
this(target, target.getClass().getMethod(methodName));
}
@@ -79,9 +103,18 @@ public class ScheduledMethodRunnable implements Runnable {
@Override
public void run() {
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(this.target, this.method);
Observation observation = ScheduledTaskObservationDocumentation.TASKS_SCHEDULED_EXECUTION.observation(
null, DEFAULT_CONVENTION,
() -> context, this.observationRegistrySupplier.get());
observation.observe(() -> runInternal(context));
}
private void runInternal(ScheduledTaskObservationContext context) {
try {
ReflectionUtils.makeAccessible(this.method);
this.method.invoke(this.target);
context.setComplete(true);
}
catch (InvocationTargetException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getTargetException());