You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/15 10:15:13 UTC

(rocketmq-dashboard) branch rocketmq-studio updated: fix(audit): record every audit result in the summary's vocabulary (#6541)

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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 13d55c3ad fix(audit): record every audit result in the summary's vocabulary (#6541)
13d55c3ad is described below

commit 13d55c3ada12a63eaeccfbc5165ec7278b40a13a
Author: Apulupie <16...@users.noreply.github.com>
AuthorDate: Thu Oct 15 18:15:07 2026 +0800

    fix(audit): record every audit result in the summary's vocabulary (#6541)
    
    The audit result vocabulary is SUCCESS/FAILED/PARTIAL (docs/api-spec.md) and
    MybatisPlusAuditRepository buckets only those three, but three writers stored other words:
    NotificationOutboxService audited a retry-wait attempt as "RETRYING" (#4657 fixed only the
    exhausted branch next to it) and RocketMQDLQProvider audited a missing DLQ topic as "NOT_FOUND"
    and a resend that matched nothing as "NO_MESSAGES". Every one of those rows was counted in the
    summary's total and in no bucket, so the per-result breakdown silently dropped them.
    
    The domain nuance stays where it belongs: the outbox keeps RETRY_/FAIL_ALERT_NOTIFICATION as
    the operation, the DLQ keeps its documented NO_MESSAGES/PARTIAL/FAILED/SUCCESS response
    outcome and the dlqTopicMissing=true detail marker, and the missing-topic path still answers
    404. Only the audit result column now uses the shared vocabulary - a retry-wait attempt and a
    missing DLQ topic are failed operations, and a resend that matched nothing performed no
    mutation and is not a failure. Rows already stored keep their values, as in #4657.
---
 .../ops/alert/NotificationOutboxService.java       |  8 +++++---
 .../provider/apache/RocketMQDLQProvider.java       | 21 ++++++++++++++-----
 .../ops/alert/NotificationOutboxServiceTest.java   |  8 +++++---
 .../provider/apache/RocketMQDLQProviderTest.java   | 24 +++++++++++++++-------
 4 files changed, 43 insertions(+), 18 deletions(-)

diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
index 8a94ff230..9a44f4560 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
@@ -544,10 +544,12 @@ public class NotificationOutboxService {
                 .set("last_error", abbreviate(error)).set("claim_token", null))) {
             return;
         }
-        // "FAILURE" is outside the shared audit result vocabulary (SUCCESS/FAILED/PARTIAL), so
-        // exhausted deliveries never landed in the audit summary's failed bucket.
+        // The audit result vocabulary is SUCCESS/FAILED/PARTIAL (docs/api-spec.md), and the summary
+        // buckets only those three: "RETRYING" rows were counted in its total but in no bucket.
+        // Both branches audit a failed attempt, and the operation name already tells a scheduled
+        // retry (RETRY_ALERT_NOTIFICATION) from an exhausted one (FAIL_ALERT_NOTIFICATION).
         recordDeliverySafely(row, exhausted ? "FAIL_ALERT_NOTIFICATION" : "RETRY_ALERT_NOTIFICATION",
-                exhausted ? "FAILED" : "RETRYING", abbreviate(error));
+                "FAILED", abbreviate(error));
         log.warn("Alert notification {} for event {}: {}", exhausted ? "failed" : "will retry", row.getAlertId(), error);
     }
 
diff --git a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index efc9bf91b..12a3a5251 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -216,13 +216,13 @@ public class RocketMQDLQProvider implements DLQProvider {
         }
         if (scanResult.topicMissing()) {
             // The group has no %DLQ% topic yet (never dead-lettered a message). Resending is a
-            // mutation against a non-existent target, so surface a clean NOT_FOUND (legacy
-            // dlq.resend semantics) instead of silently reporting a zero-message success.
+            // mutation against a non-existent target, so answer the legacy dlq.resend 404 (audited
+            // with the shared FAILED word) instead of silently reporting a zero-message success.
             String detail = String.format("instanceId=%s, group=%s, dlqTopic=%s, targetTopic=%s, "
                             + "matched=0, resent=0, failed=0, dlqTopicMissing=true",
                     instanceId, groupName, dlqTopic,
                     StringUtils.hasText(targetTopic) ? targetTopic : "<original>");
-            recordAudit(groupName, detail, "NOT_FOUND");
+            recordAudit(groupName, detail, "FAILED");
             throw new BusinessException(404, "No dead-letter queue found for consumer group: " + groupName);
         }
         List<MessageExt> deadLetters = scanResult.messages();
