Browse Source

Support for Micrometer Observations

with this change we're introducing Micrometer Observations for CircuitBreakers
pull/1131/head
Marcin Grzejszczak 2 years ago
parent
commit
4812bebbbe
  1. 58
      docs/pom.xml
  2. 8
      docs/src/main/asciidoc/_observability.adoc
  3. 4
      docs/src/main/asciidoc/appendix.adoc
  4. 5
      spring-cloud-commons/pom.xml
  5. 92
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerDocumentedObservation.java
  6. 64
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationContext.java
  7. 35
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationConvention.java
  8. 44
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/DefaultCircuitBreakerObservationConvention.java
  9. 67
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreaker.java
  10. 50
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedFunction.java
  11. 50
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedSupplier.java
  12. 143
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreakerTests.java

58
docs/pom.xml

@ -20,6 +20,12 @@ @@ -20,6 +20,12 @@
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
<!-- Don't upload docs jar to central / repo.spring.io -->
<maven-deploy-plugin-default.phase>none</maven-deploy-plugin-default.phase>
<!-- Observability -->
<micrometer-docs-generator.version>1.0.0-M5</micrometer-docs-generator.version>
<micrometer-docs-generator.inputPath>${maven.multiModuleProjectDirectory}/spring-cloud-commons/</micrometer-docs-generator.inputPath>
<micrometer-docs-generator.inclusionPattern>.*</micrometer-docs-generator.inclusionPattern>
<micrometer-docs-generator.outputPath>${maven.multiModuleProjectDirectory}/target/</micrometer-docs-generator.outputPath>
</properties>
<dependencies>
<dependency>
@ -46,6 +52,58 @@ @@ -46,6 +52,58 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-metrics-metadata</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>io.micrometer.docs.metrics.DocsFromSources</mainClass>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>${micrometer-docs-generator.inputPath}</argument>
<argument>${micrometer-docs-generator.inclusionPattern}</argument>
<argument>${micrometer-docs-generator.outputPath}</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>generate-tracing-metadata</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>io.micrometer.docs.spans.DocsFromSources</mainClass>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>${micrometer-docs-generator.inputPath}</argument>
<argument>${micrometer-docs-generator.inclusionPattern}</argument>
<argument>${micrometer-docs-generator.outputPath}</argument>
</arguments>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>io.micrometer
</groupId>
<artifactId>micrometer-docs-generator-spans</artifactId>
<version>${micrometer-docs-generator.version}
</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>io.micrometer
</groupId>
<artifactId>micrometer-docs-generator-metrics</artifactId>
<version>${micrometer-docs-generator.version}
</version>
<type>jar</type>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

8
docs/src/main/asciidoc/_observability.adoc

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
:root-target: ../../../target/
[[observability]]
== Observability metadata
include::{root-target}_metrics.adoc[]
include::{root-target}_spans.adoc[]

4
docs/src/main/asciidoc/appendix.adoc

@ -11,4 +11,6 @@ This appendix provides a list of common {project-full-name} properties and refer @@ -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[]
include::_configprops.adoc[]
include::_observability.adoc[]

5
spring-cloud-commons/pom.xml

@ -192,5 +192,10 @@ @@ -192,5 +192,10 @@
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

92
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerDocumentedObservation.java

@ -0,0 +1,92 @@ @@ -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<? extends Observation.ObservationConvention<? extends Observation.Context>> 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<? extends Observation.ObservationConvention<? extends Observation.Context>> 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";
}
}
}
}

64
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationContext.java

@ -0,0 +1,64 @@ @@ -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
}
}

35
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/CircuitBreakerObservationConvention.java

@ -0,0 +1,35 @@ @@ -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<CircuitBreakerObservationContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof CircuitBreakerObservationContext;
}
}

44
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/DefaultCircuitBreakerObservationConvention.java

@ -0,0 +1,44 @@ @@ -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";
}
}

67
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreaker.java

@ -0,0 +1,67 @@ @@ -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> T run(Supplier<T> toRun, Function<Throwable, T> 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> T run(Supplier<T> 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;
}
}

50
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedFunction.java

@ -0,0 +1,50 @@ @@ -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 <T> type returned by the fallback
* @since 4.0.0
*/
class ObservedFunction<T> implements Function<Throwable, T> {
private final Function<Throwable, T> 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<Throwable, T> 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));
}
}

50
spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedSupplier.java

@ -0,0 +1,50 @@ @@ -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 <T> type returned by the supplier
* @since 4.0.0
*/
class ObservedSupplier<T> implements Supplier<T> {
private final Supplier<T> 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<T> 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);
}
}

143
spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/observation/ObservedCircuitBreakerTests.java

@ -0,0 +1,143 @@ @@ -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> T run(Supplier<T> toRun, Function<Throwable, T> 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> T run(Supplier<T> toRun, Function<Throwable, T> 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> T run(Supplier<T> toRun, Function<Throwable, T> 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<Observation.Context> 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<Observation.Context> {
List<Observation.Context> contexts = new ArrayList<>();
@Override
public void onStop(Observation.Context context) {
this.contexts.add(context);
}
@Override
public boolean supportsContext(Observation.Context context) {
return true;
}
}
}
Loading…
Cancel
Save