feat: add environment filtering across all APIs and UI
Backend: Added optional `environment` query parameter to catalog, search, stats, timeseries, punchcard, top-errors, logs, and agents endpoints. ClickHouse queries filter by environment when specified (literal SQL for AggregatingMergeTree, ? binds for raw tables). StatsStore interface methods all accept environment parameter. UI: Added EnvironmentSelector component (compact native select). LayoutShell extracts distinct environments from agent data and passes selected environment to catalog and agent queries via URL search param (?env=). TopBar shows current environment label. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -271,7 +271,8 @@ public class AgentRegistrationController {
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<List<AgentInstanceResponse>> listAgents(
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String application) {
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String environment) {
|
||||
List<AgentInfo> agents;
|
||||
|
||||
if (status != null) {
|
||||
@@ -292,6 +293,13 @@ public class AgentRegistrationController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Apply environment filter if specified
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
agents = agents.stream()
|
||||
.filter(a -> environment.equals(a.environmentId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Enrich with runtime metrics from continuous aggregates
|
||||
Map<String, double[]> agentMetrics = queryAgentMetrics();
|
||||
final List<AgentInfo> finalAgents = agents;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class LogQueryController {
|
||||
@RequestParam(name = "agentId", required = false) String instanceId,
|
||||
@RequestParam(required = false) String exchangeId,
|
||||
@RequestParam(required = false) String logger,
|
||||
@RequestParam(required = false) String environment,
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to,
|
||||
@RequestParam(required = false) String cursor,
|
||||
@@ -63,7 +64,7 @@ public class LogQueryController {
|
||||
|
||||
LogSearchRequest request = new LogSearchRequest(
|
||||
searchText, levels, application, instanceId, exchangeId,
|
||||
logger, fromInstant, toInstant, cursor, limit, sort);
|
||||
logger, environment, fromInstant, toInstant, cursor, limit, sort);
|
||||
|
||||
LogSearchResponse result = logIndex.search(request);
|
||||
|
||||
|
||||
@@ -59,9 +59,17 @@ public class RouteCatalogController {
|
||||
@ApiResponse(responseCode = "200", description = "Catalog returned")
|
||||
public ResponseEntity<List<AppCatalogEntry>> getCatalog(
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to) {
|
||||
@RequestParam(required = false) String to,
|
||||
@RequestParam(required = false) String environment) {
|
||||
List<AgentInfo> allAgents = registryService.findAll();
|
||||
|
||||
// Filter agents by environment if specified
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
allAgents = allAgents.stream()
|
||||
.filter(a -> environment.equals(a.environmentId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Group agents by application name
|
||||
Map<String, List<AgentInfo>> agentsByApp = allAgents.stream()
|
||||
.collect(Collectors.groupingBy(AgentInfo::applicationId, LinkedHashMap::new, Collectors.toList()));
|
||||
@@ -87,9 +95,12 @@ public class RouteCatalogController {
|
||||
Map<String, Long> routeExchangeCounts = new LinkedHashMap<>();
|
||||
Map<String, Instant> routeLastSeen = new LinkedHashMap<>();
|
||||
try {
|
||||
String envFilter = (environment != null && !environment.isBlank())
|
||||
? " AND environment = " + lit(environment) : "";
|
||||
jdbc.query(
|
||||
"SELECT application_id, route_id, countMerge(total_count) AS cnt, MAX(bucket) AS last_seen " +
|
||||
"FROM stats_1m_route WHERE bucket >= " + lit(rangeFrom) + " AND bucket < " + lit(rangeTo) +
|
||||
envFilter +
|
||||
" GROUP BY application_id, route_id",
|
||||
rs -> {
|
||||
String key = rs.getString("application_id") + "/" + rs.getString("route_id");
|
||||
@@ -169,6 +180,11 @@ public class RouteCatalogController {
|
||||
.format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'";
|
||||
}
|
||||
|
||||
/** Format a string as a ClickHouse SQL literal with backslash + quote escaping. */
|
||||
private static String lit(String value) {
|
||||
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'";
|
||||
}
|
||||
|
||||
private String computeWorstHealth(List<AgentInfo> agents) {
|
||||
boolean hasDead = false;
|
||||
boolean hasStale = false;
|
||||
|
||||
@@ -115,7 +115,7 @@ public class RouteMetricsController {
|
||||
.map(AppSettings::slaThresholdMs).orElse(300);
|
||||
|
||||
Map<String, long[]> slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant,
|
||||
effectiveAppId, threshold);
|
||||
effectiveAppId, threshold, null);
|
||||
|
||||
for (int i = 0; i < metrics.size(); i++) {
|
||||
RouteMetrics m = metrics.get(i);
|
||||
|
||||
@@ -60,6 +60,7 @@ public class SearchController {
|
||||
@RequestParam(name = "agentId", required = false) String instanceId,
|
||||
@RequestParam(required = false) String processorType,
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String environment,
|
||||
@RequestParam(defaultValue = "0") int offset,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(required = false) String sortField,
|
||||
@@ -75,7 +76,8 @@ public class SearchController {
|
||||
routeId, instanceId, processorType,
|
||||
application, agentIds,
|
||||
offset, limit,
|
||||
sortField, sortDir
|
||||
sortField, sortDir,
|
||||
environment
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(searchService.search(request));
|
||||
@@ -100,23 +102,24 @@ public class SearchController {
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String application) {
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String environment) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
ExecutionStats stats;
|
||||
if (routeId == null && application == null) {
|
||||
stats = searchService.stats(from, end);
|
||||
stats = searchService.stats(from, end, environment);
|
||||
} else if (routeId == null) {
|
||||
stats = searchService.statsForApp(from, end, application);
|
||||
stats = searchService.statsForApp(from, end, application, environment);
|
||||
} else {
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
stats = searchService.stats(from, end, routeId, agentIds);
|
||||
stats = searchService.stats(from, end, routeId, agentIds, environment);
|
||||
}
|
||||
|
||||
// Enrich with SLA compliance
|
||||
int threshold = appSettingsRepository
|
||||
.findByApplicationId(application != null ? application : "")
|
||||
.map(AppSettings::slaThresholdMs).orElse(300);
|
||||
double sla = searchService.slaCompliance(from, end, threshold, application, routeId);
|
||||
double sla = searchService.slaCompliance(from, end, threshold, application, routeId, environment);
|
||||
return ResponseEntity.ok(stats.withSlaCompliance(sla));
|
||||
}
|
||||
|
||||
@@ -127,19 +130,20 @@ public class SearchController {
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String application) {
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String environment) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
if (routeId == null && application == null) {
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets));
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, environment));
|
||||
}
|
||||
if (routeId == null) {
|
||||
return ResponseEntity.ok(searchService.timeseriesForApp(from, end, buckets, application));
|
||||
return ResponseEntity.ok(searchService.timeseriesForApp(from, end, buckets, application, environment));
|
||||
}
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
if (routeId == null && agentIds.isEmpty()) {
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets));
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, environment));
|
||||
}
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, routeId, agentIds));
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, routeId, agentIds, environment));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/timeseries/by-app")
|
||||
@@ -147,9 +151,10 @@ public class SearchController {
|
||||
public ResponseEntity<Map<String, StatsTimeseries>> timeseriesByApp(
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets) {
|
||||
@RequestParam(defaultValue = "24") int buckets,
|
||||
@RequestParam(required = false) String environment) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByApp(from, end, buckets));
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByApp(from, end, buckets, environment));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/timeseries/by-route")
|
||||
@@ -158,18 +163,20 @@ public class SearchController {
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets,
|
||||
@RequestParam String application) {
|
||||
@RequestParam String application,
|
||||
@RequestParam(required = false) String environment) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByRoute(from, end, buckets, application));
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByRoute(from, end, buckets, application, environment));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/punchcard")
|
||||
@Operation(summary = "Transaction punchcard: weekday x hour grid (rolling 7 days)")
|
||||
public ResponseEntity<List<StatsStore.PunchcardCell>> punchcard(
|
||||
@RequestParam(required = false) String application) {
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String environment) {
|
||||
Instant to = Instant.now();
|
||||
Instant from = to.minus(java.time.Duration.ofDays(7));
|
||||
return ResponseEntity.ok(searchService.punchcard(from, to, application));
|
||||
return ResponseEntity.ok(searchService.punchcard(from, to, application, environment));
|
||||
}
|
||||
|
||||
@GetMapping("/attributes/keys")
|
||||
@@ -185,9 +192,10 @@ public class SearchController {
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String environment,
|
||||
@RequestParam(defaultValue = "5") int limit) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.topErrors(from, end, application, routeId, limit));
|
||||
return ResponseEntity.ok(searchService.topErrors(from, end, application, routeId, limit, environment));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ public record AgentInstanceResponse(
|
||||
@NotNull String instanceId,
|
||||
@NotNull String displayName,
|
||||
@NotNull String applicationId,
|
||||
String environmentId,
|
||||
@NotNull String status,
|
||||
@NotNull List<String> routeIds,
|
||||
@NotNull Instant registeredAt,
|
||||
@@ -30,6 +31,7 @@ public record AgentInstanceResponse(
|
||||
long uptime = Duration.between(info.registeredAt(), Instant.now()).toSeconds();
|
||||
return new AgentInstanceResponse(
|
||||
info.instanceId(), info.displayName(), info.applicationId(),
|
||||
info.environmentId(),
|
||||
info.state().name(), info.routeIds(),
|
||||
info.registeredAt(), info.lastHeartbeat(),
|
||||
info.version(), info.capabilities(),
|
||||
@@ -41,7 +43,8 @@ public record AgentInstanceResponse(
|
||||
|
||||
public AgentInstanceResponse withMetrics(double tps, double errorRate, int activeRoutes) {
|
||||
return new AgentInstanceResponse(
|
||||
instanceId, displayName, applicationId, status, routeIds, registeredAt, lastHeartbeat,
|
||||
instanceId, displayName, applicationId, environmentId,
|
||||
status, routeIds, registeredAt, lastHeartbeat,
|
||||
version, capabilities,
|
||||
tps, errorRate, activeRoutes, totalRoutes, uptimeSeconds
|
||||
);
|
||||
|
||||
@@ -108,6 +108,11 @@ public class ClickHouseLogStore implements LogIndex {
|
||||
baseConditions.add("tenant_id = ?");
|
||||
baseParams.add(tenantId);
|
||||
|
||||
if (request.environment() != null && !request.environment().isEmpty()) {
|
||||
baseConditions.add("environment = ?");
|
||||
baseParams.add(request.environment());
|
||||
}
|
||||
|
||||
if (request.application() != null && !request.application().isEmpty()) {
|
||||
baseConditions.add("application = ?");
|
||||
baseParams.add(request.application());
|
||||
|
||||
@@ -182,6 +182,11 @@ public class ClickHouseSearchIndex implements SearchIndex {
|
||||
params.add(request.durationMax());
|
||||
}
|
||||
|
||||
if (request.environment() != null && !request.environment().isBlank()) {
|
||||
conditions.add("environment = ?");
|
||||
params.add(request.environment());
|
||||
}
|
||||
|
||||
// Global full-text search: exact ID match, full-text on execution + processor level
|
||||
if (request.text() != null && !request.text().isBlank()) {
|
||||
String term = escapeLike(request.text());
|
||||
|
||||
@@ -42,20 +42,20 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
// ── Stats (aggregate) ────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public ExecutionStats stats(Instant from, Instant to) {
|
||||
return queryStats("stats_1m_all", from, to, List.of(), true);
|
||||
public ExecutionStats stats(Instant from, Instant to, String environment) {
|
||||
return queryStats("stats_1m_all", from, to, List.of(), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionStats statsForApp(Instant from, Instant to, String applicationId) {
|
||||
public ExecutionStats statsForApp(Instant from, Instant to, String applicationId, String environment) {
|
||||
return queryStats("stats_1m_app", from, to, List.of(
|
||||
new Filter("application_id", applicationId)), true);
|
||||
new Filter("application_id", applicationId)), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionStats statsForRoute(Instant from, Instant to, String routeId, List<String> agentIds) {
|
||||
public ExecutionStats statsForRoute(Instant from, Instant to, String routeId, List<String> agentIds, String environment) {
|
||||
return queryStats("stats_1m_route", from, to, List.of(
|
||||
new Filter("route_id", routeId)), true);
|
||||
new Filter("route_id", routeId)), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,21 +66,21 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
// ── Timeseries ───────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public StatsTimeseries timeseries(Instant from, Instant to, int bucketCount) {
|
||||
return queryTimeseries("stats_1m_all", from, to, bucketCount, List.of(), true);
|
||||
public StatsTimeseries timeseries(Instant from, Instant to, int bucketCount, String environment) {
|
||||
return queryTimeseries("stats_1m_all", from, to, bucketCount, List.of(), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String applicationId) {
|
||||
public StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String applicationId, String environment) {
|
||||
return queryTimeseries("stats_1m_app", from, to, bucketCount, List.of(
|
||||
new Filter("application_id", applicationId)), true);
|
||||
new Filter("application_id", applicationId)), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatsTimeseries timeseriesForRoute(Instant from, Instant to, int bucketCount,
|
||||
String routeId, List<String> agentIds) {
|
||||
String routeId, List<String> agentIds, String environment) {
|
||||
return queryTimeseries("stats_1m_route", from, to, bucketCount, List.of(
|
||||
new Filter("route_id", routeId)), true);
|
||||
new Filter("route_id", routeId)), true, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,23 +92,23 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
// ── Grouped timeseries ───────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount) {
|
||||
public Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount, String environment) {
|
||||
return queryGroupedTimeseries("stats_1m_app", "application_id", from, to,
|
||||
bucketCount, List.of());
|
||||
bucketCount, List.of(), environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, StatsTimeseries> timeseriesGroupedByRoute(Instant from, Instant to,
|
||||
int bucketCount, String applicationId) {
|
||||
int bucketCount, String applicationId, String environment) {
|
||||
return queryGroupedTimeseries("stats_1m_route", "route_id", from, to,
|
||||
bucketCount, List.of(new Filter("application_id", applicationId)));
|
||||
bucketCount, List.of(new Filter("application_id", applicationId)), environment);
|
||||
}
|
||||
|
||||
// ── SLA compliance (raw table — prepared statements OK) ──────────────
|
||||
|
||||
@Override
|
||||
public double slaCompliance(Instant from, Instant to, int thresholdMs,
|
||||
String applicationId, String routeId) {
|
||||
String applicationId, String routeId, String environment) {
|
||||
String sql = "SELECT " +
|
||||
"countIf(duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"countIf(status != 'RUNNING') AS total " +
|
||||
@@ -120,6 +120,10 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
params.add(tenantId);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = ?";
|
||||
params.add(environment);
|
||||
}
|
||||
if (applicationId != null) {
|
||||
sql += " AND application_id = ?";
|
||||
params.add(applicationId);
|
||||
@@ -137,37 +141,59 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs) {
|
||||
public Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs, String environment) {
|
||||
String sql = "SELECT application_id, " +
|
||||
"countIf(duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"countIf(status != 'RUNNING') AS total " +
|
||||
"FROM executions FINAL " +
|
||||
"WHERE tenant_id = ? AND start_time >= ? AND start_time < ? " +
|
||||
"GROUP BY application_id";
|
||||
"WHERE tenant_id = ? AND start_time >= ? AND start_time < ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(defaultThresholdMs);
|
||||
params.add(tenantId);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = ?";
|
||||
params.add(environment);
|
||||
}
|
||||
sql += " GROUP BY application_id";
|
||||
|
||||
Map<String, long[]> result = new LinkedHashMap<>();
|
||||
jdbc.query(sql, (rs) -> {
|
||||
result.put(rs.getString("application_id"),
|
||||
new long[]{rs.getLong("compliant"), rs.getLong("total")});
|
||||
}, defaultThresholdMs, tenantId, Timestamp.from(from), Timestamp.from(to));
|
||||
}, params.toArray());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, long[]> slaCountsByRoute(Instant from, Instant to,
|
||||
String applicationId, int thresholdMs) {
|
||||
String applicationId, int thresholdMs, String environment) {
|
||||
String sql = "SELECT route_id, " +
|
||||
"countIf(duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"countIf(status != 'RUNNING') AS total " +
|
||||
"FROM executions FINAL " +
|
||||
"WHERE tenant_id = ? AND start_time >= ? AND start_time < ? " +
|
||||
"AND application_id = ? GROUP BY route_id";
|
||||
"AND application_id = ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(thresholdMs);
|
||||
params.add(tenantId);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
params.add(applicationId);
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = ?";
|
||||
params.add(environment);
|
||||
}
|
||||
sql += " GROUP BY route_id";
|
||||
|
||||
Map<String, long[]> result = new LinkedHashMap<>();
|
||||
jdbc.query(sql, (rs) -> {
|
||||
result.put(rs.getString("route_id"),
|
||||
new long[]{rs.getLong("compliant"), rs.getLong("total")});
|
||||
}, thresholdMs, tenantId, Timestamp.from(from), Timestamp.from(to), applicationId);
|
||||
}, params.toArray());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -175,12 +201,16 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
|
||||
@Override
|
||||
public List<TopError> topErrors(Instant from, Instant to, String applicationId,
|
||||
String routeId, int limit) {
|
||||
String routeId, int limit, String environment) {
|
||||
StringBuilder where = new StringBuilder(
|
||||
"status = 'FAILED' AND start_time >= ? AND start_time < ?");
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
where.append(" AND environment = ?");
|
||||
params.add(environment);
|
||||
}
|
||||
if (applicationId != null) {
|
||||
where.append(" AND application_id = ?");
|
||||
params.add(applicationId);
|
||||
@@ -247,7 +277,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int activeErrorTypes(Instant from, Instant to, String applicationId) {
|
||||
public int activeErrorTypes(Instant from, Instant to, String applicationId, String environment) {
|
||||
String sql = "SELECT COUNT(DISTINCT COALESCE(error_type, substring(error_message, 1, 200))) " +
|
||||
"FROM executions FINAL " +
|
||||
"WHERE tenant_id = ? AND status = 'FAILED' AND start_time >= ? AND start_time < ?";
|
||||
@@ -256,6 +286,10 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
params.add(tenantId);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = ?";
|
||||
params.add(environment);
|
||||
}
|
||||
if (applicationId != null) {
|
||||
sql += " AND application_id = ?";
|
||||
params.add(applicationId);
|
||||
@@ -268,7 +302,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
// ── Punchcard (AggregatingMergeTree — literal SQL) ───────────────────
|
||||
|
||||
@Override
|
||||
public List<PunchcardCell> punchcard(Instant from, Instant to, String applicationId) {
|
||||
public List<PunchcardCell> punchcard(Instant from, Instant to, String applicationId, String environment) {
|
||||
String view = applicationId != null ? "stats_1m_app" : "stats_1m_all";
|
||||
String sql = "SELECT toDayOfWeek(bucket, 1) % 7 AS weekday, " +
|
||||
"toHour(bucket) AS hour, " +
|
||||
@@ -278,6 +312,9 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
" WHERE tenant_id = " + lit(tenantId) +
|
||||
" AND bucket >= " + lit(from) +
|
||||
" AND bucket < " + lit(to);
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = " + lit(environment);
|
||||
}
|
||||
if (applicationId != null) {
|
||||
sql += " AND application_id = " + lit(applicationId);
|
||||
}
|
||||
@@ -294,7 +331,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
|
||||
/**
|
||||
* Format an Instant as a ClickHouse DateTime literal.
|
||||
* Uses java.sql.Timestamp to match the JVM→ClickHouse timezone convention
|
||||
* Uses java.sql.Timestamp to match the JVM-ClickHouse timezone convention
|
||||
* used by the JDBC driver, then truncates to second precision for DateTime
|
||||
* column compatibility.
|
||||
*/
|
||||
@@ -318,7 +355,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
* Build -Merge combinator SQL for the given view and time range.
|
||||
*/
|
||||
private String buildStatsSql(String view, Instant rangeFrom, Instant rangeTo,
|
||||
List<Filter> filters, boolean hasRunning) {
|
||||
List<Filter> filters, boolean hasRunning, String environment) {
|
||||
String runningCol = hasRunning ? "countIfMerge(running_count)" : "0";
|
||||
String sql = "SELECT " +
|
||||
"countMerge(total_count) AS total_count, " +
|
||||
@@ -330,6 +367,9 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
" WHERE tenant_id = " + lit(tenantId) +
|
||||
" AND bucket >= " + lit(rangeFrom) +
|
||||
" AND bucket < " + lit(rangeTo);
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = " + lit(environment);
|
||||
}
|
||||
for (Filter f : filters) {
|
||||
sql += " AND " + f.column() + " = " + lit(f.value());
|
||||
}
|
||||
@@ -341,15 +381,15 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
* Uses literal SQL to avoid ClickHouse JDBC driver PreparedStatement issues.
|
||||
*/
|
||||
private ExecutionStats queryStats(String view, Instant from, Instant to,
|
||||
List<Filter> filters, boolean hasRunning) {
|
||||
List<Filter> filters, boolean hasRunning, String environment) {
|
||||
|
||||
String sql = buildStatsSql(view, from, to, filters, hasRunning);
|
||||
String sql = buildStatsSql(view, from, to, filters, hasRunning, environment);
|
||||
|
||||
long totalCount = 0, failedCount = 0, avgDuration = 0, p99Duration = 0, activeCount = 0;
|
||||
var currentResult = jdbc.query(sql, (rs, rowNum) -> {
|
||||
long tc = rs.getLong("total_count");
|
||||
long fc = rs.getLong("failed_count");
|
||||
long ds = rs.getLong("duration_sum"); // Nullable → 0 if null
|
||||
long ds = rs.getLong("duration_sum"); // Nullable -> 0 if null
|
||||
long p99 = (long) rs.getDouble("p99_duration"); // quantileMerge returns Float64
|
||||
long ac = rs.getLong("active_count");
|
||||
return new long[]{tc, fc, ds, p99, ac};
|
||||
@@ -364,7 +404,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
// Previous period (shifted back 24h)
|
||||
Instant prevFrom = from.minus(Duration.ofHours(24));
|
||||
Instant prevTo = to.minus(Duration.ofHours(24));
|
||||
String prevSql = buildStatsSql(view, prevFrom, prevTo, filters, hasRunning);
|
||||
String prevSql = buildStatsSql(view, prevFrom, prevTo, filters, hasRunning, environment);
|
||||
|
||||
long prevTotal = 0, prevFailed = 0, prevAvg = 0, prevP99 = 0;
|
||||
var prevResult = jdbc.query(prevSql, (rs, rowNum) -> {
|
||||
@@ -383,7 +423,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
|
||||
// Today total
|
||||
Instant todayStart = Instant.now().truncatedTo(ChronoUnit.DAYS);
|
||||
String todaySql = buildStatsSql(view, todayStart, Instant.now(), filters, hasRunning);
|
||||
String todaySql = buildStatsSql(view, todayStart, Instant.now(), filters, hasRunning, environment);
|
||||
|
||||
long totalToday = 0;
|
||||
var todayResult = jdbc.query(todaySql, (rs, rowNum) -> rs.getLong("total_count"));
|
||||
@@ -399,7 +439,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
*/
|
||||
private StatsTimeseries queryTimeseries(String view, Instant from, Instant to,
|
||||
int bucketCount, List<Filter> filters,
|
||||
boolean hasRunningCount) {
|
||||
boolean hasRunningCount, String environment) {
|
||||
long intervalSeconds = Duration.between(from, to).toSeconds() / Math.max(bucketCount, 1);
|
||||
if (intervalSeconds < 60) intervalSeconds = 60;
|
||||
|
||||
@@ -416,6 +456,9 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
" WHERE tenant_id = " + lit(tenantId) +
|
||||
" AND bucket >= " + lit(from) +
|
||||
" AND bucket < " + lit(to);
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = " + lit(environment);
|
||||
}
|
||||
for (Filter f : filters) {
|
||||
sql += " AND " + f.column() + " = " + lit(f.value());
|
||||
}
|
||||
@@ -439,7 +482,7 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
*/
|
||||
private Map<String, StatsTimeseries> queryGroupedTimeseries(
|
||||
String view, String groupCol, Instant from, Instant to,
|
||||
int bucketCount, List<Filter> filters) {
|
||||
int bucketCount, List<Filter> filters, String environment) {
|
||||
|
||||
long intervalSeconds = Duration.between(from, to).toSeconds() / Math.max(bucketCount, 1);
|
||||
if (intervalSeconds < 60) intervalSeconds = 60;
|
||||
@@ -456,6 +499,9 @@ public class ClickHouseStatsStore implements StatsStore {
|
||||
" WHERE tenant_id = " + lit(tenantId) +
|
||||
" AND bucket >= " + lit(from) +
|
||||
" AND bucket < " + lit(to);
|
||||
if (environment != null && !environment.isBlank()) {
|
||||
sql += " AND environment = " + lit(environment);
|
||||
}
|
||||
for (Filter f : filters) {
|
||||
sql += " AND " + f.column() + " = " + lit(f.value());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user