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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 17:12:37 +02:00
parent e95c21d0cb
commit 82e82350f9
4 changed files with 34 additions and 16 deletions

View File

@@ -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<AlertInstance> 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(),

View File

@@ -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<String, Object> 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<String, Object> 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);
}

View File

@@ -1,3 +1,3 @@
package com.cameleer.server.core.alerting;
public enum AlertState { PENDING, FIRING, ACKNOWLEDGED, RESOLVED }
public enum AlertState { PENDING, FIRING, RESOLVED }

View File

@@ -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(