diff --git a/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfiguration.java b/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfiguration.java index 15b6961..7c54040 100644 --- a/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfiguration.java +++ b/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfiguration.java @@ -28,7 +28,10 @@ import io.github.resilience4j.micrometer.tagged.TaggedCircuitBreakerMetrics; import io.github.resilience4j.micrometer.tagged.TaggedCircuitBreakerMetricsPublisher; import io.github.resilience4j.micrometer.tagged.TaggedThreadPoolBulkheadMetrics; import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.config.MeterFilter; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; @@ -36,6 +39,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.context.annotation.Bean; @@ -47,6 +51,7 @@ import org.springframework.context.annotation.Configuration; * @author Andrii Bohutskyi */ @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(Resilience4JConfigurationProperties.class) @ConditionalOnProperty(name = { "spring.cloud.circuitbreaker.resilience4j.enabled", "spring.cloud.circuitbreaker.resilience4j.blocking.enabled" }, matchIfMissing = true) public class Resilience4JAutoConfiguration { @@ -84,6 +89,31 @@ public class Resilience4JAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean({ MeterRegistry.class }) + public static class MicrometerResilience4JGroupCustomizerConfiguration { + + private static final String RESILIENCE4J_METER_PREFIX = "resilience4j"; + + @Bean + @ConditionalOnProperty(value = "spring.cloud.circuitbreaker.resilience4j.enableGroupMeterFilter", + havingValue = "true", matchIfMissing = true) + MeterFilter resilience4JMeterFilter(Resilience4JConfigurationProperties properties) { + return new MeterFilter() { + @Override + public Meter.Id map(Meter.Id id) { + if (id.getName().startsWith(RESILIENCE4J_METER_PREFIX) + && id.getTag(Resilience4JCircuitBreaker.CIRCUIT_BREAKER_GROUP_TAG) == null) { + return id.withTag(Tag.of(Resilience4JCircuitBreaker.CIRCUIT_BREAKER_GROUP_TAG, + properties.getDefaultGroupTag())); + } + return id; + } + }; + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnBean({ MeterRegistry.class }) @ConditionalOnClass(name = { "io.github.resilience4j.micrometer.tagged.TaggedCircuitBreakerMetrics", diff --git a/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JConfigurationProperties.java b/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JConfigurationProperties.java new file mode 100644 index 0000000..bf49be3 --- /dev/null +++ b/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JConfigurationProperties.java @@ -0,0 +1,47 @@ +/* + * 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.circuitbreaker.resilience4j; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Ryan Baxter + */ +@ConfigurationProperties("spring.cloud.circuitbreaker.resilience4j") +public class Resilience4JConfigurationProperties { + + private boolean enableGroupMeterFilter = true; + + private String defaultGroupTag = "none"; + + public boolean isEnableGroupMeterFilter() { + return enableGroupMeterFilter; + } + + public void setEnableGroupMeterFilter(boolean enableGroupMeterFilter) { + this.enableGroupMeterFilter = enableGroupMeterFilter; + } + + public String getDefaultGroupTag() { + return defaultGroupTag; + } + + public void setDefaultGroupTag(String defaultGroupTag) { + this.defaultGroupTag = defaultGroupTag; + } + +} diff --git a/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfigurationTests.java b/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfigurationTests.java new file mode 100644 index 0000000..356bd89 --- /dev/null +++ b/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfigurationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2018 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.circuitbreaker.resilience4j; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import org.junit.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; + +/** + * @author Ryan Baxter + */ +public class Resilience4JAutoConfigurationTests { + + static Resilience4JCircuitBreakerFactory circuitBreakerFactory = spy(new Resilience4JCircuitBreakerFactory( + CircuitBreakerRegistry.ofDefaults(), TimeLimiterRegistry.ofDefaults(), null)); + + @Test + public void meterFilterEnabled() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder().web(WebApplicationType.NONE) + .sources(Resilience4JAutoConfigurationTests.TestApp.class).run()) { + assertThat(context.getBean("resilience4JMeterFilter")).isNotNull(); + } + } + + @Test + public void meterFilterDisabled() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder().web(WebApplicationType.NONE) + .sources(Resilience4JAutoConfigurationTests.TestApp.class) + .properties("spring.cloud.circuitbreaker.resilience4j.enableGroupMeterFilter=false").run()) { + assertThat(context.containsBean("resilience4JMeterFilter")).isFalse(); + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestApp { + + @Bean + Resilience4JCircuitBreakerFactory circuitBreakerFactory() { + return circuitBreakerFactory; + } + + } + +} diff --git a/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JBulkheadIntegrationTest.java b/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JBulkheadIntegrationTest.java index 4b03eb4..1b06f03 100644 --- a/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JBulkheadIntegrationTest.java +++ b/spring-cloud-circuitbreaker-resilience4j/src/test/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JBulkheadIntegrationTest.java @@ -17,6 +17,8 @@ package org.springframework.cloud.circuitbreaker.resilience4j; import java.time.Duration; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -87,6 +89,9 @@ public class Resilience4JBulkheadIntegrationTest { @Autowired Application.DemoControllerService service; + @Autowired + private TestRestTemplate rest; + @Test public void testSlow() { assertThat(service.slow()).isEqualTo("fallback"); @@ -132,6 +137,17 @@ public class Resilience4JBulkheadIntegrationTest { verify(Application.slowThreadPoolRejectedConsumer, times(1)).consumeEvent(any()); } + @Test + public void testResilience4JMetricsAvailable() { + assertThat(service.normal()).isEqualTo("normal"); + assertThat(((List) rest.getForObject("/actuator/metrics", Map.class).get("names")) + .contains("resilience4j.bulkhead.max.thread.pool.size")).isTrue(); + + assertThat(((List) rest + .getForObject("/actuator/metrics/resilience4j.bulkhead.max.thread.pool.size?tag=group:none", Map.class) + .get("availableTags"))).hasSize(1); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @RestController