feat(alerting): DEPLOYMENT_STATE evaluator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
package com.cameleer.server.app.alerting.eval;
|
||||||
|
|
||||||
|
import com.cameleer.server.core.alerting.AlertRule;
|
||||||
|
import com.cameleer.server.core.alerting.ConditionKind;
|
||||||
|
import com.cameleer.server.core.alerting.DeploymentStateCondition;
|
||||||
|
import com.cameleer.server.core.runtime.App;
|
||||||
|
import com.cameleer.server.core.runtime.AppRepository;
|
||||||
|
import com.cameleer.server.core.runtime.Deployment;
|
||||||
|
import com.cameleer.server.core.runtime.DeploymentRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DeploymentStateEvaluator implements ConditionEvaluator<DeploymentStateCondition> {
|
||||||
|
|
||||||
|
private final AppRepository appRepo;
|
||||||
|
private final DeploymentRepository deploymentRepo;
|
||||||
|
|
||||||
|
public DeploymentStateEvaluator(AppRepository appRepo, DeploymentRepository deploymentRepo) {
|
||||||
|
this.appRepo = appRepo;
|
||||||
|
this.deploymentRepo = deploymentRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConditionKind kind() { return ConditionKind.DEPLOYMENT_STATE; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EvalResult evaluate(DeploymentStateCondition c, AlertRule rule, EvalContext ctx) {
|
||||||
|
String appSlug = c.scope() != null ? c.scope().appSlug() : null;
|
||||||
|
App app = (appSlug != null)
|
||||||
|
? appRepo.findByEnvironmentIdAndSlug(rule.environmentId(), appSlug).orElse(null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (app == null) return EvalResult.Clear.INSTANCE;
|
||||||
|
|
||||||
|
Set<String> wanted = Set.copyOf(c.states());
|
||||||
|
List<Deployment> hits = deploymentRepo.findByAppId(app.id()).stream()
|
||||||
|
.filter(d -> wanted.contains(d.status().name()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (hits.isEmpty()) return EvalResult.Clear.INSTANCE;
|
||||||
|
|
||||||
|
Deployment d = hits.get(0);
|
||||||
|
return new EvalResult.Firing(
|
||||||
|
(double) hits.size(), null,
|
||||||
|
Map.of(
|
||||||
|
"deployment", Map.of(
|
||||||
|
"id", d.id().toString(),
|
||||||
|
"status", d.status().name()
|
||||||
|
),
|
||||||
|
"app", Map.of("slug", app.slug())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.cameleer.server.app.alerting.eval;
|
||||||
|
|
||||||
|
import com.cameleer.server.core.alerting.*;
|
||||||
|
import com.cameleer.server.core.runtime.*;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class DeploymentStateEvaluatorTest {
|
||||||
|
|
||||||
|
private AppRepository appRepo;
|
||||||
|
private DeploymentRepository deploymentRepo;
|
||||||
|
private DeploymentStateEvaluator eval;
|
||||||
|
|
||||||
|
private static final UUID ENV_ID = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
||||||
|
private static final UUID RULE_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||||
|
private static final UUID APP_ID = UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc");
|
||||||
|
private static final UUID DEP_ID = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd");
|
||||||
|
private static final Instant NOW = Instant.parse("2026-04-19T10:00:00Z");
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
appRepo = mock(AppRepository.class);
|
||||||
|
deploymentRepo = mock(DeploymentRepository.class);
|
||||||
|
eval = new DeploymentStateEvaluator(appRepo, deploymentRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AlertRule ruleWith(AlertCondition condition) {
|
||||||
|
return new AlertRule(RULE_ID, ENV_ID, "test", null,
|
||||||
|
AlertSeverity.WARNING, true, condition.kind(), condition,
|
||||||
|
60, 0, 0, null, null, List.of(), List.of(),
|
||||||
|
null, null, null, Map.of(), null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private App app(String slug) {
|
||||||
|
return new App(APP_ID, ENV_ID, slug, "Orders", null, NOW.minusSeconds(3600), NOW.minusSeconds(3600));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Deployment deployment(DeploymentStatus status) {
|
||||||
|
return new Deployment(DEP_ID, APP_ID, UUID.randomUUID(), ENV_ID, status,
|
||||||
|
null, null, List.of(), null, null, "orders-0", null,
|
||||||
|
Map.of(), NOW.minusSeconds(60), null, NOW.minusSeconds(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void firesWhenDeploymentInWantedState() {
|
||||||
|
var condition = new DeploymentStateCondition(new AlertScope("orders", null, null), List.of("FAILED"));
|
||||||
|
var rule = ruleWith(condition);
|
||||||
|
when(appRepo.findByEnvironmentIdAndSlug(ENV_ID, "orders")).thenReturn(Optional.of(app("orders")));
|
||||||
|
when(deploymentRepo.findByAppId(APP_ID)).thenReturn(List.of(deployment(DeploymentStatus.FAILED)));
|
||||||
|
|
||||||
|
EvalResult r = eval.evaluate(condition, rule, new EvalContext("default", NOW, new TickCache()));
|
||||||
|
assertThat(r).isInstanceOf(EvalResult.Firing.class);
|
||||||
|
assertThat(((EvalResult.Firing) r).currentValue()).isEqualTo(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearWhenDeploymentNotInWantedState() {
|
||||||
|
var condition = new DeploymentStateCondition(new AlertScope("orders", null, null), List.of("FAILED"));
|
||||||
|
var rule = ruleWith(condition);
|
||||||
|
when(appRepo.findByEnvironmentIdAndSlug(ENV_ID, "orders")).thenReturn(Optional.of(app("orders")));
|
||||||
|
when(deploymentRepo.findByAppId(APP_ID)).thenReturn(List.of(deployment(DeploymentStatus.RUNNING)));
|
||||||
|
|
||||||
|
EvalResult r = eval.evaluate(condition, rule, new EvalContext("default", NOW, new TickCache()));
|
||||||
|
assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearWhenAppNotFound() {
|
||||||
|
var condition = new DeploymentStateCondition(new AlertScope("unknown-app", null, null), List.of("FAILED"));
|
||||||
|
var rule = ruleWith(condition);
|
||||||
|
when(appRepo.findByEnvironmentIdAndSlug(ENV_ID, "unknown-app")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
EvalResult r = eval.evaluate(condition, rule, new EvalContext("default", NOW, new TickCache()));
|
||||||
|
assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearWhenNoDeployments() {
|
||||||
|
var condition = new DeploymentStateCondition(new AlertScope("orders", null, null), List.of("FAILED"));
|
||||||
|
var rule = ruleWith(condition);
|
||||||
|
when(appRepo.findByEnvironmentIdAndSlug(ENV_ID, "orders")).thenReturn(Optional.of(app("orders")));
|
||||||
|
when(deploymentRepo.findByAppId(APP_ID)).thenReturn(List.of());
|
||||||
|
|
||||||
|
EvalResult r = eval.evaluate(condition, rule, new EvalContext("default", NOW, new TickCache()));
|
||||||
|
assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void firesWhenMultipleWantedStates() {
|
||||||
|
var condition = new DeploymentStateCondition(new AlertScope("orders", null, null), List.of("FAILED", "DEGRADED"));
|
||||||
|
var rule = ruleWith(condition);
|
||||||
|
when(appRepo.findByEnvironmentIdAndSlug(ENV_ID, "orders")).thenReturn(Optional.of(app("orders")));
|
||||||
|
when(deploymentRepo.findByAppId(APP_ID)).thenReturn(List.of(deployment(DeploymentStatus.DEGRADED)));
|
||||||
|
|
||||||
|
EvalResult r = eval.evaluate(condition, rule, new EvalContext("default", NOW, new TickCache()));
|
||||||
|
assertThat(r).isInstanceOf(EvalResult.Firing.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void kindIsDeploymentState() {
|
||||||
|
assertThat(eval.kind()).isEqualTo(ConditionKind.DEPLOYMENT_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user