This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 5547a2917f [#8593]improve(server): expose more server metrics (#8670)
5547a2917f is described below
commit 5547a2917f8f4c411c2afd41853901c56cf8ac6e
Author: Junda Yang <[email protected]>
AuthorDate: Wed Sep 24 00:28:37 2025 -0700
[#8593]improve(server): expose more server metrics (#8670)
### What changes were proposed in this pull request?
Expose these jetty server thread pool metrics -
min-threads
max-threads
total-threads
busy-threads
idle-threads
queued-requests
### Why are the changes needed?
This PR exposes more metrics about jetty server. We can observe if the
server is overloaded by checking idle-threads, busy-threads, as well as
queued requests.
Fix: #8593
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
1. Unit test added
2. Tested in our staging / production deployment
---
.../org/apache/gravitino/metrics/MetricNames.java | 7 +-
.../server/web/HttpServerMetricsSource.java | 17 +-
.../server/web/TestHttpServerMetricsSource.java | 275 +++++++++++++++++++++
3 files changed, 296 insertions(+), 3 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/metrics/MetricNames.java
b/core/src/main/java/org/apache/gravitino/metrics/MetricNames.java
index b3c8271942..a6c9d16db8 100644
--- a/core/src/main/java/org/apache/gravitino/metrics/MetricNames.java
+++ b/core/src/main/java/org/apache/gravitino/metrics/MetricNames.java
@@ -21,11 +21,16 @@ package org.apache.gravitino.metrics;
public class MetricNames {
public static final String HTTP_PROCESS_DURATION =
"http-request-duration-seconds";
- public static final String SERVER_IDLE_THREAD_NUM =
"http-server.idle-thread.num";
public static final String DATASOURCE_ACTIVE_CONNECTIONS =
"datasource.active-connections";
public static final String DATASOURCE_IDLE_CONNECTIONS =
"datasource.idle-connections";
public static final String DATASOURCE_MAX_CONNECTIONS =
"datasource.max-connections";
public static final String FILESYSTEM_CACHE = "filesystem-cache";
+ public static final String SERVER_BUSY_THREAD_NUM =
"http-server.busy-thread.num";
+ public static final String SERVER_IDLE_THREAD_NUM =
"http-server.idle-thread.num";
+ public static final String SERVER_QUEUED_REQUEST_NUM =
"http-server.queued-request.num";
+ public static final String SERVER_TOTAL_THREAD_NUM =
"http-server.total-thread.num";
+ public static final String SERVER_MIN_THREAD_NUM =
"http-server.min-thread.num";
+ public static final String SERVER_MAX_THREAD_NUM =
"http-server.max-thread.num";
private MetricNames() {}
}
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/web/HttpServerMetricsSource.java
b/server-common/src/main/java/org/apache/gravitino/server/web/HttpServerMetricsSource.java
index 013c5fe7c0..437cb30090 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/web/HttpServerMetricsSource.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/web/HttpServerMetricsSource.java
@@ -25,6 +25,8 @@ import
com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListene
import java.util.concurrent.TimeUnit;
import org.apache.gravitino.metrics.MetricNames;
import org.apache.gravitino.metrics.source.MetricsSource;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
import org.glassfish.jersey.server.ResourceConfig;
public class HttpServerMetricsSource extends MetricsSource {
@@ -38,7 +40,18 @@ public class HttpServerMetricsSource extends MetricsSource {
() ->
new SlidingTimeWindowArrayReservoir(
getTimeSlidingWindowSeconds(), TimeUnit.SECONDS)));
- registerGauge(
- MetricNames.SERVER_IDLE_THREAD_NUM, () ->
server.getThreadPool().getIdleThreads());
+
+ // Register QueuedThreadPool specific metrics with instance checks
+ ThreadPool threadPool = server.getThreadPool();
+ registerGauge(MetricNames.SERVER_IDLE_THREAD_NUM, () ->
threadPool.getIdleThreads());
+ registerGauge(MetricNames.SERVER_TOTAL_THREAD_NUM, () ->
threadPool.getThreads());
+
+ if (threadPool instanceof QueuedThreadPool) {
+ QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool;
+ registerGauge(MetricNames.SERVER_BUSY_THREAD_NUM, () ->
queuedThreadPool.getBusyThreads());
+ registerGauge(MetricNames.SERVER_QUEUED_REQUEST_NUM, () ->
queuedThreadPool.getQueueSize());
+ registerGauge(MetricNames.SERVER_MIN_THREAD_NUM, () ->
queuedThreadPool.getMinThreads());
+ registerGauge(MetricNames.SERVER_MAX_THREAD_NUM, () ->
queuedThreadPool.getMaxThreads());
+ }
}
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpServerMetricsSource.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpServerMetricsSource.java
new file mode 100644
index 0000000000..7ed99b8219
--- /dev/null
+++
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpServerMetricsSource.java
@@ -0,0 +1,275 @@
+/*
+ * 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.gravitino.server.web;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+import
com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener;
+import org.apache.gravitino.metrics.MetricNames;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+public class TestHttpServerMetricsSource {
+
+ private JettyServer mockJettyServer;
+ private ThreadPool mockThreadPool;
+ private ResourceConfig mockResourceConfig;
+
+ private HttpServerMetricsSource metricsSource;
+
+ @BeforeEach
+ void setUp() {
+ mockJettyServer = mock(JettyServer.class);
+ mockThreadPool = mock(ThreadPool.class);
+ mockResourceConfig = mock(ResourceConfig.class);
+ }
+
+ @Test
+ void testConstructorWithQueuedThreadPool() {
+ // Arrange - Use a real QueuedThreadPool instance
+ QueuedThreadPool realQueuedThreadPool = new QueuedThreadPool(20, 1, 60000);
+ when(mockJettyServer.getThreadPool()).thenReturn(realQueuedThreadPool);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert
+ assertNotNull(metricsSource);
+ assertNotNull(metricsSource.getMetricRegistry());
+
+ // Verify ResourceConfig registration
+
verify(mockResourceConfig).register(any(InstrumentedResourceMethodApplicationListener.class));
+
+ // Verify basic thread pool metrics are registered
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_IDLE_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_TOTAL_THREAD_NUM));
+
+ // Verify QueuedThreadPool specific metrics are registered
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_BUSY_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_QUEUED_REQUEST_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_MIN_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_MAX_THREAD_NUM));
+ }
+
+ @Test
+ void testConstructorWithNonQueuedThreadPool() {
+ // Arrange - Use a mock ThreadPool that is not QueuedThreadPool
+ when(mockJettyServer.getThreadPool()).thenReturn(mockThreadPool);
+ when(mockThreadPool.getIdleThreads()).thenReturn(3);
+ when(mockThreadPool.getThreads()).thenReturn(8);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert
+ assertNotNull(metricsSource);
+ assertNotNull(metricsSource.getMetricRegistry());
+
+ // Verify ResourceConfig registration
+
verify(mockResourceConfig).register(any(InstrumentedResourceMethodApplicationListener.class));
+
+ // Verify only basic thread pool metrics are registered
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_IDLE_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_TOTAL_THREAD_NUM));
+
+ // Verify QueuedThreadPool specific metrics are NOT registered
+ assertTrue(
+ registry.getGauges().isEmpty()
+ ||
!registry.getGauges().containsKey(MetricNames.SERVER_BUSY_THREAD_NUM));
+ assertTrue(
+ registry.getGauges().isEmpty()
+ ||
!registry.getGauges().containsKey(MetricNames.SERVER_QUEUED_REQUEST_NUM));
+ assertTrue(
+ registry.getGauges().isEmpty()
+ ||
!registry.getGauges().containsKey(MetricNames.SERVER_MIN_THREAD_NUM));
+ assertTrue(
+ registry.getGauges().isEmpty()
+ ||
!registry.getGauges().containsKey(MetricNames.SERVER_MAX_THREAD_NUM));
+ }
+
+ @Test
+ void testGaugeValuesWithQueuedThreadPool() {
+ // Arrange - Use a real QueuedThreadPool instance
+ QueuedThreadPool realQueuedThreadPool = new QueuedThreadPool(20, 1, 60000);
+ when(mockJettyServer.getThreadPool()).thenReturn(realQueuedThreadPool);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert - Test gauge values
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> idleThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_IDLE_THREAD_NUM);
+ assertTrue(idleThreadsGauge.getValue() >= 0, "Idle threads should be
non-negative");
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> totalThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_TOTAL_THREAD_NUM);
+ assertTrue(totalThreadsGauge.getValue() >= 0, "Total threads should be
non-negative");
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> busyThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_BUSY_THREAD_NUM);
+ assertTrue(busyThreadsGauge.getValue() >= 0, "Busy threads should be
non-negative");
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> queuedRequestsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_QUEUED_REQUEST_NUM);
+ assertTrue(queuedRequestsGauge.getValue() >= 0, "Queued requests should be
non-negative");
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> minThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_MIN_THREAD_NUM);
+ assertEquals(1, minThreadsGauge.getValue().intValue());
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> maxThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_MAX_THREAD_NUM);
+ assertEquals(20, maxThreadsGauge.getValue().intValue());
+ }
+
+ @Test
+ void testGaugeValuesWithNonQueuedThreadPool() {
+ // Arrange
+ when(mockJettyServer.getThreadPool()).thenReturn(mockThreadPool);
+ when(mockThreadPool.getIdleThreads()).thenReturn(3);
+ when(mockThreadPool.getThreads()).thenReturn(8);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert - Test gauge values
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> idleThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_IDLE_THREAD_NUM);
+ assertEquals(3, idleThreadsGauge.getValue().intValue());
+
+ @SuppressWarnings("unchecked")
+ Gauge<Integer> totalThreadsGauge =
+ (Gauge<Integer>)
registry.getGauges().get(MetricNames.SERVER_TOTAL_THREAD_NUM);
+ assertEquals(8, totalThreadsGauge.getValue().intValue());
+ }
+
+ @Test
+ void testResourceConfigRegistration() {
+ // Arrange
+ QueuedThreadPool realQueuedThreadPool = new QueuedThreadPool(10, 5, 60000);
+ when(mockJettyServer.getThreadPool()).thenReturn(realQueuedThreadPool);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert
+ ArgumentCaptor<InstrumentedResourceMethodApplicationListener> captor =
+
ArgumentCaptor.forClass(InstrumentedResourceMethodApplicationListener.class);
+ verify(mockResourceConfig).register(captor.capture());
+
+ InstrumentedResourceMethodApplicationListener listener = captor.getValue();
+ assertNotNull(listener);
+ }
+
+ @Test
+ void testMetricsSourceName() {
+ // Arrange
+ when(mockJettyServer.getThreadPool()).thenReturn(mockThreadPool);
+ when(mockThreadPool.getIdleThreads()).thenReturn(0);
+ when(mockThreadPool.getThreads()).thenReturn(0);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert
+ assertEquals("test-server", metricsSource.getMetricsSourceName());
+ }
+
+ @Test
+ void testGaugeLambdasAreCalled() {
+ // Arrange
+ when(mockJettyServer.getThreadPool()).thenReturn(mockThreadPool);
+ when(mockThreadPool.getIdleThreads()).thenReturn(5);
+ when(mockThreadPool.getThreads()).thenReturn(10);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert - Verify that the gauge lambdas call the thread pool methods
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
+ // Call getValue() on each gauge to trigger the lambda
+ registry.getGauges().get(MetricNames.SERVER_IDLE_THREAD_NUM).getValue();
+ registry.getGauges().get(MetricNames.SERVER_TOTAL_THREAD_NUM).getValue();
+
+ // Verify that the thread pool methods were called
+ verify(mockThreadPool).getIdleThreads();
+ verify(mockThreadPool).getThreads();
+ }
+
+ @Test
+ void testQueuedThreadPoolCast() {
+ // Arrange - Create a real QueuedThreadPool instance
+ QueuedThreadPool realQueuedThreadPool = new QueuedThreadPool(10, 5, 60000);
+ when(mockJettyServer.getThreadPool()).thenReturn(realQueuedThreadPool);
+
+ // Act
+ metricsSource = new HttpServerMetricsSource("test-server",
mockResourceConfig, mockJettyServer);
+
+ // Assert
+ MetricRegistry registry = metricsSource.getMetricRegistry();
+
+ // Verify all metrics are registered including QueuedThreadPool specific
ones
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_IDLE_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_TOTAL_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_BUSY_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_QUEUED_REQUEST_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_MIN_THREAD_NUM));
+
assertTrue(registry.getGauges().containsKey(MetricNames.SERVER_MAX_THREAD_NUM));
+
+ // Verify gauge values are reasonable (not null and non-negative)
+ registry
+ .getGauges()
+ .values()
+ .forEach(
+ gauge -> {
+ Object value = gauge.getValue();
+ assertNotNull(value);
+ if (value instanceof Integer) {
+ assertTrue((Integer) value >= 0, "Gauge value should be
non-negative: " + value);
+ }
+ });
+ }
+}