Browse Source
with this change we're introducing Micrometer Observations for CircuitBreakerspull/1131/head
Marcin Grzejszczak
2 years ago
12 changed files with 619 additions and 1 deletions
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
:root-target: ../../../target/ |
||||
|
||||
[[observability]] |
||||
== Observability metadata |
||||
|
||||
include::{root-target}_metrics.adoc[] |
||||
|
||||
include::{root-target}_spans.adoc[] |
@ -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"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -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 |
||||
|
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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"; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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)); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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…
Reference in new issue