From 956afdcf972a3af41bf580fe7a54f20a8602f8d1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sun, 26 Apr 2020 22:04:58 +0200 Subject: [PATCH] Add support for Reactor Netty Micrometer metrics This commit enables the production of TCP and buffer allocator metrics for Reactor Netty, client and server. When applications use auto-configured server (`NettyReactiveWebServerFactory`) and client (through `WebClient.Builder`) instances, metrics will be enabled. Note that HTTP metrics are not enabled here, since similar metrics are already produced at the WebFlux level. Also, to avoid cardinality explosion of metrics (through the URI tag), Reactor Netty offers configurable infrastructure to deduplicate URI tags by turning expanded URI instances into templated URIs. This is not targeted for Spring usage. Closes gh-19388 --- .../client/WebClientMetricsConfiguration.java | 14 +++- .../netty/NettyMetricsAutoConfiguration.java | 45 ++++++++++ .../metrics/web/netty/package-info.java | 20 +++++ .../main/resources/META-INF/spring.factories | 1 + .../WebClientMetricsConfigurationTests.java | 50 +++++++++++ .../NettyMetricsAutoConfigurationTests.java | 83 +++++++++++++++++++ .../asciidoc/production-ready-features.adoc | 1 + 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java index cb9609c232..7220c45fd7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.client; import io.micrometer.core.instrument.MeterRegistry; +import reactor.netty.http.client.HttpClient; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Client.ClientRequest; @@ -25,6 +26,7 @@ import org.springframework.boot.actuate.metrics.web.reactive.client.MetricsWebCl import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.reactive.function.client.ReactorNettyHttpClientMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @@ -53,4 +55,14 @@ class WebClientMetricsConfiguration { request.getAutotime()); } + @ConditionalOnClass(HttpClient.class) + static class ReactorNettyClientMetricsConfiguration { + + @Bean + ReactorNettyHttpClientMapper metricsHttpClientMapper() { + return (httpClient) -> httpClient.tcpConfiguration((tcpClient) -> tcpClient.metrics(true)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfiguration.java new file mode 100644 index 0000000000..813aca0b27 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.boot.actuate.autoconfigure.metrics.web.netty; + +import io.micrometer.core.instrument.MeterRegistry; +import reactor.netty.http.server.HttpServer; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Reactor Netty metrics. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication +@ConditionalOnClass({ MeterRegistry.class, HttpServer.class }) +public class NettyMetricsAutoConfiguration { + + @Bean + public NettyServerCustomizer nettyServerMetricsCustomizer() { + return (httpServer) -> httpServer.tcpConfiguration((tcpServer) -> tcpServer.metrics(true)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/package-info.java new file mode 100644 index 0000000000..2633bed6a9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Auto-configuration for Reactor Netty actuator metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.web.netty; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index e297520518..560dc63b37 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -71,6 +71,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsA org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.web.netty.NettyMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java index 7eea946366..6fd541faf7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java @@ -18,16 +18,25 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.client; import java.time.Duration; +import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -107,6 +116,47 @@ class WebClientMetricsConfigurationTests { }); } + @Test + void shouldConfigureMetricsForReactorNetty() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ClientHttpConnectorAutoConfiguration.class)) + .run((context) -> { + CompositeMeterRegistry registry = Metrics.globalRegistry; + MockWebServer server = new MockWebServer(); + try { + server.start(); + server.enqueue(new MockResponse()); + String serverAddress = "http://" + server.getHostName() + ":" + server.getPort(); + WebClient.Builder builder = context.getBean(WebClient.Builder.class); + WebClient client = builder.baseUrl(serverAddress).build(); + assertThat(registry.find("reactor.netty.tcp.client.connect.time").timer()).isNull(); + client.get().uri("/test").retrieve().toBodilessEntity().block(); + assertThat(registry.find("reactor.netty.tcp.client.connect.time").timer()).isNotNull(); + } + finally { + server.shutdown(); + } + }); + } + + @Test + void sanityTest() throws Exception { + MockWebServer server = new MockWebServer(); + try { + server.start(); + server.enqueue(new MockResponse()); + SimpleMeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, Clock.SYSTEM); + Metrics.addRegistry(registry); + HttpClient client = HttpClient.create().metrics(true); + assertThat(registry.find("reactor.netty.http.client.connect.time").timer()).isNull(); + client.get().uri("http://" + server.getHostName() + ":" + server.getPort()).response().block(); + assertThat(registry.find("reactor.netty.http.client.connect.time").timer()).isNotNull(); + } + finally { + server.shutdown(); + } + + } + private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext context) { WebClient webClient = mockWebClient(context.getBean(WebClient.Builder.class)); MeterRegistry registry = context.getBean(MeterRegistry.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..573bddc89c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/netty/NettyMetricsAutoConfigurationTests.java @@ -0,0 +1,83 @@ +/* + * 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.boot.actuate.autoconfigure.metrics.web.netty; + +import java.util.stream.Collectors; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; +import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NettyMetricsAutoConfiguration} + * + * @author Brian Clozel + */ +class NettyMetricsAutoConfigurationTests { + + @Test + void autoConfiguresTcpMetricsWithReactorNettyServer() { + MeterRegistry registry = Metrics.globalRegistry; + assertThat(registry.find("reactor.netty.tcp.server.data.received").summary()).isNull(); + new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(NettyMetricsAutoConfiguration.class, + ReactiveWebServerFactoryAutoConfiguration.class)) + .withUserConfiguration(ReactiveWebServerConfiguration.class).run((context) -> { + AnnotationConfigReactiveWebServerApplicationContext serverContext = context + .getSourceApplicationContext(AnnotationConfigReactiveWebServerApplicationContext.class); + context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, + context.getSourceApplicationContext())); + WebClient.create("http://localhost:" + serverContext.getWebServer().getPort()).get().retrieve() + .toBodilessEntity().block(); + assertThat(registry.find("reactor.netty.tcp.server.data.received").summary()).isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ReactiveWebServerConfiguration { + + @Bean + NettyReactiveWebServerFactory nettyFactory(ObjectProvider customizers) { + NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0); + serverFactory.setServerCustomizers(customizers.orderedStream().collect(Collectors.toList())); + return serverFactory; + } + + @Bean + HttpHandler httpHandler() { + return (req, res) -> res.setComplete(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index be87be2228..c17c8cc546 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -1813,6 +1813,7 @@ Spring Boot registers the following core metrics when applicable: * Logback metrics: record the number of events logged to Logback at each level * Uptime metrics: report a gauge for uptime and a fixed gauge representing the application's absolute start time * Tomcat metrics (`server.tomcat.mbeanregistry.enabled` must be set to `true` for all Tomcat metrics to be registered) +* Reactor Netty metrics (TCP and allocator metrics for client and server) * {spring-integration-docs}system-management.html#micrometer-integration[Spring Integration] metrics