From 3817fe14e752d6bfd8a3e44c2d92e3c209cd6242 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 11 May 2022 11:22:01 +0200 Subject: [PATCH] Support Observations for CommandLineRunner and ApplicationRunner First pass on observation Added automatic docs generation Renamed Observed -> Observation Fixed copyright dates Removed execution task listener - it makes runner spans share the same trace id Updated version for micrometer docs to milestone --- docs/pom.xml | 58 +++++++++ docs/src/main/asciidoc/_observability.adoc | 8 ++ docs/src/main/asciidoc/appendix.adoc | 3 + spring-cloud-task-core/pom.xml | 44 +++++++ .../DefaultTaskKeyValuesProvider.java | 34 ++++++ .../ObservationApplicationRunner.java | 78 ++++++++++++ ...ionApplicationRunnerBeanPostProcessor.java | 45 +++++++ .../ObservationCommandLineRunner.java | 77 ++++++++++++ ...ionCommandLineRunnerBeanPostProcessor.java | 45 +++++++ .../ObservationTaskAutoConfiguration.java | 54 +++++++++ .../TaskDocumentedObservation.java | 59 +++++++++ .../observation/TaskKeyValuesProvider.java | 33 +++++ .../observation/TaskObservationContext.java | 38 ++++++ .../main/resources/META-INF/spring.factories | 3 +- .../ObservationIntegrationTests.java | 114 ++++++++++++++++++ src/checkstyle/checkstyle-suppressions.xml | 7 ++ 16 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/asciidoc/_observability.adoc create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/DefaultTaskKeyValuesProvider.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunner.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunnerBeanPostProcessor.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunner.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunnerBeanPostProcessor.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationTaskAutoConfiguration.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskDocumentedObservation.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskKeyValuesProvider.java create mode 100644 spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskObservationContext.java create mode 100644 spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservationIntegrationTests.java create mode 100644 src/checkstyle/checkstyle-suppressions.xml diff --git a/docs/pom.xml b/docs/pom.xml index 28627635..b38a7b7d 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -15,6 +15,12 @@ spring.cloud.task.* deploy 1.5.0-alpha.16 + + + 1.0.0-M4 + ${maven.multiModuleProjectDirectory}/spring-cloud-task-core/ + .* + ${maven.multiModuleProjectDirectory}/target/ @@ -49,6 +55,58 @@ org.codehaus.mojo exec-maven-plugin + + + generate-metrics-metadata + prepare-package + + java + + + io.micrometer.docs.metrics.DocsFromSources + true + + ${micrometer-docs-generator.inputPath} + ${micrometer-docs-generator.inclusionPattern} + ${micrometer-docs-generator.outputPath} + + + + + generate-tracing-metadata + prepare-package + + java + + + io.micrometer.docs.spans.DocsFromSources + true + + ${micrometer-docs-generator.inputPath} + ${micrometer-docs-generator.inclusionPattern} + ${micrometer-docs-generator.outputPath} + + + + + + + io.micrometer + + micrometer-docs-generator-spans + ${micrometer-docs-generator.version} + + jar + + + io.micrometer + + micrometer-docs-generator-metrics + ${micrometer-docs-generator.version} + + jar + + org.asciidoctor diff --git a/docs/src/main/asciidoc/_observability.adoc b/docs/src/main/asciidoc/_observability.adoc new file mode 100644 index 00000000..f31372d3 --- /dev/null +++ b/docs/src/main/asciidoc/_observability.adoc @@ -0,0 +1,8 @@ +:root-target: ../../../../target/ + +[[observability]] +== Observability metadata + +include::{root-target}_metrics.adoc[] + +include::{root-target}_spans.adoc[] diff --git a/docs/src/main/asciidoc/appendix.adoc b/docs/src/main/asciidoc/appendix.adoc index 34185ba3..5dee4c42 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/src/main/asciidoc/appendix.adoc @@ -3,4 +3,7 @@ = Appendices include::appendix-task-repository-schema.adoc[] + include::appendix-building-the-documentation.adoc[] + +include::_observability.adoc[] 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..d37a75ab --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/DefaultTaskKeyValuesProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-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 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/ObservationApplicationRunner.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunner.java new file mode 100644 index 00000000..b143912f --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunner.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018-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 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 ObservationApplicationRunner implements ApplicationRunner, Observation.KeyValuesProviderAware { + + private final BeanFactory beanFactory; + + private final ApplicationRunner delegate; + + private final String beanName; + + private ObservationRegistry registry; + + private TaskKeyValuesProvider keyValuesProvider = new DefaultTaskKeyValuesProvider(); + + ObservationApplicationRunner(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/ObservationApplicationRunnerBeanPostProcessor.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunnerBeanPostProcessor.java new file mode 100644 index 00000000..eee9ab1e --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationApplicationRunnerBeanPostProcessor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-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 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 ObservationApplicationRunnerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + ObservationApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ApplicationRunner applicationRunner && !(bean instanceof ObservationApplicationRunner)) { + return new ObservationApplicationRunner(this.beanFactory, applicationRunner, beanName); + } + return bean; + } + +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunner.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunner.java new file mode 100644 index 00000000..14801f69 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunner.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018-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 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 ObservationCommandLineRunner implements CommandLineRunner, Observation.KeyValuesProviderAware { + + private final BeanFactory beanFactory; + + private final CommandLineRunner delegate; + + private final String beanName; + + private ObservationRegistry registry; + + private TaskKeyValuesProvider keyValuesProvider = new DefaultTaskKeyValuesProvider(); + + ObservationCommandLineRunner(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/ObservationCommandLineRunnerBeanPostProcessor.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunnerBeanPostProcessor.java new file mode 100644 index 00000000..93350416 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationCommandLineRunnerBeanPostProcessor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-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 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 ObservationCommandLineRunnerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + ObservationCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof CommandLineRunner commandLineRunner && !(bean instanceof ObservationCommandLineRunner)) { + return new ObservationCommandLineRunner(this.beanFactory, commandLineRunner, beanName); + } + return bean; + } + +} 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..3d3eb2f5 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/ObservationTaskAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-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 io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.BeanFactory; +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 + static ObservationCommandLineRunnerBeanPostProcessor observedCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) { + return new ObservationCommandLineRunnerBeanPostProcessor(beanFactory); + } + + @Bean + static ObservationApplicationRunnerBeanPostProcessor observedApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) { + return new ObservationApplicationRunnerBeanPostProcessor(beanFactory); + } + +} 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..80747bc6 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskDocumentedObservation.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-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 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"; + } + }; + + /** + * 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..687438c4 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskKeyValuesProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-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 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..5a74a117 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/observation/TaskObservationContext.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-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 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..b56ec25a --- /dev/null +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/configuration/observation/ObservationIntegrationTests.java @@ -0,0 +1,114 @@ +/* + * 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.cloud.task.configuration.EnableTask; +import org.springframework.cloud.task.configuration.SimpleTaskAutoConfiguration; +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({SimpleTaskAutoConfiguration.class, ObservationAutoConfiguration.class, ObservationTaskAutoConfiguration.class, BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class, MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, ZipkinAutoConfiguration.class}) + @EnableTask + 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/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 00000000..ab55cdec --- /dev/null +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,7 @@ + + + + +