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());
}