feat(alerting): ROUTE_METRIC evaluator
P95_LATENCY_MS maps to avgDurationMs (ExecutionStats has no p95 bucket). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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.RouteMetricCondition;
|
||||
import com.cameleer.server.core.runtime.EnvironmentRepository;
|
||||
import com.cameleer.server.core.search.ExecutionStats;
|
||||
import com.cameleer.server.core.storage.StatsStore;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class RouteMetricEvaluator implements ConditionEvaluator<RouteMetricCondition> {
|
||||
|
||||
private final StatsStore statsStore;
|
||||
private final EnvironmentRepository envRepo;
|
||||
|
||||
public RouteMetricEvaluator(StatsStore statsStore, EnvironmentRepository envRepo) {
|
||||
this.statsStore = statsStore;
|
||||
this.envRepo = envRepo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionKind kind() { return ConditionKind.ROUTE_METRIC; }
|
||||
|
||||
@Override
|
||||
public EvalResult evaluate(RouteMetricCondition c, AlertRule rule, EvalContext ctx) {
|
||||
Instant from = ctx.now().minusSeconds(c.windowSeconds());
|
||||
Instant to = ctx.now();
|
||||
|
||||
String envSlug = envRepo.findById(rule.environmentId())
|
||||
.map(e -> e.slug())
|
||||
.orElse(null);
|
||||
|
||||
String appSlug = c.scope() != null ? c.scope().appSlug() : null;
|
||||
String routeId = c.scope() != null ? c.scope().routeId() : null;
|
||||
|
||||
ExecutionStats stats;
|
||||
if (routeId != null) {
|
||||
stats = statsStore.statsForRoute(from, to, routeId, appSlug, envSlug);
|
||||
} else if (appSlug != null) {
|
||||
stats = statsStore.statsForApp(from, to, appSlug, envSlug);
|
||||
} else {
|
||||
stats = statsStore.stats(from, to, envSlug);
|
||||
}
|
||||
|
||||
double actual = switch (c.metric()) {
|
||||
case ERROR_RATE -> errorRate(stats);
|
||||
// ExecutionStats has no p95 field; avgDurationMs is the closest available proxy
|
||||
case P95_LATENCY_MS -> (double) stats.avgDurationMs();
|
||||
case P99_LATENCY_MS -> (double) stats.p99LatencyMs();
|
||||
case THROUGHPUT -> (double) stats.totalCount();
|
||||
case ERROR_COUNT -> (double) stats.failedCount();
|
||||
};
|
||||
|
||||
boolean fire = switch (c.comparator()) {
|
||||
case GT -> actual > c.threshold();
|
||||
case GTE -> actual >= c.threshold();
|
||||
case LT -> actual < c.threshold();
|
||||
case LTE -> actual <= c.threshold();
|
||||
case EQ -> actual == c.threshold();
|
||||
};
|
||||
|
||||
if (!fire) return EvalResult.Clear.INSTANCE;
|
||||
|
||||
return new EvalResult.Firing(actual, c.threshold(),
|
||||
Map.of(
|
||||
"route", Map.of("id", routeId == null ? "" : routeId),
|
||||
"app", Map.of("slug", appSlug == null ? "" : appSlug)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private double errorRate(ExecutionStats s) {
|
||||
long total = s.totalCount();
|
||||
return total == 0 ? 0.0 : (double) s.failedCount() / total;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user