From 82e82350f99a93f32ffd0c2bb85ee3d14889dc11 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:12:37 +0200 Subject: [PATCH] refactor(alerts): drop ACKNOWLEDGED from AlertState, add readAt/deletedAt to AlertInstance - AlertState: remove ACKNOWLEDGED case (V17 migration already dropped it from DB enum) - AlertInstance: insert readAt + deletedAt Instant fields after lastNotifiedAt; add withReadAt/withDeletedAt withers; update all existing withers to pass both fields positionally - AlertStateTransitions: add null,null for readAt/deletedAt in newInstance ctor call; collapse FIRING,ACKNOWLEDGED switch arm to just FIRING - AlertScopeTest: update AlertState.values() assertion to 3 values; fix stale ConditionKind.hasSize(6) to 7 (JVM_METRIC was added earlier) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../alerting/eval/AlertStateTransitions.java | 10 +++--- .../server/core/alerting/AlertInstance.java | 34 ++++++++++++++----- .../server/core/alerting/AlertState.java | 2 +- .../server/core/alerting/AlertScopeTest.java | 4 +-- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java index 1e0297f0..22596e32 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java @@ -28,7 +28,7 @@ public final class AlertStateTransitions { /** * Apply an EvalResult to the current open AlertInstance. * - * @param current the open instance for this rule (PENDING / FIRING / ACKNOWLEDGED), or null if none + * @param current the open instance for this rule (PENDING / FIRING), or null if none * @param result the evaluator outcome * @param rule the rule being evaluated * @param now wall-clock instant for the current tick @@ -50,7 +50,7 @@ public final class AlertStateTransitions { private static Optional onClear(AlertInstance current, Instant now) { if (current == null) return Optional.empty(); // no open instance — no-op if (current.state() == AlertState.RESOLVED) return Optional.empty(); // already resolved - // Any open state (PENDING / FIRING / ACKNOWLEDGED) → RESOLVED + // Any open state (PENDING / FIRING) → RESOLVED return Optional.of(current .withState(AlertState.RESOLVED) .withResolvedAt(now)); @@ -84,8 +84,8 @@ public final class AlertStateTransitions { // Still within forDuration — stay PENDING, nothing to persist yield Optional.empty(); } - // FIRING / ACKNOWLEDGED — re-notification cadence handled by the dispatcher - case FIRING, ACKNOWLEDGED -> Optional.empty(); + // FIRING — re-notification cadence handled by the dispatcher + case FIRING -> Optional.empty(); // RESOLVED should never appear as the "current open" instance, but guard anyway case RESOLVED -> Optional.empty(); }; @@ -126,6 +126,8 @@ public final class AlertStateTransitions { null, // ackedBy null, // resolvedAt null, // lastNotifiedAt + null, // readAt + null, // deletedAt false, // silenced f.currentValue(), f.threshold(), diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java index cdc1822b..c2fcfd09 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java @@ -17,6 +17,8 @@ public record AlertInstance( String ackedBy, Instant resolvedAt, Instant lastNotifiedAt, + Instant readAt, // NEW — global "someone has seen this" + Instant deletedAt, // NEW — soft delete boolean silenced, Double currentValue, Double threshold, @@ -39,63 +41,77 @@ public record AlertInstance( public AlertInstance withState(AlertState s) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - s, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + s, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withFiredAt(Instant i) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, i, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, i, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withResolvedAt(Instant i) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, i, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, i, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withAck(String ackedBy, Instant ackedAt) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withSilenced(boolean silenced) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withTitleMessage(String title, String message) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withLastNotifiedAt(Instant instant) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, instant, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, instant, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withContext(Map context) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withRuleSnapshot(Map snapshot) { return new AlertInstance(id, ruleId, snapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, + currentValue, threshold, context, title, message, + targetUserIds, targetGroupIds, targetRoleNames); + } + + public AlertInstance withReadAt(Instant i) { + return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, i, deletedAt, silenced, + currentValue, threshold, context, title, message, + targetUserIds, targetGroupIds, targetRoleNames); + } + + public AlertInstance withDeletedAt(Instant i) { + return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, i, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java index d42d7e03..f3f35feb 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java @@ -1,3 +1,3 @@ package com.cameleer.server.core.alerting; -public enum AlertState { PENDING, FIRING, ACKNOWLEDGED, RESOLVED } +public enum AlertState { PENDING, FIRING, RESOLVED } diff --git a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java index 5713a18a..497a6e22 100644 --- a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java +++ b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java @@ -23,8 +23,8 @@ class AlertScopeTest { assertThat(AlertSeverity.values()).containsExactly( AlertSeverity.CRITICAL, AlertSeverity.WARNING, AlertSeverity.INFO); assertThat(AlertState.values()).containsExactly( - AlertState.PENDING, AlertState.FIRING, AlertState.ACKNOWLEDGED, AlertState.RESOLVED); - assertThat(ConditionKind.values()).hasSize(6); + AlertState.PENDING, AlertState.FIRING, AlertState.RESOLVED); + assertThat(ConditionKind.values()).hasSize(7); assertThat(TargetKind.values()).containsExactly( TargetKind.USER, TargetKind.GROUP, TargetKind.ROLE); assertThat(NotificationStatus.values()).containsExactly(