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;
|
package com.cameleer.server.core.alerting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
public record AlertScope(String appSlug, String routeId, String agentId) {
|
public record AlertScope(String appSlug, String routeId, String agentId) {
|
||||||
|
@JsonIgnore
|
||||||
public boolean isEnvWide() { return appSlug == null && routeId == null && agentId == null; }
|
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