This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 5961490ff5 refactor(agent-gateway): clarify MCP call metrics 
observation (#7538)
5961490ff5 is described below

commit 5961490ff5930139e92c4fe84f0f7f1f011aecf7
Author: BobSong <[email protected]>
AuthorDate: Sun Oct 11 16:45:23 2026 +0800

    refactor(agent-gateway): clarify MCP call metrics observation (#7538)
    
    Co-authored-by: BobSong-dev <[email protected]>
---
 .../gateway/protocol/AgentMcpCallObservation.java  |  4 +-
 .../gateway/protocol/AgentMcpHttpHandler.java      | 11 +++--
 .../protocol/AgentMcpCallObservationTest.java      | 51 ++++++++++++++++++++--
 .../shenyu/plugin/metrics/MetricsPlugin.java       |  1 +
 .../metrics/reporter/MetricsReporterTest.java      | 19 +++++---
 5 files changed, 73 insertions(+), 13 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservation.java
 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservation.java
index 2e47c4354d..ab997a904f 100644
--- 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservation.java
+++ 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservation.java
@@ -46,8 +46,8 @@ final class AgentMcpCallObservation {
 
     private volatile Outcome outcome = Outcome.SERVER_ERROR;
 
-    AgentMcpCallObservation(final Object observer) {
-        this.observer = observer instanceof AgentMcpCallObserver ? 
(AgentMcpCallObserver) observer : null;
+    AgentMcpCallObservation(final AgentMcpCallObserver observer) {
+        this.observer = observer;
     }
 
     void parsed(final AgentMcpRequest request) {
diff --git 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpHttpHandler.java
 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpHttpHandler.java
index b0edc084dc..354d1765de 100644
--- 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpHttpHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpHttpHandler.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.shenyu.common.constant.Constants;
 import org.apache.shenyu.common.dto.AgentGatewayMcpConfig;
+import org.apache.shenyu.common.metrics.AgentMcpCallObserver;
 import org.apache.shenyu.plugin.agent.gateway.AgentTrafficContext;
 import 
org.apache.shenyu.plugin.agent.gateway.security.AgentMcpSecurityResolver;
 import org.springframework.core.io.buffer.DataBuffer;
@@ -80,7 +81,8 @@ public final class AgentMcpHttpHandler {
                              final AgentTrafficContext traffic, final long 
configurationVersion) {
         return Mono.defer(() -> {
             final Instant deadline = 
Instant.now().plusMillis(config.getTimeoutMs());
-            final AgentMcpCallObservation observation = new 
AgentMcpCallObservation(exchange.getAttribute(Constants.METRICS_AGENT_MCP_CALL));
+            Object callback = 
exchange.getAttribute(Constants.METRICS_AGENT_MCP_CALL);
+            final AgentMcpCallObservation observation = new 
AgentMcpCallObservation(callback instanceof AgentMcpCallObserver observer ? 
observer : null);
             AtomicReference<JsonNode> rpcId = new AtomicReference<>();
             return Mono.fromRunnable(() -> preflight(exchange, config))
                     .then(Mono.defer(() -> securityResolver.resolve(exchange))
@@ -97,8 +99,11 @@ public final class AgentMcpHttpHandler {
                         return write(exchange, response, config, 
"sse".equals(config.getResponseMode()), 200);
                     })
                     .timeout(Duration.ofMillis(config.getTimeoutMs()))
-                    .doOnError(observation::failure)
-                    .onErrorResume(error -> handleFailure(exchange, config, 
rpcId.get(), error))
+                    .onErrorResume(error -> {
+                        observation.failure(error);
+                        return handleFailure(exchange, config, rpcId.get(), 
error);
+                    })
+                    // Recovery writes can fail independently of the execution 
error classified above.
                     .doOnError(observation::failure)
                     .doFinally(observation::finish);
         });
diff --git 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservationTest.java
 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservationTest.java
index dbde556e0f..ccad4dad40 100644
--- 
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservationTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/protocol/AgentMcpCallObservationTest.java
@@ -158,6 +158,48 @@ class AgentMcpCallObservationTest {
         observer.assertOnly(Outcome.SERVER_ERROR);
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"json", "sse"})
+    void errorResponseWriteFailureOverridesOriginalRejection(final String 
mode) {
+        Recording observer = new Recording();
+        AtomicInteger invocations = new AtomicInteger();
+        MockServerWebExchange exchange = exchange("tools/call", observer);
+        exchange.getResponse().setWriteHandler(buffers -> 
buffers.doOnNext(DataBufferUtils::release)
+                .then(Mono.error(new 
IllegalStateException("private-error-response-write"))));
+        AgentMcpHttpHandler handler = handler(tool(Mono.defer(() -> {
+            invocations.incrementAndGet();
+            return Mono.just(new JsonObject());
+        })), Set.of());
+        StepVerifier.create(handler.handle(exchange, config(mode, 30000), 
traffic, 1))
+                .expectError(IllegalStateException.class).verify();
+        observer.assertOnly(Outcome.SERVER_ERROR);
+        assertEquals(0, invocations.get());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"json", "sse"})
+    void errorResponseWriteTimeoutOverridesOriginalRejection(final String 
mode) {
+        Recording observer = new Recording();
+        MockServerWebExchange exchange = exchange("tools/call", observer);
+        exchange.getResponse().setWriteHandler(buffers -> 
buffers.doOnNext(DataBufferUtils::release).then(Mono.never()));
+        AgentMcpHttpHandler handler = handler(tool(Mono.just(new 
JsonObject())), Set.of());
+        StepVerifier.withVirtualTime(() -> handler.handle(exchange, 
config(mode, 100), traffic, 1))
+                .then(() -> assertTrue(observer.outcomes.isEmpty()))
+                
.thenAwait(Duration.ofMillis(101)).expectError(TimeoutException.class).verify();
+        observer.assertOnly(Outcome.TIMEOUT);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"json", "sse"})
+    void cancellingErrorResponseWriteOverridesOriginalRejection(final String 
mode) {
+        Recording observer = new Recording();
+        MockServerWebExchange exchange = exchange("tools/call", observer);
+        exchange.getResponse().setWriteHandler(buffers -> 
buffers.doOnNext(DataBufferUtils::release).then(Mono.never()));
+        StepVerifier.create(handler(tool(Mono.just(new JsonObject())), 
Set.of()).handle(exchange, config(mode, 30000), traffic, 1))
+                .then(() -> 
assertTrue(observer.outcomes.isEmpty())).thenCancel().verify();
+        observer.assertOnly(Outcome.CANCELLED);
+    }
+
     @ParameterizedTest
     @ValueSource(strings = {"json", "sse"})
     void recordsResponseLimitFailureWithoutLeakingSuccess(final String mode) {
@@ -274,10 +316,13 @@ class AgentMcpCallObservationTest {
         observer.assertOnly(Outcome.CANCELLED);
     }
 
-    @Test
-    void absentOrWrongTypeCallbackDoesNotRequireMetricsPlugin() {
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void absentOrWrongTypeCallbackDoesNotRequireMetricsPlugin(final boolean 
wrongType) {
         MockServerWebExchange exchange = exchange("tools/call", null);
-        exchange.getAttributes().put(Constants.METRICS_AGENT_MCP_CALL, 
"unusable-callback");
+        if (wrongType) {
+            exchange.getAttributes().put(Constants.METRICS_AGENT_MCP_CALL, 
"unusable-callback");
+        }
         StepVerifier.create(handler(tool(Mono.just(new JsonObject())), 
Set.of("read"))
                 .handle(exchange, config("json", 30000), traffic, 
1)).verifyComplete();
         assertEquals(200, exchange.getResponse().getStatusCode().value());
diff --git 
a/shenyu-plugin/shenyu-plugin-metrics/src/main/java/org/apache/shenyu/plugin/metrics/MetricsPlugin.java
 
b/shenyu-plugin/shenyu-plugin-metrics/src/main/java/org/apache/shenyu/plugin/metrics/MetricsPlugin.java
index a6527250a6..e20f28c951 100644
--- 
a/shenyu-plugin/shenyu-plugin-metrics/src/main/java/org/apache/shenyu/plugin/metrics/MetricsPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-metrics/src/main/java/org/apache/shenyu/plugin/metrics/MetricsPlugin.java
@@ -58,6 +58,7 @@ public class MetricsPlugin implements ShenyuPlugin {
     }
 
     private void setMetricsCallbacks(final ServerWebExchange exchange) {
+        // Metrics must run before Agent Gateway so its per-subscription 
observation can capture this callback.
         exchange.getAttributes().put(Constants.METRICS_AGENT_MCP_CALL, 
(AgentMcpCallObserver) (outcome, millis) -> {
             String[] labels = {outcome.name().toLowerCase(Locale.ROOT)};
             MetricsReporter.counterIncrement(LabelNames.AGENT_MCP_CALLS_TOTAL, 
labels);
diff --git 
a/shenyu-plugin/shenyu-plugin-metrics/src/test/java/org/apache/shenyu/plugin/metrics/reporter/MetricsReporterTest.java
 
b/shenyu-plugin/shenyu-plugin-metrics/src/test/java/org/apache/shenyu/plugin/metrics/reporter/MetricsReporterTest.java
index 023635a2d9..057aaa77d0 100644
--- 
a/shenyu-plugin/shenyu-plugin-metrics/src/test/java/org/apache/shenyu/plugin/metrics/reporter/MetricsReporterTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-metrics/src/test/java/org/apache/shenyu/plugin/metrics/reporter/MetricsReporterTest.java
@@ -22,6 +22,7 @@ import io.prometheus.client.Gauge;
 import io.prometheus.client.Histogram;
 import org.apache.shenyu.common.utils.ReflectUtils;
 import org.apache.shenyu.plugin.metrics.config.Metric;
+import org.apache.shenyu.plugin.metrics.constant.LabelNames;
 import org.apache.shenyu.plugin.metrics.enums.MetricType;
 import org.apache.shenyu.plugin.metrics.prometheus.PrometheusMetricsRegister;
 import org.apache.shenyu.plugin.metrics.spi.MetricsRegister;
@@ -58,9 +59,15 @@ public final class MetricsReporterTest {
     @Test
     public void testRegister() throws Exception {
         Map<String, Counter> map1 = getPrivateField(metricsRegister, 
"COUNTER_MAP", Map.class);
-        Assertions.assertEquals(map1.size(), 10);
+        for (String name : List.of(LabelNames.REQUEST_TOTAL, 
LabelNames.REQUEST_TYPE_TOTAL, LabelNames.REQUEST_THROW_TOTAL,
+                LabelNames.AGENT_MCP_CALLS_TOTAL, 
LabelNames.SENTINEL_REQUEST_RESTRICT_TOTAL, 
LabelNames.SENTINEL_REQUEST_CIRCUITBREAKER_TOTAL,
+                LabelNames.RESILIENCE4J_REQUEST_RESTRICT_TOTAL, 
LabelNames.RESILIENCE4J_REQUEST_CIRCUITBREAKER_TOTAL,
+                LabelNames.HYSTRIX_REQUEST_CIRCUITBREAKER_TOTAL, 
LabelNames.RATELIMITER_REQUEST_RESTRICT_TOTAL)) {
+            Assertions.assertNotNull(map1.get(name), name);
+        }
         Map<String, Histogram> map2 = getPrivateField(metricsRegister, 
"HISTOGRAM_MAP", Map.class);
-        Assertions.assertEquals(map2.size(), 4);
+        Assertions.assertNotNull(map2.get(LabelNames.EXECUTE_LATENCY_NAME));
+        Assertions.assertNotNull(map2.get(LabelNames.AGENT_MCP_CALL_LATENCY));
         List<String> labels = new ArrayList<>();
         labels.add("shenyu_request_total");
         Collection<Metric> metrics = new ArrayList<>();
@@ -69,11 +76,13 @@ public final class MetricsReporterTest {
         metrics.add(new Metric(MetricType.HISTOGRAM, "name3", DOCUMENT, 
labels));
         MetricsReporter.registerMetrics(metrics);
         Map<String, Counter> map3 = getPrivateField(metricsRegister, 
"COUNTER_MAP", Map.class);
-        Assertions.assertEquals(map3.size(), 11);
+        Assertions.assertNotNull(map3.get("name1"));
+        Assertions.assertFalse(map3.containsKey("name2"));
         Map<String, Histogram> map4 = getPrivateField(metricsRegister, 
"HISTOGRAM_MAP", Map.class);
-        Assertions.assertEquals(map4.size(), 5);
+        Assertions.assertNotNull(map4.get("name3"));
+        Assertions.assertFalse(map4.containsKey("name2"));
         Map<String, Gauge> map5 = getPrivateField(metricsRegister, 
"GAUGE_MAP", Map.class);
-        Assertions.assertEquals(map5.size(), 3);
+        Assertions.assertNotNull(map5.get("name2"));
     }
 
     @Test

Reply via email to