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:
parent
842569c9e5
commit
09cb844421
|
|
@ -21,11 +21,14 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|
||||||
|===
|
|===
|
||||||
|Observation name |Description
|
|Observation name |Description
|
||||||
|
|
||||||
|xref:integration/observability.adoc#http-client[`"http.client.requests"`]
|
|xref:integration/observability.adoc#observability.http-client[`"http.client.requests"`]
|
||||||
|Time spent for HTTP client exchanges
|
|Time spent for HTTP client exchanges
|
||||||
|
|
||||||
|xref:integration/observability.adoc#http-server[`"http.server.requests"`]
|
|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`]
|
||||||
|Processing time for HTTP server exchanges at the Framework level
|
|Processing time for HTTP server exchanges at the Framework level
|
||||||
|
|
||||||
|
|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`]
|
||||||
|
|Processing time for an execution of a `@Scheduled` task
|
||||||
|===
|
|===
|
||||||
|
|
||||||
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||||
|
|
@ -79,6 +82,31 @@ include-code::./ServerRequestObservationFilter[]
|
||||||
|
|
||||||
You can configure `ObservationFilter` instances on the `ObservationRegistry`.
|
You can configure `ObservationFilter` instances on the `ObservationRegistry`.
|
||||||
|
|
||||||
|
[[observability.tasks-scheduled]]
|
||||||
|
== @Scheduled tasks instrumentation
|
||||||
|
|
||||||
|
An Observation is created for xref:integration/scheduling.adoc#scheduling-enable-annotation-support[each execution of an `@Scheduled` task].
|
||||||
|
Applications need to configure the `ObservationRegistry` on the `ScheduledTaskRegistrar` to enable the recording of observations.
|
||||||
|
This can be done by declaring a `SchedulingConfigurer` bean that sets the observation registry:
|
||||||
|
|
||||||
|
include-code::./ObservationSchedulingConfigurer[]
|
||||||
|
|
||||||
|
It is using the `org.springframework.scheduling.config.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||||
|
You can configure a custom implementation on the `ObservationRegistry` directly.
|
||||||
|
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
|
||||||
|
|
||||||
|
By default, the following `KeyValues` are created:
|
||||||
|
|
||||||
|
.Low cardinality Keys
|
||||||
|
[cols="a,a"]
|
||||||
|
|===
|
||||||
|
|Name | Description
|
||||||
|
|`exception` _(required)_|Name of the exception thrown during the execution, or `KeyValue#NONE_VALUE`} if no exception happened.
|
||||||
|
|`method.name` _(required)_|Name of Java `Method` that is scheduled for execution.
|
||||||
|
|`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution.
|
||||||
|
|`target.type` _(required)_|Simple class name of the bean instance that holds the scheduled method.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[[observability.http-server]]
|
[[observability.http-server]]
|
||||||
== HTTP Server instrumentation
|
== HTTP Server instrumentation
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.docs.integration.observability.tasksscheduled;
|
||||||
|
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||||
|
|
||||||
|
public class ObservationSchedulingConfigurer implements SchedulingConfigurer {
|
||||||
|
|
||||||
|
private final ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
|
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
|
||||||
|
this.observationRegistry = observationRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||||
|
taskRegistrar.setObservationRegistry(this.observationRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ dependencies {
|
||||||
api(project(":spring-beans"))
|
api(project(":spring-beans"))
|
||||||
api(project(":spring-core"))
|
api(project(":spring-core"))
|
||||||
api(project(":spring-expression"))
|
api(project(":spring-expression"))
|
||||||
|
api("io.micrometer:micrometer-observation")
|
||||||
optional(project(":spring-instrument"))
|
optional(project(":spring-instrument"))
|
||||||
optional("jakarta.annotation:jakarta.annotation-api")
|
optional("jakarta.annotation:jakarta.annotation-api")
|
||||||
optional("jakarta.ejb:jakarta.ejb-api")
|
optional("jakarta.ejb:jakarta.ejb-api")
|
||||||
|
|
@ -41,6 +42,8 @@ dependencies {
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||||
testImplementation("io.reactivex.rxjava3:rxjava")
|
testImplementation("io.reactivex.rxjava3:rxjava")
|
||||||
|
testImplementation('io.micrometer:context-propagation')
|
||||||
|
testImplementation("io.micrometer:micrometer-observation-test")
|
||||||
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
|
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
|
||||||
testRuntimeOnly("org.glassfish:jakarta.el")
|
testRuntimeOnly("org.glassfish:jakarta.el")
|
||||||
// Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central)
|
// Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central)
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,10 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
* accordingly. The Runnable can represent either a synchronous method invocation
|
* accordingly. The Runnable can represent either a synchronous method invocation
|
||||||
* (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous
|
* (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous
|
||||||
* one (see {@link #processScheduledAsync(Scheduled, Method, Object)}).
|
* 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) {
|
protected void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -578,6 +582,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
Runnable task;
|
Runnable task;
|
||||||
try {
|
try {
|
||||||
task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
|
task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
|
||||||
|
this.registrar::getObservationRegistry,
|
||||||
this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
|
this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException ex) {
|
catch (IllegalArgumentException ex) {
|
||||||
|
|
@ -598,7 +603,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
protected Runnable createRunnable(Object target, Method method) {
|
protected Runnable createRunnable(Object target, Method method) {
|
||||||
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
|
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
|
||||||
Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
|
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) {
|
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
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.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
|
@ -34,16 +38,22 @@ import org.springframework.core.KotlinDetector;
|
||||||
import org.springframework.core.ReactiveAdapter;
|
import org.springframework.core.ReactiveAdapter;
|
||||||
import org.springframework.core.ReactiveAdapterRegistry;
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.lang.Nullable;
|
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.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import static org.springframework.scheduling.config.ScheduledTaskObservationDocumentation.TASKS_SCHEDULED_EXECUTION;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive
|
* Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive
|
||||||
* cases without a dependency on optional classes.
|
* cases without a dependency on optional classes.
|
||||||
*
|
*
|
||||||
* @author Simon Baslé
|
* @author Simon Baslé
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 6.1
|
* @since 6.1
|
||||||
*/
|
*/
|
||||||
abstract class ScheduledAnnotationReactiveSupport {
|
abstract class ScheduledAnnotationReactiveSupport {
|
||||||
|
|
@ -157,11 +167,12 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
* delay is applied until the next iteration).
|
* delay is applied until the next iteration).
|
||||||
*/
|
*/
|
||||||
static Runnable createSubscriptionRunnable(Method method, Object targetBean, Scheduled scheduled,
|
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()));
|
boolean shouldBlock = (scheduled.fixedDelay() > 0 || StringUtils.hasText(scheduled.fixedDelayString()));
|
||||||
Publisher<?> publisher = getPublisherFor(method, targetBean);
|
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 final Publisher<?> publisher;
|
||||||
|
|
||||||
|
private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = new DefaultScheduledTaskObservationConvention();
|
||||||
|
|
||||||
final boolean shouldBlock;
|
final boolean shouldBlock;
|
||||||
|
|
||||||
private final List<Runnable> subscriptionTrackerRegistry;
|
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.publisher = publisher;
|
||||||
this.shouldBlock = shouldBlock;
|
this.shouldBlock = shouldBlock;
|
||||||
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
|
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
|
||||||
|
this.observationRegistrySupplier = observationRegistrySupplier;
|
||||||
|
this.contextSupplier = contextSupplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
Observation observation = TASKS_SCHEDULED_EXECUTION.observation(null, DEFAULT_CONVENTION,
|
||||||
|
this.contextSupplier, this.observationRegistrySupplier.get());
|
||||||
if (this.shouldBlock) {
|
if (this.shouldBlock) {
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, latch);
|
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation, latch);
|
||||||
this.subscriptionTrackerRegistry.add(subscriber);
|
subscribe(subscriber, observation);
|
||||||
this.publisher.subscribe(subscriber);
|
|
||||||
try {
|
try {
|
||||||
latch.await();
|
latch.await();
|
||||||
}
|
}
|
||||||
|
|
@ -198,8 +219,19 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry);
|
TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation);
|
||||||
|
subscribe(subscriber, observation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void subscribe(TrackingSubscriber subscriber, Observation observation) {
|
||||||
this.subscriptionTrackerRegistry.add(subscriber);
|
this.subscriptionTrackerRegistry.add(subscriber);
|
||||||
|
if (reactorPresent) {
|
||||||
|
Flux.from(this.publisher)
|
||||||
|
.contextWrite(context -> context.put(ObservationThreadLocalAccessor.KEY, observation))
|
||||||
|
.subscribe(subscriber);
|
||||||
|
}
|
||||||
|
else {
|
||||||
this.publisher.subscribe(subscriber);
|
this.publisher.subscribe(subscriber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +247,8 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
|
|
||||||
private final List<Runnable> subscriptionTrackerRegistry;
|
private final List<Runnable> subscriptionTrackerRegistry;
|
||||||
|
|
||||||
|
private final Observation observation;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final CountDownLatch blockingLatch;
|
private final CountDownLatch blockingLatch;
|
||||||
|
|
||||||
|
|
@ -225,12 +259,13 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
@Nullable
|
@Nullable
|
||||||
private Subscription subscription;
|
private Subscription subscription;
|
||||||
|
|
||||||
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry) {
|
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, Observation observation) {
|
||||||
this(subscriptionTrackerRegistry, null);
|
this(subscriptionTrackerRegistry, observation, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, @Nullable CountDownLatch latch) {
|
TrackingSubscriber(List<Runnable> subscriptionTrackerRegistry, Observation observation, @Nullable CountDownLatch latch) {
|
||||||
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
|
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
|
||||||
|
this.observation = observation;
|
||||||
this.blockingLatch = latch;
|
this.blockingLatch = latch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,6 +273,7 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
public void run() {
|
public void run() {
|
||||||
if (this.subscription != null) {
|
if (this.subscription != null) {
|
||||||
this.subscription.cancel();
|
this.subscription.cancel();
|
||||||
|
this.observation.stop();
|
||||||
}
|
}
|
||||||
if (this.blockingLatch != null) {
|
if (this.blockingLatch != null) {
|
||||||
this.blockingLatch.countDown();
|
this.blockingLatch.countDown();
|
||||||
|
|
@ -247,6 +283,7 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(Subscription subscription) {
|
public void onSubscribe(Subscription subscription) {
|
||||||
this.subscription = subscription;
|
this.subscription = subscription;
|
||||||
|
this.observation.start();
|
||||||
subscription.request(Integer.MAX_VALUE);
|
subscription.request(Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +296,8 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
public void onError(Throwable ex) {
|
public void onError(Throwable ex) {
|
||||||
this.subscriptionTrackerRegistry.remove(this);
|
this.subscriptionTrackerRegistry.remove(this);
|
||||||
logger.warn("Unexpected error occurred in scheduled reactive task", ex);
|
logger.warn("Unexpected error occurred in scheduled reactive task", ex);
|
||||||
|
this.observation.error(ex);
|
||||||
|
this.observation.stop();
|
||||||
if (this.blockingLatch != null) {
|
if (this.blockingLatch != null) {
|
||||||
this.blockingLatch.countDown();
|
this.blockingLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +306,10 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
this.subscriptionTrackerRegistry.remove(this);
|
this.subscriptionTrackerRegistry.remove(this);
|
||||||
|
if (this.observation.getContext() instanceof ScheduledTaskObservationContext context) {
|
||||||
|
context.setComplete(true);
|
||||||
|
}
|
||||||
|
this.observation.stop();
|
||||||
if (this.blockingLatch != null) {
|
if (this.blockingLatch != null) {
|
||||||
this.blockingLatch.countDown();
|
this.blockingLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,8 @@ import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
|
||||||
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
|
@ -53,6 +55,7 @@ import org.springframework.util.CollectionUtils;
|
||||||
* @author Tobias Montagna-Hay
|
* @author Tobias Montagna-Hay
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
* @see org.springframework.scheduling.annotation.EnableAsync
|
* @see org.springframework.scheduling.annotation.EnableAsync
|
||||||
* @see org.springframework.scheduling.annotation.SchedulingConfigurer
|
* @see org.springframework.scheduling.annotation.SchedulingConfigurer
|
||||||
|
|
@ -77,6 +80,9 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
|
||||||
@Nullable
|
@Nullable
|
||||||
private ScheduledExecutorService localExecutor;
|
private ScheduledExecutorService localExecutor;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private List<TriggerTask> triggerTasks;
|
private List<TriggerTask> triggerTasks;
|
||||||
|
|
||||||
|
|
@ -130,6 +136,22 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
|
||||||
return this.taskScheduler;
|
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
|
* Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.UndeclaredThrowableException;
|
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;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,15 +36,33 @@ import org.springframework.util.ReflectionUtils;
|
||||||
* assuming that an error strategy for Runnables is in place.
|
* assuming that an error strategy for Runnables is in place.
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 3.0.6
|
* @since 3.0.6
|
||||||
* @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor
|
* @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor
|
||||||
*/
|
*/
|
||||||
public class ScheduledMethodRunnable implements Runnable {
|
public class ScheduledMethodRunnable implements Runnable {
|
||||||
|
|
||||||
|
private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = new DefaultScheduledTaskObservationConvention();
|
||||||
|
|
||||||
private final Object target;
|
private final Object target;
|
||||||
|
|
||||||
private final Method method;
|
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,
|
* 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
|
* @param method the target method to call
|
||||||
*/
|
*/
|
||||||
public ScheduledMethodRunnable(Object target, Method method) {
|
public ScheduledMethodRunnable(Object target, Method method) {
|
||||||
this.target = target;
|
this(target, method, () -> ObservationRegistry.NOOP);
|
||||||
this.method = method;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,8 +82,7 @@ public class ScheduledMethodRunnable implements Runnable {
|
||||||
* @throws NoSuchMethodException if the specified method does not exist
|
* @throws NoSuchMethodException if the specified method does not exist
|
||||||
*/
|
*/
|
||||||
public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException {
|
public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException {
|
||||||
this.target = target;
|
this(target, target.getClass().getMethod(methodName));
|
||||||
this.method = target.getClass().getMethod(methodName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,9 +103,18 @@ public class ScheduledMethodRunnable implements Runnable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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 {
|
try {
|
||||||
ReflectionUtils.makeAccessible(this.method);
|
ReflectionUtils.makeAccessible(this.method);
|
||||||
this.method.invoke(this.target);
|
this.method.invoke(this.target);
|
||||||
|
context.setComplete(true);
|
||||||
}
|
}
|
||||||
catch (InvocationTargetException ex) {
|
catch (InvocationTargetException ex) {
|
||||||
ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
|
ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.micrometer.observation.Observation;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.observability.DefaultSignalListener;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||||
|
import org.springframework.context.support.StaticApplicationContext;
|
||||||
|
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTask;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskHolder;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskObservationContext;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observability tests for {@link ScheduledAnnotationBeanPostProcessor}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class ScheduledAnnotationBeanPostProcessorObservabilityTests {
|
||||||
|
|
||||||
|
private final StaticApplicationContext context = new StaticApplicationContext();
|
||||||
|
|
||||||
|
private final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
|
||||||
|
|
||||||
|
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void closeContext() {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordSuccessObservationsForTasks() throws Exception {
|
||||||
|
registerScheduledBean(FixedDelayBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "fixedDelay")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "FixedDelayBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordFailureObservationsForTasksThrowing() throws Exception {
|
||||||
|
registerScheduledBean(FixedDelayErrorBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "error")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "FixedDelayErrorBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "IllegalStateException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordSuccessObservationsForReactiveTasks() throws Exception {
|
||||||
|
registerScheduledBean(FixedDelayReactiveBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "fixedDelay")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "FixedDelayReactiveBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordFailureObservationsForReactiveTasksThrowing() throws Exception {
|
||||||
|
registerScheduledBean(FixedDelayReactiveErrorBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "error")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "FixedDelayReactiveErrorBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "IllegalStateException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordCancelledObservationsForTasks() throws Exception {
|
||||||
|
registerScheduledBean(CancelledTaskBean.class);
|
||||||
|
ScheduledTask scheduledTask = getScheduledTask();
|
||||||
|
this.taskExecutor.execute(scheduledTask.getTask().getRunnable());
|
||||||
|
context.getBean(TaskTester.class).await();
|
||||||
|
scheduledTask.cancel();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "cancelled")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "CancelledTaskBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordCancelledObservationsForReactiveTasks() throws Exception {
|
||||||
|
registerScheduledBean(CancelledReactiveTaskBean.class);
|
||||||
|
ScheduledTask scheduledTask = getScheduledTask();
|
||||||
|
this.taskExecutor.execute(scheduledTask.getTask().getRunnable());
|
||||||
|
context.getBean(TaskTester.class).await();
|
||||||
|
scheduledTask.cancel();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "cancelled")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "CancelledReactiveTaskBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHaveCurrentObservationInScope() throws Exception {
|
||||||
|
registerScheduledBean(CurrentObservationBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "hasCurrentObservation")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "CurrentObservationBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHaveCurrentObservationInReactiveScope() throws Exception {
|
||||||
|
registerScheduledBean(CurrentObservationReactiveBean.class);
|
||||||
|
runScheduledTaskAndAwait();
|
||||||
|
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS")
|
||||||
|
.hasLowCardinalityKeyValue("method.name", "hasCurrentObservation")
|
||||||
|
.hasLowCardinalityKeyValue("target.type", "CurrentObservationReactiveBean")
|
||||||
|
.hasLowCardinalityKeyValue("exception", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void registerScheduledBean(Class<?> beanClass) {
|
||||||
|
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
|
||||||
|
BeanDefinition targetDefinition = new RootBeanDefinition(beanClass);
|
||||||
|
targetDefinition.getPropertyValues().add("observationRegistry", this.observationRegistry);
|
||||||
|
context.registerBeanDefinition("postProcessor", processorDefinition);
|
||||||
|
context.registerBeanDefinition("target", targetDefinition);
|
||||||
|
context.registerBean("schedulingConfigurer", SchedulingConfigurer.class, () -> {
|
||||||
|
return new SchedulingConfigurer() {
|
||||||
|
@Override
|
||||||
|
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||||
|
taskRegistrar.setObservationRegistry(observationRegistry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
context.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduledTask getScheduledTask() {
|
||||||
|
ScheduledTaskHolder taskHolder = context.getBean("postProcessor", ScheduledTaskHolder.class);
|
||||||
|
return taskHolder.getScheduledTasks().iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runScheduledTaskAndAwait() throws InterruptedException {
|
||||||
|
ScheduledTask scheduledTask = getScheduledTask();
|
||||||
|
try {
|
||||||
|
scheduledTask.getTask().getRunnable().run();
|
||||||
|
}
|
||||||
|
catch (Throwable exc) {
|
||||||
|
// ignore exceptions thrown by test tasks
|
||||||
|
}
|
||||||
|
context.getBean(TaskTester.class).await();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatTaskObservation() {
|
||||||
|
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||||
|
.hasObservationWithNameEqualTo("tasks.scheduled.execution").that();
|
||||||
|
}
|
||||||
|
|
||||||
|
static abstract class TaskTester {
|
||||||
|
|
||||||
|
ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
public void setObservationRegistry(ObservationRegistry observationRegistry) {
|
||||||
|
this.observationRegistry = observationRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void await() throws InterruptedException {
|
||||||
|
this.latch.await(3, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class FixedDelayBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public void fixedDelay() {
|
||||||
|
this.latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class FixedDelayErrorBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public void error() {
|
||||||
|
this.latch.countDown();
|
||||||
|
throw new IllegalStateException("test error");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class FixedDelayReactiveBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public Mono<Object> fixedDelay() {
|
||||||
|
return Mono.empty().doOnTerminate(() -> this.latch.countDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class FixedDelayReactiveErrorBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public Mono<Object> error() {
|
||||||
|
return Mono.error(new IllegalStateException("test error"))
|
||||||
|
.doOnTerminate(() -> this.latch.countDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CancelledTaskBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public void cancelled() {
|
||||||
|
this.latch.countDown();
|
||||||
|
try {
|
||||||
|
Thread.sleep(5000);
|
||||||
|
}
|
||||||
|
catch (InterruptedException exc) {
|
||||||
|
// ignore cancelled task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CancelledReactiveTaskBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public Flux<Long> cancelled() {
|
||||||
|
return Flux.interval(Duration.ZERO, Duration.ofSeconds(1))
|
||||||
|
.doOnNext(el -> this.latch.countDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CurrentObservationBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public void hasCurrentObservation() {
|
||||||
|
assertThat(this.observationRegistry.getCurrentObservation()).isNotNull();
|
||||||
|
assertThat(this.observationRegistry.getCurrentObservation().getContext()).isInstanceOf(ScheduledTaskObservationContext.class);
|
||||||
|
this.latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CurrentObservationReactiveBean extends TaskTester {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
|
||||||
|
public Mono<String> hasCurrentObservation() {
|
||||||
|
return Mono.just("test")
|
||||||
|
.tap(() -> new DefaultSignalListener<String>() {
|
||||||
|
@Override
|
||||||
|
public void doFirst() throws Throwable {
|
||||||
|
Observation observation = observationRegistry.getCurrentObservation();
|
||||||
|
assertThat(observation).isNotNull();
|
||||||
|
assertThat(observation.getContext()).isInstanceOf(ScheduledTaskObservationContext.class);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnTerminate(() -> this.latch.countDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -42,6 +43,7 @@ import static org.springframework.scheduling.annotation.ScheduledAnnotationReact
|
||||||
import static org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.isReactive;
|
import static org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.isReactive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Tests for {@link ScheduledAnnotationReactiveSupportTests}.
|
||||||
* @author Simon Baslé
|
* @author Simon Baslé
|
||||||
* @since 6.1
|
* @since 6.1
|
||||||
*/
|
*/
|
||||||
|
|
@ -116,12 +118,12 @@ class ScheduledAnnotationReactiveSupportTests {
|
||||||
Scheduled fixedDelayLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedDelay", 123L), Scheduled.class, null);
|
Scheduled fixedDelayLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedDelay", 123L), Scheduled.class, null);
|
||||||
List<Runnable> tracker = new ArrayList<>();
|
List<Runnable> tracker = new ArrayList<>();
|
||||||
|
|
||||||
assertThat(createSubscriptionRunnable(m, target, fixedDelayString, tracker))
|
assertThat(createSubscriptionRunnable(m, target, fixedDelayString, () -> ObservationRegistry.NOOP, tracker))
|
||||||
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
||||||
assertThat(sr.shouldBlock).as("fixedDelayString.shouldBlock").isTrue()
|
assertThat(sr.shouldBlock).as("fixedDelayString.shouldBlock").isTrue()
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(createSubscriptionRunnable(m, target, fixedDelayLong, tracker))
|
assertThat(createSubscriptionRunnable(m, target, fixedDelayLong, () -> ObservationRegistry.NOOP, tracker))
|
||||||
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
||||||
assertThat(sr.shouldBlock).as("fixedDelayLong.shouldBlock").isTrue()
|
assertThat(sr.shouldBlock).as("fixedDelayLong.shouldBlock").isTrue()
|
||||||
);
|
);
|
||||||
|
|
@ -135,12 +137,12 @@ class ScheduledAnnotationReactiveSupportTests {
|
||||||
Scheduled fixedRateLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedRate", 123L), Scheduled.class, null);
|
Scheduled fixedRateLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedRate", 123L), Scheduled.class, null);
|
||||||
List<Runnable> tracker = new ArrayList<>();
|
List<Runnable> tracker = new ArrayList<>();
|
||||||
|
|
||||||
assertThat(createSubscriptionRunnable(m, target, fixedRateString, tracker))
|
assertThat(createSubscriptionRunnable(m, target, fixedRateString, () -> ObservationRegistry.NOOP, tracker))
|
||||||
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
||||||
assertThat(sr.shouldBlock).as("fixedRateString.shouldBlock").isFalse()
|
assertThat(sr.shouldBlock).as("fixedRateString.shouldBlock").isFalse()
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(createSubscriptionRunnable(m, target, fixedRateLong, tracker))
|
assertThat(createSubscriptionRunnable(m, target, fixedRateLong, () -> ObservationRegistry.NOOP, tracker))
|
||||||
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
||||||
assertThat(sr.shouldBlock).as("fixedRateLong.shouldBlock").isFalse()
|
assertThat(sr.shouldBlock).as("fixedRateLong.shouldBlock").isFalse()
|
||||||
);
|
);
|
||||||
|
|
@ -153,7 +155,7 @@ class ScheduledAnnotationReactiveSupportTests {
|
||||||
Scheduled cron = AnnotationUtils.synthesizeAnnotation(Map.of("cron", "-"), Scheduled.class, null);
|
Scheduled cron = AnnotationUtils.synthesizeAnnotation(Map.of("cron", "-"), Scheduled.class, null);
|
||||||
List<Runnable> tracker = new ArrayList<>();
|
List<Runnable> tracker = new ArrayList<>();
|
||||||
|
|
||||||
assertThat(createSubscriptionRunnable(m, target, cron, tracker))
|
assertThat(createSubscriptionRunnable(m, target, cron, () -> ObservationRegistry.NOOP, tracker))
|
||||||
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
.isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr ->
|
||||||
assertThat(sr.shouldBlock).as("cron.shouldBlock").isFalse()
|
assertThat(sr.shouldBlock).as("cron.shouldBlock").isFalse()
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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.common.KeyValue;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.aop.framework.ProxyFactory;
|
||||||
|
import org.springframework.aop.target.SingletonTargetSource;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DefaultScheduledTaskObservationConvention}.
|
||||||
|
*/
|
||||||
|
class DefaultScheduledTaskObservationConventionTests {
|
||||||
|
|
||||||
|
private final Method taskMethod = ClassUtils.getMethod(BeanWithScheduledMethods.class, "process");
|
||||||
|
|
||||||
|
private final ScheduledTaskObservationConvention convention = new DefaultScheduledTaskObservationConvention();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveDefaultName() {
|
||||||
|
assertThat(convention.getName()).isEqualTo("tasks.scheduled.execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveContextualName() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveContextualNameForProxiedClass() {
|
||||||
|
Object proxy = ProxyFactory.getProxy(new SingletonTargetSource(new BeanWithScheduledMethods()));
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(proxy, taskMethod);
|
||||||
|
assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveTargetType() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("target.type", "BeanWithScheduledMethods"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveMethodName() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("method.name", "process"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveSuccessfulOutcome() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
context.setComplete(true);
|
||||||
|
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "SUCCESS"),
|
||||||
|
KeyValue.of("exception", "none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveErrorOutcome() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
context.setError(new IllegalStateException("test error"));
|
||||||
|
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "ERROR"),
|
||||||
|
KeyValue.of("exception", "IllegalStateException"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observationShouldHaveUnknownOutcome() {
|
||||||
|
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);
|
||||||
|
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "UNKNOWN"),
|
||||||
|
KeyValue.of("exception", "none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class BeanWithScheduledMethods implements TaskProcessor {
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskProcessor {
|
||||||
|
|
||||||
|
void process();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue