feat(alerting): countExecutionsForAlerting for exchange-match evaluator

Adds AlertMatchSpec record (core) and ClickHouseSearchIndex.countExecutionsForAlerting —
no FINAL, no text subqueries. Filters by tenant, env, app, route, status, time window,
and optional after-cursor. Attributes (JSON string column) use inlined JSONExtractString
key literals since ClickHouse JDBC does not bind ? placeholders inside JSON functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 19:18:49 +02:00
parent 44e91ccdb5
commit 7b79d3aa64
3 changed files with 220 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
package com.cameleer.server.app.search;
import com.cameleer.server.core.alerting.AlertMatchSpec;
import com.cameleer.server.core.search.ExecutionSummary;
import com.cameleer.server.core.search.SearchRequest;
import com.cameleer.server.core.search.SearchResult;
@@ -317,6 +318,54 @@ public class ClickHouseSearchIndex implements SearchIndex {
.replace("_", "\\_");
}
/**
* Counts executions matching the given alerting spec — no {@code FINAL}, no text subqueries.
* Attributes are stored as a JSON string column; use {@code JSONExtractString} for key=value filters.
*/
public long countExecutionsForAlerting(AlertMatchSpec spec) {
List<String> conditions = new ArrayList<>();
List<Object> args = new ArrayList<>();
conditions.add("tenant_id = ?");
args.add(spec.tenantId());
conditions.add("environment = ?");
args.add(spec.environment());
conditions.add("start_time >= ?");
args.add(Timestamp.from(spec.from()));
conditions.add("start_time <= ?");
args.add(Timestamp.from(spec.to()));
if (spec.applicationId() != null) {
conditions.add("application_id = ?");
args.add(spec.applicationId());
}
if (spec.routeId() != null) {
conditions.add("route_id = ?");
args.add(spec.routeId());
}
if (spec.status() != null) {
conditions.add("status = ?");
args.add(spec.status());
}
if (spec.after() != null) {
conditions.add("start_time > ?");
args.add(Timestamp.from(spec.after()));
}
// attributes is a JSON String column. JSONExtractString does not accept a ? placeholder for
// the key argument via ClickHouse JDBC — inline the key as a single-quoted literal.
// Keys originate from internal AlertMatchSpec (evaluator-constructed, not user HTTP input).
for (Map.Entry<String, String> entry : spec.attributes().entrySet()) {
String escapedKey = entry.getKey().replace("'", "\\'");
conditions.add("JSONExtractString(attributes, '" + escapedKey + "') = ?");
args.add(entry.getValue());
}
String sql = "SELECT count() FROM executions WHERE " + String.join(" AND ", conditions); // NO FINAL
Long result = jdbc.queryForObject(sql, Long.class, args.toArray());
return result != null ? result : 0L;
}
@Override
public List<String> distinctAttributeKeys(String environment) {
try {