feat(alerting): Postgres repository for alert_rules
Implements AlertRuleRepository with JSONB condition/webhooks/eval_state serialization via ObjectMapper, UPSERT on conflict, JSONB containment query for findRuleIdsByOutboundConnectionId, and FOR UPDATE SKIP LOCKED claim-polling for horizontal scale. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package com.cameleer.server.app.alerting.storage;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.core.alerting.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PostgresAlertRuleRepositoryIT extends AbstractPostgresIT {
|
||||
|
||||
private PostgresAlertRuleRepository repo;
|
||||
private UUID envId;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
repo = new PostgresAlertRuleRepository(jdbcTemplate, new ObjectMapper());
|
||||
envId = UUID.randomUUID();
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
|
||||
envId, "test-env-" + UUID.randomUUID(), "Test Env");
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, email) VALUES ('test-user', 'local', 'test@example.com')" +
|
||||
" ON CONFLICT (user_id) DO NOTHING");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", envId);
|
||||
jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
|
||||
jdbcTemplate.update("DELETE FROM users WHERE user_id = 'test-user'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAndFindByIdRoundtrip() {
|
||||
var rule = newRule(List.of());
|
||||
repo.save(rule);
|
||||
var found = repo.findById(rule.id()).orElseThrow();
|
||||
assertThat(found.name()).isEqualTo(rule.name());
|
||||
assertThat(found.condition()).isInstanceOf(AgentStateCondition.class);
|
||||
assertThat(found.severity()).isEqualTo(AlertSeverity.WARNING);
|
||||
assertThat(found.conditionKind()).isEqualTo(ConditionKind.AGENT_STATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findRuleIdsByOutboundConnectionId() {
|
||||
var connId = UUID.randomUUID();
|
||||
var wb = new WebhookBinding(UUID.randomUUID(), connId, null, Map.of());
|
||||
var rule = newRule(List.of(wb));
|
||||
repo.save(rule);
|
||||
|
||||
List<UUID> ids = repo.findRuleIdsByOutboundConnectionId(connId);
|
||||
assertThat(ids).containsExactly(rule.id());
|
||||
|
||||
assertThat(repo.findRuleIdsByOutboundConnectionId(UUID.randomUUID())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimDueRulesAtomicSkipLocked() {
|
||||
var rule = newRule(List.of());
|
||||
repo.save(rule);
|
||||
|
||||
List<AlertRule> claimed = repo.claimDueRules("instance-A", 10, 30);
|
||||
assertThat(claimed).hasSize(1);
|
||||
|
||||
// Second claimant sees nothing until first releases or TTL expires
|
||||
List<AlertRule> second = repo.claimDueRules("instance-B", 10, 30);
|
||||
assertThat(second).isEmpty();
|
||||
}
|
||||
|
||||
private AlertRule newRule(List<WebhookBinding> webhooks) {
|
||||
return new AlertRule(
|
||||
UUID.randomUUID(), envId, "rule-" + UUID.randomUUID(), "desc",
|
||||
AlertSeverity.WARNING, true, ConditionKind.AGENT_STATE,
|
||||
new AgentStateCondition(new AlertScope(null, null, null), "DEAD", 60),
|
||||
60, 0, 60, "t", "m", webhooks, List.of(),
|
||||
Instant.now().minusSeconds(10), null, null, Map.of(),
|
||||
Instant.now(), "test-user", Instant.now(), "test-user");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user