diff --git a/docs/pom.xml b/docs/pom.xml index 34de7cc6..b9be0e51 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -20,6 +20,12 @@ deploy none + + + 1.0.0-M5 + ${maven.multiModuleProjectDirectory}/spring-cloud-commons/ + .* + ${maven.multiModuleProjectDirectory}/target/ @@ -46,6 +52,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.apache.maven.plugins diff --git a/docs/src/main/asciidoc/_observability.adoc b/docs/src/main/asciidoc/_observability.adoc new file mode 100644 index 00000000..01121b26 --- /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 2c18b865..aa8ab264 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/src/main/asciidoc/appendix.adoc @@ -11,4 +11,6 @@ This appendix provides a list of common {project-full-name} properties and refer NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. Also, you can define your own properties. -include::_configprops.adoc[] \ No newline at end of file +include::_configprops.adoc[] + +include::_observability.adoc[] diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 9c92ac82..e96e548d 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -192,5 +192,10 @@ reactor-test test + + io.micrometer + micrometer-observation-test + test + diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerDocumentedObservation.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerDocumentedObservation.java new file mode 100644 index 00000000..78ee446e --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerDocumentedObservation.java @@ -0,0 +1,92 @@ +/* + * 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.client.circuitbreaker.observation; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.DocumentedObservation; + +enum CircuitBreakerDocumentedObservation implements DocumentedObservation { + + /** + * Observation created when we wrap a Supplier passed to the CircuitBreaker. + */ + CIRCUIT_BREAKER_SUPPLIER_OBSERVATION { + @Override + public Class> getDefaultConvention() { + return DefaultCircuitBreakerObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityTags.values(); + } + + @Override + public String getPrefix() { + return "spring.cloud.circuitbreaker"; + } + + // TODO: Move this to convention with the next micrometer release + // @Override + // public String getContextualName() { + // return "circuit-breaker"; + // } + }, + + /** + * Observation created when we wrap a Function passed to the CircuitBreaker as + * fallback. + */ + CIRCUIT_BREAKER_FUNCTION_OBSERVATION { + @Override + public Class> getDefaultConvention() { + return DefaultCircuitBreakerObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityTags.values(); + } + + @Override + public String getPrefix() { + return "spring.cloud.circuitbreaker"; + } + + // TODO: Move this to convention with the next micrometer release + // @Override + // public String getContextualName() { + // return "circuit-breaker fallback"; + // } + }; + + enum LowCardinalityTags implements KeyName { + + /** + * Defines the type of wrapped lambda. + */ + OBJECT_TYPE { + @Override + public String getKeyName() { + return "spring.cloud.circuitbreaker.type"; + } + } + + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationContext.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationContext.java new file mode 100644 index 00000000..12ecd8c2 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationContext.java @@ -0,0 +1,64 @@ +/* + * 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.client.circuitbreaker.observation; + +import io.micrometer.observation.Observation; + +/** + * Circuit Breaker {@link Observation.Context}. + * + * @author Marcin Grzejszczak + * @since 4.0.0 + */ +public class CircuitBreakerObservationContext extends Observation.Context { + + private final Type type; + + /** + * Creates a new instance of {@link CircuitBreakerDocumentedObservation}. + * @param type type of wrapped object + */ + public CircuitBreakerObservationContext(Type type) { + this.type = type; + } + + /** + * Gets the wrapped object type. + * @return type of wrapped object + */ + public Type getType() { + return type; + } + + /** + * Describes the type of wrapped object. + */ + public enum Type { + + /** + * Fallback function. + */ + FUNCTION, + + /** + * Operation to run. + */ + SUPPLIER + + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationConvention.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationConvention.java new file mode 100644 index 00000000..21b08250 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationConvention.java @@ -0,0 +1,35 @@ +/* + * 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.client.circuitbreaker.observation; + +import io.micrometer.observation.Observation; + +/** + * {@link Observation.ObservationConvention} for {@link CircuitBreakerObservationContext}. + * + * @author Marcin Grzejszczak + * @since 4.0.0 + */ +public interface CircuitBreakerObservationConvention + extends Observation.ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof CircuitBreakerObservationContext; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/DefaultCircuitBreakerObservationConvention.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/DefaultCircuitBreakerObservationConvention.java new file mode 100644 index 00000000..f8e41611 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/DefaultCircuitBreakerObservationConvention.java @@ -0,0 +1,44 @@ +/* + * 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.client.circuitbreaker.observation; + +import java.util.Locale; + +import io.micrometer.common.KeyValues; + +/** + * Default implementation of {@link CircuitBreakerObservationContext}. + * + * @author Marcin Grzejszczak + * @since 4.0.0 + */ +public class DefaultCircuitBreakerObservationConvention implements CircuitBreakerObservationConvention { + + static final DefaultCircuitBreakerObservationConvention INSTANCE = new DefaultCircuitBreakerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(CircuitBreakerObservationContext context) { + return KeyValues.of(CircuitBreakerDocumentedObservation.LowCardinalityTags.OBJECT_TYPE + .of(context.getType().name().toLowerCase(Locale.ROOT))); + } + + @Override + public String getName() { + return "spring.cloud.circuitbreaker"; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreaker.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreaker.java new file mode 100644 index 00000000..b30ffe5b --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreaker.java @@ -0,0 +1,67 @@ +/* + * 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.client.circuitbreaker.observation; + +import java.util.function.Function; +import java.util.function.Supplier; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; + +/** + * Observed Circuit Breaker. + * + * @author Marcin Grzejszczak + * @since 4.0.0 + */ +public class ObservedCircuitBreaker implements CircuitBreaker { + + private final CircuitBreaker delegate; + + private final ObservationRegistry observationRegistry; + + private CircuitBreakerObservationConvention customConvention; + + public ObservedCircuitBreaker(CircuitBreaker delegate, ObservationRegistry observationRegistry) { + this.delegate = delegate; + this.observationRegistry = observationRegistry; + } + + @Override + public T run(Supplier toRun, Function fallback) { + return this.delegate.run( + new ObservedSupplier<>(this.customConvention, + new CircuitBreakerObservationContext(CircuitBreakerObservationContext.Type.SUPPLIER), + "circuit-breaker", this.observationRegistry, toRun), + new ObservedFunction<>(this.customConvention, + new CircuitBreakerObservationContext(CircuitBreakerObservationContext.Type.FUNCTION), + "circuit-breaker fallback", this.observationRegistry, fallback)); + } + + @Override + public T run(Supplier toRun) { + return this.delegate.run(new ObservedSupplier<>(this.customConvention, + new CircuitBreakerObservationContext(CircuitBreakerObservationContext.Type.SUPPLIER), "circuit-breaker", + this.observationRegistry, toRun)); + } + + public void setCustomConvention(CircuitBreakerObservationConvention customConvention) { + this.customConvention = customConvention; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedFunction.java new file mode 100644 index 00000000..ca2ea09a --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedFunction.java @@ -0,0 +1,50 @@ +/* + * 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.client.circuitbreaker.observation; + +import java.util.function.Function; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +/** + * Observed {@link Function}. + * + * @param type returned by the fallback + * @since 4.0.0 + */ +class ObservedFunction implements Function { + + private final Function delegate; + + private final Observation observation; + + // TODO: Move out contextual name with the next micrometer release + ObservedFunction(CircuitBreakerObservationConvention customConvention, CircuitBreakerObservationContext context, + String conextualName, ObservationRegistry observationRegistry, Function toRun) { + this.delegate = toRun; + this.observation = CircuitBreakerDocumentedObservation.CIRCUIT_BREAKER_SUPPLIER_OBSERVATION.observation( + customConvention, DefaultCircuitBreakerObservationConvention.INSTANCE, context, observationRegistry); + this.observation.contextualName(conextualName); + } + + @Override + public T apply(Throwable throwable) { + return this.observation.observe(() -> this.delegate.apply(throwable)); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedSupplier.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedSupplier.java new file mode 100644 index 00000000..016d6e0d --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedSupplier.java @@ -0,0 +1,50 @@ +/* + * 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.client.circuitbreaker.observation; + +import java.util.function.Supplier; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +/** + * Observed {@link Supplier}. + * + * @param type returned by the supplier + * @since 4.0.0 + */ +class ObservedSupplier implements Supplier { + + private final Supplier delegate; + + private final Observation observation; + + // TODO: Move out contextual name with the next micrometer release + ObservedSupplier(CircuitBreakerObservationConvention customConvention, CircuitBreakerObservationContext context, + String contextualName, ObservationRegistry observationRegistry, Supplier toRun) { + this.delegate = toRun; + this.observation = CircuitBreakerDocumentedObservation.CIRCUIT_BREAKER_SUPPLIER_OBSERVATION.observation( + customConvention, DefaultCircuitBreakerObservationConvention.INSTANCE, context, observationRegistry); + this.observation.contextualName(contextualName); + } + + @Override + public T get() { + return this.observation.observe(this.delegate); + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreakerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreakerTests.java new file mode 100644 index 00000000..c674f0fb --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreakerTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2020 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.client.circuitbreaker.observation; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.ObservationContextAssert; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; + +import static org.assertj.core.api.BDDAssertions.then; + +class ObservedCircuitBreakerTests { + + TestObservationRegistry registry = TestObservationRegistry.create(); + + @Test + void should_wrap_circuit_breaker_in_observation() { + CircuitBreaker delegate = new CircuitBreaker() { + @Override + public T run(Supplier toRun, Function fallback) { + return toRun.get(); + } + }; + ObservedCircuitBreaker circuitBreaker = new ObservedCircuitBreaker(delegate, registry); + + String result = circuitBreaker.run(() -> "hello"); + + then(result).isEqualTo("hello"); + TestObservationRegistryAssert.assertThat(registry).hasSingleObservationThat() + .hasNameEqualTo("spring.cloud.circuitbreaker") + .hasLowCardinalityKeyValue("spring.cloud.circuitbreaker.type", "supplier") + .hasContextualNameEqualTo("circuit-breaker"); + } + + @Test + void should_wrap_circuit_breaker_in_observation_with_custom_convention() { + CircuitBreaker delegate = new CircuitBreaker() { + @Override + public T run(Supplier toRun, Function fallback) { + return toRun.get(); + } + }; + ObservedCircuitBreaker circuitBreaker = new ObservedCircuitBreaker(delegate, registry); + circuitBreaker.setCustomConvention(new CircuitBreakerObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(CircuitBreakerObservationContext context) { + return KeyValues.of("bar", "baz"); + } + + @Override + public String getName() { + return "foo"; + } + }); + + String result = circuitBreaker.run(() -> "hello"); + + then(result).isEqualTo("hello"); + TestObservationRegistryAssert.assertThat(registry).hasSingleObservationThat().hasNameEqualTo("foo") + .hasLowCardinalityKeyValue("bar", "baz").hasContextualNameEqualTo("circuit-breaker"); + } + + @Test + void should_wrap_circuit_breaker_with_fallback_in_observation() { + ObservationRegistry registry = ObservationRegistry.create(); + MyHandler myHandler = new MyHandler(); + registry.observationConfig().observationHandler(myHandler); + CircuitBreaker delegate = new CircuitBreaker() { + @Override + public T run(Supplier toRun, Function fallback) { + try { + return toRun.get(); + } + catch (Throwable t) { + return fallback.apply(t); + } + } + }; + ObservedCircuitBreaker circuitBreaker = new ObservedCircuitBreaker(delegate, registry); + + String result = circuitBreaker.run(() -> { + throw new IllegalStateException("BOOM!"); + }, throwable -> "goodbye"); + + then(result).isEqualTo("goodbye"); + List contexts = myHandler.contexts; + + // TODO: Convert to usage of test registry assert with the next micrometer release + BDDAssertions.then(contexts).hasSize(2); + BDDAssertions.then(contexts.get(0)) + .satisfies(context -> ObservationContextAssert.then(context) + .hasNameEqualTo("spring.cloud.circuitbreaker").hasContextualNameEqualTo("circuit-breaker") + .hasLowCardinalityKeyValue("spring.cloud.circuitbreaker.type", "supplier")); + BDDAssertions.then(contexts.get(1)).satisfies(context -> ObservationContextAssert.then(context) + .hasNameEqualTo("spring.cloud.circuitbreaker").hasContextualNameEqualTo("circuit-breaker fallback") + .hasLowCardinalityKeyValue("spring.cloud.circuitbreaker.type", "function")); + } + + // TODO: Convert to usage of test registry assert with the next micrometer release + static class MyHandler implements ObservationHandler { + + List contexts = new ArrayList<>(); + + @Override + public void onStop(Observation.Context context) { + this.contexts.add(context); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + + } + +}