diff --git a/spring-cloud-task-core/pom.xml b/spring-cloud-task-core/pom.xml
index 14c535df..4d77103d 100755
--- a/spring-cloud-task-core/pom.xml
+++ b/spring-cloud-task-core/pom.xml
@@ -109,10 +109,19 @@
spring-boot-autoconfigure-processor
true
+
+ org.springframework.boot
+ spring-boot-actuator-autoconfigure
+ true
+
io.micrometer
micrometer-core
+
+ io.micrometer
+ micrometer-observation
+
org.junit.jupiter
junit-jupiter-params
@@ -129,6 +138,41 @@
${jmock-version}
test
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
+
+ io.micrometer
+ micrometer-test
+ test
+
+
+ io.micrometer
+ micrometer-tracing-test
+ test
+
+
+ io.micrometer
+ micrometer-tracing-bridge-brave
+ test
+
+
+ io.zipkin.brave
+ brave-tests
+ test
+
+
+ io.zipkin.reporter2
+ zipkin-reporter-brave
+ test
+
+
+ io.zipkin.reporter2
+ zipkin-sender-urlconnection
+ test
+
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/DefaultTaskKeyValuesProvider.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/DefaultTaskKeyValuesProvider.java
new file mode 100644
index 00000000..76b3385b
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/DefaultTaskKeyValuesProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.common.KeyValues;
+import io.micrometer.observation.Observation;
+
+/**
+ * {@link Observation.KeyValuesProvider} for Spring Cloud Task.
+ *
+ * @author Marcin Grzejszczak
+ * @since 3.0.0
+ */
+public class DefaultTaskKeyValuesProvider implements TaskKeyValuesProvider {
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(TaskObservationContext context) {
+ return KeyValues.of(TaskDocumentedObservation.TaskRunnerTags.BEAN_NAME.of(context.getBeanName()));
+ }
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationTaskAutoConfiguration.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationTaskAutoConfiguration.java
new file mode 100644
index 00000000..04ec7403
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationTaskAutoConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
+ * Auto-configuration} that registers instrumentation for Spring Cloud Task.
+ *
+ * @author Marcin Grzejszczak
+ * @since 3.0.0
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(ObservationRegistry.class)
+@ConditionalOnProperty(value = "spring.cloud.task.observation.enabled", matchIfMissing = true)
+@ConditionalOnBean(ObservationRegistry.class)
+@AutoConfigureAfter(ObservationAutoConfiguration.class)
+public class ObservationTaskAutoConfiguration {
+
+ @Bean
+ ObservedTaskExecutionListener traceTaskExecutionListener(ObservationRegistry registry,
+ @Value("${spring.application.name:default}") String appName) {
+ return new ObservedTaskExecutionListener(registry, appName);
+ }
+
+ @Bean
+ static ObservedCommandLineRunnerBeanPostProcessor observedCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) {
+ return new ObservedCommandLineRunnerBeanPostProcessor(beanFactory);
+ }
+
+ @Bean
+ static ObservedApplicationRunnerBeanPostProcessor observedApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) {
+ return new ObservedApplicationRunnerBeanPostProcessor(beanFactory);
+ }
+
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunner.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunner.java
new file mode 100644
index 00000000..4fd6dc41
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunner.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+
+/**
+ * Observed representation of a {@link ApplicationRunner}.
+ *
+ * @author Marcin Grzejszczak
+ */
+class ObservedApplicationRunner implements ApplicationRunner, Observation.KeyValuesProviderAware {
+
+ private final BeanFactory beanFactory;
+
+ private final ApplicationRunner delegate;
+
+ private final String beanName;
+
+ private ObservationRegistry registry;
+
+ private TaskKeyValuesProvider keyValuesProvider = new DefaultTaskKeyValuesProvider();
+
+ ObservedApplicationRunner(BeanFactory beanFactory, ApplicationRunner delegate, String beanName) {
+ this.beanFactory = beanFactory;
+ this.delegate = delegate;
+ this.beanName = beanName;
+ }
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ TaskObservationContext context = new TaskObservationContext(this.beanName);
+ Observation observation = TaskDocumentedObservation.TASK_RUNNER_OBSERVATION.observation(registry(), context)
+ .contextualName(this.beanName)
+ .keyValuesProvider(this.keyValuesProvider);
+ try (Observation.Scope scope = observation.start().openScope()) {
+ this.delegate.run(args);
+ }
+ catch (Exception error) {
+ observation.error(error);
+ throw error;
+ }
+ finally {
+ observation.stop();
+ }
+ }
+
+ private ObservationRegistry registry() {
+ if (this.registry == null) {
+ this.registry = this.beanFactory.getBean(ObservationRegistry.class);
+ }
+ return this.registry;
+ }
+
+ @Override
+ public void setKeyValuesProvider(TaskKeyValuesProvider keyValuesProvider) {
+ this.keyValuesProvider = keyValuesProvider;
+ }
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunnerBeanPostProcessor.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunnerBeanPostProcessor.java
new file mode 100644
index 00000000..bbdfb760
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedApplicationRunnerBeanPostProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.ApplicationRunner;
+
+/**
+ * Registers beans related to task scheduling.
+ *
+ * @author Marcin Grzejszczak
+ */
+class ObservedApplicationRunnerBeanPostProcessor implements BeanPostProcessor {
+
+ private final BeanFactory beanFactory;
+
+ ObservedApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof ApplicationRunner applicationRunner && !(bean instanceof ObservedApplicationRunner)) {
+ return new ObservedApplicationRunner(this.beanFactory, applicationRunner, beanName);
+ }
+ return bean;
+ }
+
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunner.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunner.java
new file mode 100644
index 00000000..868ef496
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunner.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.boot.CommandLineRunner;
+
+/**
+ * Observed representation of a {@link CommandLineRunner}.
+ *
+ * @author Marcin Grzejszczak
+ */
+class ObservedCommandLineRunner implements CommandLineRunner, Observation.KeyValuesProviderAware {
+
+ private final BeanFactory beanFactory;
+
+ private final CommandLineRunner delegate;
+
+ private final String beanName;
+
+ private ObservationRegistry registry;
+
+ private TaskKeyValuesProvider keyValuesProvider = new DefaultTaskKeyValuesProvider();
+
+ ObservedCommandLineRunner(BeanFactory beanFactory, CommandLineRunner delegate, String beanName) {
+ this.beanFactory = beanFactory;
+ this.delegate = delegate;
+ this.beanName = beanName;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ TaskObservationContext context = new TaskObservationContext(this.beanName);
+ Observation observation = TaskDocumentedObservation.TASK_RUNNER_OBSERVATION.observation(registry(), context)
+ .contextualName(this.beanName)
+ .keyValuesProvider(this.keyValuesProvider);
+ try (Observation.Scope scope = observation.start().openScope()) {
+ this.delegate.run(args);
+ }
+ catch (Exception error) {
+ observation.error(error);
+ throw error;
+ }
+ finally {
+ observation.stop();
+ }
+ }
+
+ private ObservationRegistry registry() {
+ if (this.registry == null) {
+ this.registry = this.beanFactory.getBean(ObservationRegistry.class);
+ }
+ return this.registry;
+ }
+
+ @Override
+ public void setKeyValuesProvider(TaskKeyValuesProvider keyValuesProvider) {
+ this.keyValuesProvider = keyValuesProvider;
+ }
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunnerBeanPostProcessor.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunnerBeanPostProcessor.java
new file mode 100644
index 00000000..0f5c5a03
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedCommandLineRunnerBeanPostProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.CommandLineRunner;
+
+/**
+ * Registers beans related to task scheduling.
+ *
+ * @author Marcin Grzejszczak
+ */
+class ObservedCommandLineRunnerBeanPostProcessor implements BeanPostProcessor {
+
+ private final BeanFactory beanFactory;
+
+ ObservedCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof CommandLineRunner commandLineRunner && !(bean instanceof ObservedCommandLineRunner)) {
+ return new ObservedCommandLineRunner(this.beanFactory, commandLineRunner, beanName);
+ }
+ return bean;
+ }
+
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListener.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListener.java
new file mode 100644
index 00000000..369acf68
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListener.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.cloud.task.listener.TaskExecutionListener;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.core.Ordered;
+
+/**
+ * Sets the span upon starting and closes it upon ending a task.
+ *
+ * @author Marcin Grzejszczak
+ * @since 3.1.0
+ */
+class ObservedTaskExecutionListener implements TaskExecutionListener, Ordered {
+
+ private static final Log log = LogFactory.getLog(ObservedTaskExecutionListener.class);
+
+ private final ObservationRegistry registry;
+
+ private final String projectName;
+
+ ObservedTaskExecutionListener(ObservationRegistry registry, String projectName) {
+ this.registry = registry;
+ this.projectName = projectName;
+ }
+
+ @Override
+ public void onTaskStartup(TaskExecution taskExecution) {
+ Observation observation = TaskDocumentedObservation.TASK_EXECUTION_LISTENER_OBSERVATION.observation(this.registry)
+ .contextualName(this.projectName)
+ .start();
+ observation.openScope();
+ if (log.isDebugEnabled()) {
+ log.debug("Put the observation [" + observation + "] in scope");
+ }
+ }
+
+ @Override
+ public void onTaskEnd(TaskExecution taskExecution) {
+ Observation.Scope scope = this.registry.getCurrentObservationScope();
+ if (scope != null) {
+ scope.close();
+ scope.getCurrentObservation().stop();
+ if (log.isDebugEnabled()) {
+ log.debug("Removed the [" + scope.getCurrentObservation() + "] from thread local");
+ }
+ }
+ }
+
+ @Override
+ public void onTaskFailed(TaskExecution taskExecution, Throwable throwable) {
+ Observation.Scope scope = this.registry.getCurrentObservationScope();
+ if (scope != null) {
+ Observation observation = scope.getCurrentObservation();
+ observation.error(throwable);
+ onTaskEnd(taskExecution);
+ }
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskDocumentedObservation.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskDocumentedObservation.java
new file mode 100644
index 00000000..167c8215
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskDocumentedObservation.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.docs.DocumentedObservation;
+
+enum TaskDocumentedObservation implements DocumentedObservation {
+
+ /**
+ * Observation created when a task runner is executed.
+ */
+ TASK_RUNNER_OBSERVATION {
+ @Override
+ public String getName() {
+ return "spring.cloud.task.runner";
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return TaskRunnerTags.values();
+ }
+
+ @Override
+ public String getPrefix() {
+ return "spring.cloud.task";
+ }
+ },
+
+ /**
+ * Observation created within the lifecycle of a task.
+ */
+ TASK_EXECUTION_LISTENER_OBSERVATION {
+ @Override
+ public String getName() {
+ return "spring.cloud.task.execution";
+ }
+
+
+ @Override
+ public String getPrefix() {
+ return "spring.cloud.task";
+ }
+ };
+
+ /**
+ * Key names for Spring Cloud Task Command / Application runners.
+ */
+ enum TaskRunnerTags implements KeyName {
+
+ /**
+ * Name of the bean that was executed by Spring Cloud Task.
+ */
+ BEAN_NAME {
+ @Override
+ public String getKeyName() {
+ return "spring.cloud.task.runner.bean-name";
+ }
+ }
+ }
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskKeyValuesProvider.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskKeyValuesProvider.java
new file mode 100644
index 00000000..08b107a5
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskKeyValuesProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.Observation;
+
+/**
+ * {@link Observation.KeyValuesProvider} for Spring Cloud Task.
+ *
+ * @author Marcin Grzejszczak
+ * @since 3.0.0
+ */
+public interface TaskKeyValuesProvider extends Observation.KeyValuesProvider {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof TaskObservationContext;
+ }
+}
diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskObservationContext.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskObservationContext.java
new file mode 100644
index 00000000..7b2bd8ec
--- /dev/null
+++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskObservationContext.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2013-2021 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.cloud.task.configuration.observation;
+
+import io.micrometer.observation.Observation;
+
+/**
+ * {@link Observation.Context} for Spring Cloud Task.
+ *
+ * @author Marcin Grzejszczak
+ * @since 3.0.0
+ */
+public class TaskObservationContext extends Observation.Context {
+
+ private final String beanName;
+
+ public TaskObservationContext(String beanName) {
+ this.beanName = beanName;
+ }
+
+ public String getBeanName() {
+ return beanName;
+ }
+}
diff --git a/spring-cloud-task-core/src/main/resources/META-INF/spring.factories b/spring-cloud-task-core/src/main/resources/META-INF/spring.factories
index e0401a79..76500f5a 100644
--- a/spring-cloud-task-core/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-task-core/src/main/resources/META-INF/spring.factories
@@ -1,5 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.task.configuration.SingleTaskConfiguration,\
org.springframework.cloud.task.configuration.SimpleTaskAutoConfiguration,\
-org.springframework.cloud.task.configuration.MetricsAutoConfiguration\
+org.springframework.cloud.task.configuration.MetricsAutoConfiguration,\
+org.springframework.cloud.task.configuration.observation.ObservationTaskAutoConfiguration
diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservationIntegrationTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservationIntegrationTests.java
new file mode 100644
index 00000000..9d2cc6f3
--- /dev/null
+++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservationIntegrationTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017-2022 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.cloud.task.configuration.observation;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import brave.handler.SpanHandler;
+import brave.sampler.Sampler;
+import brave.test.TestSpanHandler;
+import io.micrometer.common.KeyValues;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.tck.MeterRegistryAssert;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.brave.bridge.BraveFinishedSpan;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.test.simple.SpansAssert;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import zipkin2.Span;
+import zipkin2.reporter.Reporter;
+import zipkin2.reporter.brave.ZipkinSpanHandler;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@SpringBootTest(classes = ObservationIntegrationTests.Config.class)
+class ObservationIntegrationTests {
+
+ @Autowired
+ TestSpanHandler testSpanHandler;
+
+ @Autowired
+ MeterRegistry meterRegistry;
+
+ @Test
+ void testSuccessfulObservation() {
+ List finishedSpans = finishedSpans();
+
+ SpansAssert.then(finishedSpans)
+ .thenASpanWithNameEqualTo("my-command-line-runner")
+ .hasTag("spring.cloud.task.runner.bean-name", "myCommandLineRunner")
+ .backToSpans()
+ .thenASpanWithNameEqualTo("my-application-runner")
+ .hasTag("spring.cloud.task.runner.bean-name", "myApplicationRunner");
+ MeterRegistryAssert.then(this.meterRegistry)
+ .hasTimerWithNameAndTags("spring.cloud.task.runner", KeyValues.of("spring.cloud.task.runner.bean-name", "myCommandLineRunner"))
+ .hasTimerWithNameAndTags("spring.cloud.task.runner", KeyValues.of("spring.cloud.task.runner.bean-name", "myApplicationRunner"));
+ }
+
+ private List finishedSpans() {
+ return this.testSpanHandler.spans().stream().map(BraveFinishedSpan::fromBrave).collect(Collectors.toList());
+ }
+
+ @Configuration
+ @ImportAutoConfiguration({ObservationAutoConfiguration.class, ObservationTaskAutoConfiguration.class, BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class, MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, ZipkinAutoConfiguration.class})
+ static class Config {
+ private static final Logger log = LoggerFactory.getLogger(Config.class);
+
+ @Bean
+ TestSpanHandler testSpanHandler() {
+ return new TestSpanHandler();
+ }
+
+ @Bean
+ SpanHandler zipkinSpanHandler(Reporter spanReporter) {
+ return ZipkinSpanHandler.newBuilder(spanReporter).build();
+ }
+
+ @Bean
+ Sampler sampler() {
+ return Sampler.ALWAYS_SAMPLE;
+ }
+
+ @Bean
+ CommandLineRunner myCommandLineRunner(Tracer tracer) {
+ return args -> log.info(" Hello from command line runner", tracer.currentSpan().context().traceId());
+ }
+
+ @Bean
+ ApplicationRunner myApplicationRunner(Tracer tracer) {
+ return args -> log.info(" Hello from application runner", tracer.currentSpan().context().traceId());
+ }
+ }
+}
diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListenerTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListenerTests.java
new file mode 100644
index 00000000..d8a90865
--- /dev/null
+++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservedTaskExecutionListenerTests.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017-2022 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.cloud.task.configuration.observation;
+
+import java.util.Date;
+import java.util.List;
+
+import io.micrometer.core.tck.TestObservationRegistry;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.task.repository.TaskExecution;
+
+import static io.micrometer.core.tck.TestObservationRegistryAssert.then;
+
+class ObservedTaskExecutionListenerTests {
+
+ @Test
+ void testSuccessfulObservation() {
+ TestObservationRegistry registry = TestObservationRegistry.create();
+ ObservedTaskExecutionListener listener = new ObservedTaskExecutionListener(registry, "my-project");
+ TaskExecution taskExecution = taskExecution();
+
+ listener.onTaskStartup(taskExecution);
+ listener.onTaskEnd(taskExecution);
+
+ then(registry)
+ .hasSingleObservationThat()
+ .hasNameEqualTo("spring.cloud.task.execution")
+ .hasContextualNameEqualTo("my-project");
+ }
+
+ @Test
+ void testErrorObservation() {
+ TestObservationRegistry registry = TestObservationRegistry.create();
+ ObservedTaskExecutionListener listener = new ObservedTaskExecutionListener(registry, "my-project");
+ TaskExecution taskExecution = taskExecution();
+
+ listener.onTaskStartup(taskExecution);
+ listener.onTaskFailed(taskExecution, new RuntimeException("error"));
+
+ then(registry)
+ .hasSingleObservationThat()
+ .hasNameEqualTo("spring.cloud.task.execution")
+ .hasContextualNameEqualTo("my-project")
+ .thenThrowable()
+ .hasMessage("error");
+ }
+
+ private TaskExecution taskExecution() {
+ return new TaskExecution(1L, 1, "task", new Date(), new Date(), "bye", List.of("arg"), "boom", "id");
+ }
+}