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

Reply via email to