feat(alerting): silence matcher for notification-time dispatch

SilenceMatcherService.matches() evaluates AND semantics across ruleId,
severity, appSlug, routeId, agentId constraints. Null fields are wildcards.
Scope-based constraints (appSlug/routeId/agentId) return false when rule is
null (deleted rule — scope cannot be verified). 17 unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 19:27:18 +02:00
parent 1c74ab8541
commit 891c7f87e3
2 changed files with 246 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
package com.cameleer.server.app.alerting.notify;
import com.cameleer.server.core.alerting.AlertInstance;
import com.cameleer.server.core.alerting.AlertRule;
import com.cameleer.server.core.alerting.SilenceMatcher;
import org.springframework.stereotype.Component;
/**
* Evaluates whether an active silence matches an alert instance at notification-dispatch time.
* <p>
* Each non-null field on the matcher is an additional AND constraint. A null field is a wildcard.
* Matching is purely in-process — no I/O.
*/
@Component
public class SilenceMatcherService {
/**
* Returns {@code true} if the silence covers this alert instance.
*
* @param matcher the silence's matching spec (never null)
* @param instance the alert instance to test (never null)
* @param rule the alert rule; may be null when the rule was deleted after instance creation.
* Scope-based matchers (appSlug, routeId, agentId) return false when rule is null
* because the scope cannot be verified.
*/
public boolean matches(SilenceMatcher matcher, AlertInstance instance, AlertRule rule) {
// ruleId constraint
if (matcher.ruleId() != null && !matcher.ruleId().equals(instance.ruleId())) {
return false;
}
// severity constraint
if (matcher.severity() != null && matcher.severity() != instance.severity()) {
return false;
}
// scope-based constraints require the rule to derive scope from
boolean needsScope = matcher.appSlug() != null || matcher.routeId() != null || matcher.agentId() != null;
if (needsScope && rule == null) {
return false;
}
if (rule != null && rule.condition() != null) {
var scope = rule.condition().scope();
if (matcher.appSlug() != null && !matcher.appSlug().equals(scope.appSlug())) {
return false;
}
if (matcher.routeId() != null && !matcher.routeId().equals(scope.routeId())) {
return false;
}
if (matcher.agentId() != null && !matcher.agentId().equals(scope.agentId())) {
return false;
}
}
return true;
}
}