feat(alerting): sealed AlertCondition hierarchy with Jackson deduction
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record AgentStateCondition(AlertScope scope, String state, int forSeconds) implements AlertCondition {
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.AGENT_STATE; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = RouteMetricCondition.class, name = "ROUTE_METRIC"),
|
||||
@JsonSubTypes.Type(value = ExchangeMatchCondition.class, name = "EXCHANGE_MATCH"),
|
||||
@JsonSubTypes.Type(value = AgentStateCondition.class, name = "AGENT_STATE"),
|
||||
@JsonSubTypes.Type(value = DeploymentStateCondition.class, name = "DEPLOYMENT_STATE"),
|
||||
@JsonSubTypes.Type(value = LogPatternCondition.class, name = "LOG_PATTERN"),
|
||||
@JsonSubTypes.Type(value = JvmMetricCondition.class, name = "JVM_METRIC")
|
||||
})
|
||||
public sealed interface AlertCondition permits
|
||||
RouteMetricCondition, ExchangeMatchCondition, AgentStateCondition,
|
||||
DeploymentStateCondition, LogPatternCondition, JvmMetricCondition {
|
||||
|
||||
@JsonProperty("kind")
|
||||
ConditionKind kind();
|
||||
AlertScope scope();
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public record AlertScope(String appSlug, String routeId, String agentId) {
|
||||
@JsonIgnore
|
||||
public boolean isEnvWide() { return appSlug == null && routeId == null && agentId == null; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record DeploymentStateCondition(AlertScope scope, List<String> states) implements AlertCondition {
|
||||
public DeploymentStateCondition { states = List.copyOf(states); }
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.DEPLOYMENT_STATE; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record ExchangeMatchCondition(
|
||||
AlertScope scope,
|
||||
ExchangeFilter filter,
|
||||
FireMode fireMode,
|
||||
Integer threshold, // required when COUNT_IN_WINDOW; null for PER_EXCHANGE
|
||||
Integer windowSeconds, // required when COUNT_IN_WINDOW
|
||||
Integer perExchangeLingerSeconds // required when PER_EXCHANGE
|
||||
) implements AlertCondition {
|
||||
|
||||
public ExchangeMatchCondition {
|
||||
if (fireMode == FireMode.COUNT_IN_WINDOW && (threshold == null || windowSeconds == null))
|
||||
throw new IllegalArgumentException("COUNT_IN_WINDOW requires threshold + windowSeconds");
|
||||
if (fireMode == FireMode.PER_EXCHANGE && perExchangeLingerSeconds == null)
|
||||
throw new IllegalArgumentException("PER_EXCHANGE requires perExchangeLingerSeconds");
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.EXCHANGE_MATCH; }
|
||||
|
||||
public record ExchangeFilter(String status, Map<String, String> attributes) {
|
||||
public ExchangeFilter { attributes = attributes == null ? Map.of() : Map.copyOf(attributes); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record JvmMetricCondition(
|
||||
AlertScope scope,
|
||||
String metric,
|
||||
AggregationOp aggregation,
|
||||
Comparator comparator,
|
||||
double threshold,
|
||||
int windowSeconds) implements AlertCondition {
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.JVM_METRIC; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record LogPatternCondition(
|
||||
AlertScope scope,
|
||||
String level,
|
||||
String pattern,
|
||||
int threshold,
|
||||
int windowSeconds) implements AlertCondition {
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.LOG_PATTERN; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record RouteMetricCondition(
|
||||
AlertScope scope,
|
||||
RouteMetric metric,
|
||||
Comparator comparator,
|
||||
double threshold,
|
||||
int windowSeconds) implements AlertCondition {
|
||||
@Override
|
||||
@JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY)
|
||||
public ConditionKind kind() { return ConditionKind.ROUTE_METRIC; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.cameleer.server.core.alerting;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AlertConditionJsonTest {
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void roundtripRouteMetric() throws Exception {
|
||||
var c = new RouteMetricCondition(
|
||||
new AlertScope("orders", "route-1", null),
|
||||
RouteMetric.P99_LATENCY_MS, Comparator.GT, 2000.0, 300);
|
||||
String json = om.writeValueAsString((AlertCondition) c);
|
||||
AlertCondition parsed = om.readValue(json, AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(RouteMetricCondition.class);
|
||||
assertThat(parsed.kind()).isEqualTo(ConditionKind.ROUTE_METRIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripExchangeMatchPerExchange() throws Exception {
|
||||
var c = new ExchangeMatchCondition(
|
||||
new AlertScope("orders", null, null),
|
||||
new ExchangeMatchCondition.ExchangeFilter("FAILED", Map.of("type","payment")),
|
||||
FireMode.PER_EXCHANGE, null, null, 300);
|
||||
String json = om.writeValueAsString((AlertCondition) c);
|
||||
AlertCondition parsed = om.readValue(json, AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(ExchangeMatchCondition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripExchangeMatchCountInWindow() throws Exception {
|
||||
var c = new ExchangeMatchCondition(
|
||||
new AlertScope("orders", null, null),
|
||||
new ExchangeMatchCondition.ExchangeFilter("FAILED", Map.of()),
|
||||
FireMode.COUNT_IN_WINDOW, 5, 900, null);
|
||||
AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class);
|
||||
assertThat(((ExchangeMatchCondition) parsed).threshold()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripAgentState() throws Exception {
|
||||
var c = new AgentStateCondition(new AlertScope("orders", null, null), "DEAD", 60);
|
||||
AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(AgentStateCondition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripDeploymentState() throws Exception {
|
||||
var c = new DeploymentStateCondition(new AlertScope("orders", null, null), List.of("FAILED","DEGRADED"));
|
||||
AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(DeploymentStateCondition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripLogPattern() throws Exception {
|
||||
var c = new LogPatternCondition(new AlertScope("orders", null, null),
|
||||
"ERROR", "TimeoutException", 5, 900);
|
||||
AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(LogPatternCondition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripJvmMetric() throws Exception {
|
||||
var c = new JvmMetricCondition(new AlertScope("orders", null, null),
|
||||
"heap_used_percent", AggregationOp.MAX, Comparator.GT, 90.0, 300);
|
||||
AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class);
|
||||
assertThat(parsed).isInstanceOf(JvmMetricCondition.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user