@@ -237,7 +237,7 @@ public class RocketMQDLQProvider implements DLQProvider {
                 instanceId, groupName, dlqTopic, StringUtils.hasText(targetTopic) ? targetTopic : "<original>",
                 deadLetters.size(), resent, failed, scanResult.scanIncomplete(), scanResult.truncated(),
                 scanResult.failedQueueCount(), resendBatch.failures().size(), resendBatch.failuresTruncated());
-        recordAudit(groupName, detail, outcome);
+        recordAudit(groupName, detail, auditResult(outcome));
         log.info("DLQ resend completed: {}", detail);
         return DLQResendResultVO.builder()
                 .matched(deadLetters.size())
@@ -307,7 +307,7 @@ public class RocketMQDLQProvider implements DLQProvider {
                         + "reportedFailures=%d, failuresTruncated=%s",
                 instanceId, groupName, selected.size(), deadLetters.size(), resent, failed,
                 resendBatch.failures().size(), resendBatch.failuresTruncated());
-        recordAudit(groupName, detail, outcome);
+        recordAudit(groupName, detail, auditResult(outcome));
         log.info("Selected DLQ resend completed: {}", detail);
         return DLQResendResultVO.builder()
                 .matched(deadLetters.size())
@@ -812,6 +812,17 @@ public class RocketMQDLQProvider implements DLQProvider {
         return "SUCCESS";
     }
 
+    /**
+     * The resend outcome has its own documented vocabulary (SUCCESS / PARTIAL / FAILED /
+     * NO_MESSAGES, see the api-spec DLQ resend section), but the audit result column has one:
+     * SUCCESS / FAILED / PARTIAL. A resend that matched nothing performed no mutation and is not a
+     * failure, so it audits as SUCCESS while the response still reports NO_MESSAGES; the other
+     * three outcome words already belong to the audit vocabulary.
+     */
+    private static String auditResult(String outcome) {
+        return "NO_MESSAGES".equals(outcome) ? "SUCCESS" : outcome;
+    }
+
     private void recordAudit(String groupName, String detail, String result) {
         try {
             auditService.record("RESEND_DLQ", "DLQ", groupName, null, detail, result);
diff --git a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
index f7c2d390a..e17a2da23 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
@@ -332,8 +332,10 @@ class NotificationOutboxServiceTest {
         server.verify();
         // A char-based cut would keep the high surrogate on its own, so the audit entry - and the
         // last_error the delivery page renders - would show a replacement character instead.
+        // The audit result vocabulary is SUCCESS/FAILED/PARTIAL: a retry-wait attempt is a failed
+        // attempt whose retry is carried by the operation name, not by a fourth result word.
         verify(audit).record("RETRY_ALERT_NOTIFICATION", "ALERT_NOTIFICATION", "8", null,
-                "alertId=9, channel=dingtalk", "RETRYING",
+                "alertId=9, channel=dingtalk", "FAILED",
                 prefix + "x".repeat(999 - prefix.length()) + "\uD83D\uDE00");
     }
 
@@ -368,7 +370,7 @@ class NotificationOutboxServiceTest {
 
         server.verify();
         verify(audit).record("RETRY_ALERT_NOTIFICATION", "ALERT_NOTIFICATION", "8", null,
-                "alertId=9, channel=dingtalk", "RETRYING",
+                "alertId=9, channel=dingtalk", "FAILED",
                 "DingTalk rejected webhook: keywords not in content");
     }
 
@@ -428,7 +430,7 @@ class NotificationOutboxServiceTest {
 
         verify(mapper).update(any(), argThat(wrapper -> wrapper != null));
         verify(audit).record("RETRY_ALERT_NOTIFICATION", "ALERT_NOTIFICATION", "8", null,
-                "alertId=9, channel=dingtalk", "RETRYING", "No configured dingtalk webhook");
+                "alertId=9, channel=dingtalk", "FAILED", "No configured dingtalk webhook");
     }
 
     @Test
diff --git a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index 4374fe9b9..6adc6c4ca 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -495,8 +495,10 @@ class RocketMQDLQProviderTest {
                 "instance-a", "group-a", 100L, 200L, "target-topic"))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(error -> assertThat(((BusinessException) error).getCode()).isEqualTo(404));
