diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java index 424a05ad..78cdc966 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.controller; +import com.cameleer3.server.core.search.ExecutionStats; import com.cameleer3.server.core.search.ExecutionSummary; import com.cameleer3.server.core.search.SearchRequest; import com.cameleer3.server.core.search.SearchResult; @@ -65,4 +66,10 @@ public class SearchController { @RequestBody SearchRequest request) { return ResponseEntity.ok(searchService.search(request)); } + + @GetMapping("/stats") + @Operation(summary = "Aggregate execution stats (P99 latency, active count)") + public ResponseEntity stats() { + return ResponseEntity.ok(searchService.stats()); + } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java index 14695a92..094811c1 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.search; +import com.cameleer3.server.core.search.ExecutionStats; import com.cameleer3.server.core.search.ExecutionSummary; import com.cameleer3.server.core.search.SearchEngine; import com.cameleer3.server.core.search.SearchRequest; @@ -84,10 +85,33 @@ public class ClickHouseSearchEngine implements SearchEngine { return result != null ? result : 0L; } + @Override + public ExecutionStats stats() { + Long p99 = jdbcTemplate.queryForObject( + "SELECT quantile(0.99)(duration_ms) FROM route_executions " + + "WHERE start_time >= now() - INTERVAL 1 HOUR", + Long.class); + Long active = jdbcTemplate.queryForObject( + "SELECT count() FROM route_executions WHERE status = 'RUNNING'", + Long.class); + return new ExecutionStats( + p99 != null ? p99 : 0L, + active != null ? active : 0L); + } + private void buildWhereClause(SearchRequest req, List conditions, List params) { if (req.status() != null && !req.status().isBlank()) { - conditions.add("status = ?"); - params.add(req.status()); + String[] statuses = req.status().split(","); + if (statuses.length == 1) { + conditions.add("status = ?"); + params.add(statuses[0].trim()); + } else { + String placeholders = String.join(", ", java.util.Collections.nCopies(statuses.length, "?")); + conditions.add("status IN (" + placeholders + ")"); + for (String s : statuses) { + params.add(s.trim()); + } + } } if (req.timeFrom() != null) { conditions.add("start_time >= ?"); diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java new file mode 100644 index 00000000..4094bf03 --- /dev/null +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java @@ -0,0 +1,9 @@ +package com.cameleer3.server.core.search; + +/** + * Aggregate execution statistics. + * + * @param p99LatencyMs 99th percentile duration in milliseconds (last hour) + * @param activeCount number of currently running executions + */ +public record ExecutionStats(long p99LatencyMs, long activeCount) {} diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java index a5d1dd72..9a083e60 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java @@ -24,4 +24,11 @@ public interface SearchEngine { * @return total number of matching executions */ long count(SearchRequest request); + + /** + * Compute aggregate stats: P99 latency and count of currently running executions. + * + * @return execution stats + */ + ExecutionStats stats(); } diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java index a0cfc5e1..56aead53 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java @@ -28,4 +28,11 @@ public class SearchService { public long count(SearchRequest request) { return engine.count(request); } + + /** + * Compute aggregate execution stats (P99 latency, active count). + */ + public ExecutionStats stats() { + return engine.stats(); + } } diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts index 1a1443f8..a749106c 100644 --- a/ui/src/api/queries/executions.ts +++ b/ui/src/api/queries/executions.ts @@ -2,6 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; import type { SearchRequest } from '../schema'; +export function useExecutionStats() { + return useQuery({ + queryKey: ['executions', 'stats'], + queryFn: async () => { + const { data, error } = await api.GET('/search/stats'); + if (error) throw new Error('Failed to load stats'); + return data!; + }, + refetchInterval: 10_000, + }); +} + export function useSearchExecutions(filters: SearchRequest) { return useQuery({ queryKey: ['executions', 'search', filters], diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 77b9a061..6397ef04 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -95,6 +95,17 @@ export interface paths { }; }; }; + '/search/stats': { + get: { + responses: { + 200: { + content: { + 'application/json': ExecutionStats; + }; + }; + }; + }; + }; '/agents': { get: { parameters: { @@ -181,6 +192,11 @@ export interface ProcessorNode { /** Processor snapshot is a flat key-value map (Map in Java) */ export type ProcessorSnapshot = Record; +export interface ExecutionStats { + p99LatencyMs: number; + activeCount: number; +} + export interface AgentInstance { agentId: string; group: string; diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx index 496c6188..30ff5ee2 100644 --- a/ui/src/pages/executions/ExecutionExplorer.tsx +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -1,4 +1,4 @@ -import { useSearchExecutions } from '../../api/queries/executions'; +import { useSearchExecutions, useExecutionStats } from '../../api/queries/executions'; import { useExecutionSearch } from './use-execution-search'; import { StatCard } from '../../components/shared/StatCard'; import { Pagination } from '../../components/shared/Pagination'; @@ -10,6 +10,7 @@ export function ExecutionExplorer() { const { toSearchRequest, offset, limit, setOffset } = useExecutionSearch(); const searchRequest = toSearchRequest(); const { data, isLoading, isFetching } = useSearchExecutions(searchRequest); + const { data: stats } = useExecutionStats(); const total = data?.total ?? 0; const results = data?.data ?? []; @@ -42,8 +43,8 @@ export function ExecutionExplorer() { - - + + {/* Filters */}