This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push: new 2aa6c35b67 [Metrics] Expose a few useful Reactor Netty metrics (#2483) 2aa6c35b67 is described below commit 2aa6c35b6783617b569171b79dc55a48d0c46ac8 Author: Trần Hồng Quân <55171818+quantranhong1...@users.noreply.github.com> AuthorDate: Mon Nov 4 20:58:58 2024 +0700 [Metrics] Expose a few useful Reactor Netty metrics (#2483) --- docs/modules/servers/partials/configure/jvm.adoc | 12 ++++ pom.xml | 12 ++++ .../james/jmap/metrics/HttpClientMetrics.scala | 74 ++++++++++++++++++++++ .../apache/james/jmap/routes/SessionRoutes.scala | 15 ++++- .../james/jmap/routes/SessionRoutesTest.scala | 5 +- server/protocols/jmap/pom.xml | 8 +++ .../java/org/apache/james/jmap/JMAPServer.java | 18 ++++++ 7 files changed, 140 insertions(+), 4 deletions(-) diff --git a/docs/modules/servers/partials/configure/jvm.adoc b/docs/modules/servers/partials/configure/jvm.adoc index 6aec817009..2d8fc35833 100644 --- a/docs/modules/servers/partials/configure/jvm.adoc +++ b/docs/modules/servers/partials/configure/jvm.adoc @@ -75,6 +75,18 @@ james.jmap.quota.draft.compatibility=true ---- To enable the compatibility. +== Enable Reactor Netty metrics for JMAP server + +Allow to enable https://projectreactor.io/docs/netty/1.1.19/reference/index.html#_metrics_4[Reactor Netty metrics] for JMAP server which would provide useful debug information. + +Optional. Boolean. Default to false. + +Ex in `jvm.properties`: +---- +james.jmap.reactor.netty.metrics.enabled=true +---- +To enable the metrics. + == Enable S3 metrics James supports extracting some S3 client-level metrics e.g. number of connections being used, time to acquire an S3 connection, total time to finish a S3 request... diff --git a/pom.xml b/pom.xml index 8a00b981e7..949aee9571 100644 --- a/pom.xml +++ b/pom.xml @@ -654,6 +654,8 @@ <logback.version>1.4.14</logback.version> <tink.version>1.9.0</tink.version> <lettuce.core.version>6.3.2.RELEASE</lettuce.core.version> + <io.micrometer.core.version>1.13.6</io.micrometer.core.version> + <io.micrometer.tracing.version>1.3.5</io.micrometer.tracing.version> <bouncycastle.version>1.78.1</bouncycastle.version> @@ -2427,6 +2429,16 @@ <artifactId>lettuce-core</artifactId> <version>${lettuce.core.version}</version> </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-core</artifactId> + <version>${io.micrometer.core.version}</version> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-tracing</artifactId> + <version>${io.micrometer.tracing.version}</version> + </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec</artifactId> diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/metrics/HttpClientMetrics.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/metrics/HttpClientMetrics.scala new file mode 100644 index 0000000000..5a3087048b --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/metrics/HttpClientMetrics.scala @@ -0,0 +1,74 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.metrics + +import io.micrometer.core.instrument.Metrics.globalRegistry +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import jakarta.inject.{Inject, Singleton} +import org.apache.james.jmap.metrics.HttpClientMetrics.{NETTY_CONNECTIONS_ACTIVE, NETTY_CONNECTIONS_TOTAL, NETTY_DATA_RECEIVED, NETTY_DATA_SENT} +import org.apache.james.metrics.api.GaugeRegistry +import reactor.netty.Metrics.{CONNECTIONS_ACTIVE, CONNECTIONS_TOTAL, DATA_RECEIVED, DATA_SENT, HTTP_SERVER_PREFIX} + +object HttpClientMetrics { + lazy val NETTY_CONNECTIONS_ACTIVE: String = HTTP_SERVER_PREFIX + CONNECTIONS_ACTIVE + lazy val NETTY_CONNECTIONS_TOTAL: String = HTTP_SERVER_PREFIX + CONNECTIONS_TOTAL + lazy val NETTY_DATA_RECEIVED: String = HTTP_SERVER_PREFIX + DATA_RECEIVED + lazy val NETTY_DATA_SENT: String = HTTP_SERVER_PREFIX + DATA_SENT +} + +@Singleton +case class HttpClientMetrics @Inject()(gaugeRegistry: GaugeRegistry) { + private lazy val activeConnectionGauge: GaugeRegistry.SettableGauge[Integer] = gaugeRegistry.settableGauge(s"jmap.$NETTY_CONNECTIONS_ACTIVE") + private lazy val totalConnectionGauge: GaugeRegistry.SettableGauge[Integer] = gaugeRegistry.settableGauge(s"jmap.$NETTY_CONNECTIONS_TOTAL") + private lazy val dataReceivedGauge: GaugeRegistry.SettableGauge[Integer] = gaugeRegistry.settableGauge(s"jmap.$NETTY_DATA_RECEIVED") + private lazy val dataSentGauge: GaugeRegistry.SettableGauge[Integer] = gaugeRegistry.settableGauge(s"jmap.$NETTY_DATA_SENT") + private lazy val nettyCompositeMeterRegistry = globalRegistry.add(new SimpleMeterRegistry()) + + def update(): Unit = { + updateActiveConnectionGauge() + updateTotalConnectionGauge() + updateDateReceivedGauge() + updateDataSentGauge() + } + + private def updateActiveConnectionGauge(): Unit = + Option(nettyCompositeMeterRegistry.find(NETTY_CONNECTIONS_ACTIVE)) + .flatMap(search => Option(search.gauge())) + .flatMap(gauge => Option(gauge.value())) + .foreach(double => activeConnectionGauge.setValue(double.intValue)) + + private def updateTotalConnectionGauge(): Unit = + Option(nettyCompositeMeterRegistry.find(NETTY_CONNECTIONS_TOTAL)) + .flatMap(search => Option(search.gauge())) + .flatMap(gauge => Option(gauge.value())) + .foreach(double => totalConnectionGauge.setValue(double.intValue)) + + private def updateDateReceivedGauge(): Unit = + Option(nettyCompositeMeterRegistry.find(NETTY_DATA_RECEIVED)) + .flatMap(search => Option(search.summary())) + .flatMap(summary => Option(summary.totalAmount())) + .foreach(double => dataReceivedGauge.setValue(double.intValue)) + + private def updateDataSentGauge(): Unit = + Option(nettyCompositeMeterRegistry.find(NETTY_DATA_SENT)) + .flatMap(search => Option(search.summary())) + .flatMap(summary => Option(summary.totalAmount())) + .foreach(double => dataSentGauge.setValue(double.intValue)) +} diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala index f1fdafdd3e..128e1c838b 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala @@ -23,18 +23,20 @@ import java.nio.charset.StandardCharsets import java.util.stream.Stream import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE} -import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, INTERNAL_SERVER_ERROR, OK, UNAUTHORIZED} -import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus} +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpResponseStatus.{INTERNAL_SERVER_ERROR, OK, UNAUTHORIZED} import jakarta.inject.{Inject, Named} import org.apache.commons.lang3.tuple.Pair import org.apache.james.core.Username import org.apache.james.jmap.HttpConstants.{JSON_CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8} import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL +import org.apache.james.jmap.JMAPServer.REACTOR_NETTY_METRICS_ENABLE import org.apache.james.jmap.core.{JmapRfc8621Configuration, ProblemDetails, Session, UrlPrefixes} import org.apache.james.jmap.exceptions.UnauthorizedException import org.apache.james.jmap.http.Authenticator import org.apache.james.jmap.http.rfc8621.InjectionKeys import org.apache.james.jmap.json.ResponseSerializer +import org.apache.james.jmap.metrics.HttpClientMetrics import org.apache.james.jmap.routes.SessionRoutes.{JMAP_SESSION, LOGGER, WELL_KNOWN_JMAP} import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes} import org.apache.james.mailbox.MailboxSession @@ -56,7 +58,8 @@ object SessionRoutes { class SessionRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator, val sessionSupplier: SessionSupplier, val delegationStore: DelegationStore, - val jmapRfc8621Configuration: JmapRfc8621Configuration) extends JMAPRoutes { + val jmapRfc8621Configuration: JmapRfc8621Configuration, + val httpClientMetrics: HttpClientMetrics) extends JMAPRoutes { private val generateSession: JMAPRoute.Action = (request, response) => SMono.fromPublisher(authenticator.authenticate(request)) @@ -73,6 +76,12 @@ class SessionRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: .flatMap(session => sendRespond(session, response)) .onErrorResume(throwable => SMono.fromPublisher(errorHandling(throwable, response))) .asJava() + .doOnSuccess(_ => updateHttpClientMetricsIfNeeded()) + + private def updateHttpClientMetricsIfNeeded(): Unit = + if (REACTOR_NETTY_METRICS_ENABLE) { + httpClientMetrics.update() + } private val redirectToSession: JMAPRoute.Action = JMAPRoutes.redirectTo(JMAP_SESSION) diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala index 3c29c40f03..573252704c 100644 --- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala @@ -34,9 +34,11 @@ import org.apache.james.jmap.core.JmapRfc8621Configuration.URL_PREFIX_DEFAULT import org.apache.james.jmap.core.UuidState.INSTANCE import org.apache.james.jmap.core.{DefaultCapabilities, JmapRfc8621Configuration} import org.apache.james.jmap.http.Authenticator +import org.apache.james.jmap.metrics.HttpClientMetrics import org.apache.james.jmap.routes.SessionRoutesTest.{BOB, TEST_CONFIGURATION} import org.apache.james.jmap.{JMAPConfiguration, JMAPRoutesHandler, JMAPServer, Version, VersionParser} import org.apache.james.mailbox.MailboxSession +import org.apache.james.metrics.api.NoopGaugeRegistry import org.apache.james.user.api.DelegationStore import org.mockito.ArgumentMatchers.any import org.mockito.Mockito._ @@ -74,7 +76,8 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers { sessionSupplier = new SessionSupplier(DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION), delegationStore = mockDelegationStore, authenticator = mockedAuthFilter, - jmapRfc8621Configuration = JmapRfc8621Configuration.LOCALHOST_CONFIGURATION) + jmapRfc8621Configuration = JmapRfc8621Configuration.LOCALHOST_CONFIGURATION, + httpClientMetrics = HttpClientMetrics(new NoopGaugeRegistry)) jmapServer = new JMAPServer( TEST_CONFIGURATION, Set(new JMAPRoutesHandler(Version.RFC8621, sessionRoutes)).asJava, diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml index 2b1ae4ad5b..0318709633 100644 --- a/server/protocols/jmap/pom.xml +++ b/server/protocols/jmap/pom.xml @@ -72,6 +72,14 @@ <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-core</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-tracing</artifactId> + </dependency> <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty</artifactId> diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java index d40ca96ae8..9c72329a11 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java @@ -21,10 +21,13 @@ package org.apache.james.jmap; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; +import static reactor.netty.Metrics.HTTP_CLIENT_PREFIX; +import static reactor.netty.Metrics.URI; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Stream; import jakarta.annotation.PreDestroy; @@ -39,12 +42,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Multimap; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.config.MeterFilter; import io.netty.handler.codec.http.HttpMethod; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServerRequest; public class JMAPServer implements Startable { + public static final boolean REACTOR_NETTY_METRICS_ENABLE = Boolean.parseBoolean(System.getProperty("james.jmap.reactor.netty.metrics.enabled", "false")); + private static final int REACTOR_NETTY_METRICS_MAX_URI_TAGS = 100; private static final int RANDOM_PORT = 0; private final JMAPConfiguration configuration; @@ -90,10 +97,21 @@ public class JMAPServer implements Startable { .orElse(RANDOM_PORT)) .handle((request, response) -> handleVersionRoute(request).handleRequest(request, response)) .wiretap(wireTapEnabled()) + .metrics(REACTOR_NETTY_METRICS_ENABLE, Function.identity()) .bindNow()); + + if (REACTOR_NETTY_METRICS_ENABLE) { + configureReactorNettyMetrics(); + } } } + private void configureReactorNettyMetrics() { + Metrics.globalRegistry + .config() + .meterFilter(MeterFilter.maximumAllowableTags(HTTP_CLIENT_PREFIX, URI, REACTOR_NETTY_METRICS_MAX_URI_TAGS, MeterFilter.deny())); + } + private boolean wireTapEnabled() { return LoggerFactory.getLogger("org.apache.james.jmap.wire").isTraceEnabled(); } --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org