+        // A resend against a missing DLQ topic answers 404: the audit result uses the shared
+        // FAILED word and the detail keeps the dlqTopicMissing marker.
         verify(auditService).record(eq("RESEND_DLQ"), eq("DLQ"), eq("group-a"), isNull(),
-                contains("dlqTopicMissing=true"), eq("NOT_FOUND"));
+                contains("dlqTopicMissing=true"), eq("FAILED"));
         verify(runtimeAdminClientResolver, never()).executeProducer(anyString(), any());
     }
 
@@ -710,13 +712,16 @@ class RocketMQDLQProviderTest {
         TopicList existingTargets = new TopicList();
         existingTargets.setTopicList(Set.of("target-topic"));
         when(adminExt.fetchAllTopicList()).thenReturn(existingTargets);
-        provider.resendMessages("instance-a", " group-a ", 100L, 200L, "target-topic");
+        DLQResendResultVO result = provider.resendMessages("instance-a", " group-a ", 100L, 200L, "target-topic");
 
+        // The response keeps its own documented outcome vocabulary; only the audit result maps onto
+        // the summary's buckets, where a no-op resend is not a failure.
+        assertThat(result.getOutcome()).isEqualTo("NO_MESSAGES");
         verify(runtimeAdminClientResolver).executePullConsumer(eq("instance-a"), any());
         verify(pullConsumer).fetchSubscribeMessageQueues(dlqTopic);
         verify(runtimeAdminClientResolver, never()).executeProducer(anyString(), any());
         verify(auditService).record(eq("RESEND_DLQ"), eq("DLQ"), eq("group-a"), eq(null),
-                contains("group=group-a, dlqTopic=%DLQ%group-a"), eq("NO_MESSAGES"));
+                contains("group=group-a, dlqTopic=%DLQ%group-a"), eq("SUCCESS"));
     }
 
     @Test
@@ -726,8 +731,9 @@ class RocketMQDLQProviderTest {
         TopicList existingTargets = new TopicList();
         existingTargets.setTopicList(Set.of("target-topic"));
         when(adminExt.fetchAllTopicList()).thenReturn(existingTargets);
-        provider.resendMessages("instance-a", "group-a", 100L, 200L, "target-topic");
+        DLQResendResultVO result = provider.resendMessages("instance-a", "group-a", 100L, 200L, "target-topic");
 
+        assertThat(result.getOutcome()).isEqualTo("NO_MESSAGES");
         verify(runtimeAdminClientResolver).executePullConsumer(eq("instance-a"), any());
         verify(pullConsumer).fetchSubscribeMessageQueues(dlqTopic);
         verify(pullConsumer, never()).pull(any(MessageQueue.class), anyString(), anyLong(), anyInt());
@@ -738,7 +744,7 @@ class RocketMQDLQProviderTest {
                 eq("group-a"),
                 isNull(),
                 contains("matched=0, resent=0, failed=0"),
-                eq("NO_MESSAGES"));
+                eq("SUCCESS"));
     }
 
     @Test
@@ -763,8 +769,10 @@ class RocketMQDLQProviderTest {
         assertThatThrownBy(() -> provider.resendMessages("instance-a", "group-a", 100L, 200L, null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(error -> assertThat(((BusinessException) error).getCode()).isEqualTo(404));
+        // A resend against a missing DLQ topic answers 404: the audit result uses the shared
+        // FAILED word and the detail keeps the dlqTopicMissing marker.
         verify(auditService).record(eq("RESEND_DLQ"), eq("DLQ"), eq("group-a"), isNull(),
-                contains("dlqTopicMissing=true"), eq("NOT_FOUND"));
+                contains("dlqTopicMissing=true"), eq("FAILED"));
         verify(runtimeAdminClientResolver, never()).executeProducer(anyString(), any());
     }
 
@@ -794,8 +802,10 @@ class RocketMQDLQProviderTest {
         assertThatThrownBy(() -> provider.resendMessages("instance-a", "group-a", 100L, 200L, null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(error -> assertThat(((BusinessException) error).getCode()).isEqualTo(404));
+        // A resend against a missing DLQ topic answers 404: the audit result uses the shared
+        // FAILED word and the detail keeps the dlqTopicMissing marker.
         verify(auditService).record(eq("RESEND_DLQ"), eq("DLQ"), eq("group-a"), isNull(),
-                contains("dlqTopicMissing=true"), eq("NOT_FOUND"));
+                contains("dlqTopicMissing=true"), eq("FAILED"));
         verify(runtimeAdminClientResolver, never()).executeProducer(anyString(), any());
     }