feat: add environment filtering across all APIs and UI
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m8s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled

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:
hsiegeln
2026-04-04 15:42:26 +02:00
parent babdc1d7a4
commit 694d0eef59
25 changed files with 439 additions and 160 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));
}
/**

View File

@@ -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
);

View File

@@ -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());

View File

@@ -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());

View File

@@ -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 JVMClickHouse 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());
}