Fix status filter OR logic and add P99/active stats endpoint
Status filter now parses comma-separated values into SQL IN clause instead of exact match, so filtering by multiple statuses works. Added GET /api/v1/search/stats returning P99 latency (last hour) and active execution count, wired into the UI stat cards with 10s polling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.cameleer3.server.app.controller;
|
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.ExecutionSummary;
|
||||||
import com.cameleer3.server.core.search.SearchRequest;
|
import com.cameleer3.server.core.search.SearchRequest;
|
||||||
import com.cameleer3.server.core.search.SearchResult;
|
import com.cameleer3.server.core.search.SearchResult;
|
||||||
@@ -65,4 +66,10 @@ public class SearchController {
|
|||||||
@RequestBody SearchRequest request) {
|
@RequestBody SearchRequest request) {
|
||||||
return ResponseEntity.ok(searchService.search(request));
|
return ResponseEntity.ok(searchService.search(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@Operation(summary = "Aggregate execution stats (P99 latency, active count)")
|
||||||
|
public ResponseEntity<ExecutionStats> stats() {
|
||||||
|
return ResponseEntity.ok(searchService.stats());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cameleer3.server.app.search;
|
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.ExecutionSummary;
|
||||||
import com.cameleer3.server.core.search.SearchEngine;
|
import com.cameleer3.server.core.search.SearchEngine;
|
||||||
import com.cameleer3.server.core.search.SearchRequest;
|
import com.cameleer3.server.core.search.SearchRequest;
|
||||||
@@ -84,10 +85,33 @@ public class ClickHouseSearchEngine implements SearchEngine {
|
|||||||
return result != null ? result : 0L;
|
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<String> conditions, List<Object> params) {
|
private void buildWhereClause(SearchRequest req, List<String> conditions, List<Object> params) {
|
||||||
if (req.status() != null && !req.status().isBlank()) {
|
if (req.status() != null && !req.status().isBlank()) {
|
||||||
conditions.add("status = ?");
|
String[] statuses = req.status().split(",");
|
||||||
params.add(req.status());
|
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) {
|
if (req.timeFrom() != null) {
|
||||||
conditions.add("start_time >= ?");
|
conditions.add("start_time >= ?");
|
||||||
|
|||||||
@@ -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) {}
|
||||||
@@ -24,4 +24,11 @@ public interface SearchEngine {
|
|||||||
* @return total number of matching executions
|
* @return total number of matching executions
|
||||||
*/
|
*/
|
||||||
long count(SearchRequest request);
|
long count(SearchRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute aggregate stats: P99 latency and count of currently running executions.
|
||||||
|
*
|
||||||
|
* @return execution stats
|
||||||
|
*/
|
||||||
|
ExecutionStats stats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ public class SearchService {
|
|||||||
public long count(SearchRequest request) {
|
public long count(SearchRequest request) {
|
||||||
return engine.count(request);
|
return engine.count(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute aggregate execution stats (P99 latency, active count).
|
||||||
|
*/
|
||||||
|
public ExecutionStats stats() {
|
||||||
|
return engine.stats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { api } from '../client';
|
import { api } from '../client';
|
||||||
import type { SearchRequest } from '../schema';
|
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) {
|
export function useSearchExecutions(filters: SearchRequest) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['executions', 'search', filters],
|
queryKey: ['executions', 'search', filters],
|
||||||
|
|||||||
16
ui/src/api/schema.d.ts
vendored
16
ui/src/api/schema.d.ts
vendored
@@ -95,6 +95,17 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
'/search/stats': {
|
||||||
|
get: {
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': ExecutionStats;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
'/agents': {
|
'/agents': {
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -181,6 +192,11 @@ export interface ProcessorNode {
|
|||||||
/** Processor snapshot is a flat key-value map (Map<String, String> in Java) */
|
/** Processor snapshot is a flat key-value map (Map<String, String> in Java) */
|
||||||
export type ProcessorSnapshot = Record<string, string>;
|
export type ProcessorSnapshot = Record<string, string>;
|
||||||
|
|
||||||
|
export interface ExecutionStats {
|
||||||
|
p99LatencyMs: number;
|
||||||
|
activeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentInstance {
|
export interface AgentInstance {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
group: string;
|
group: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSearchExecutions } from '../../api/queries/executions';
|
import { useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
||||||
import { useExecutionSearch } from './use-execution-search';
|
import { useExecutionSearch } from './use-execution-search';
|
||||||
import { StatCard } from '../../components/shared/StatCard';
|
import { StatCard } from '../../components/shared/StatCard';
|
||||||
import { Pagination } from '../../components/shared/Pagination';
|
import { Pagination } from '../../components/shared/Pagination';
|
||||||
@@ -10,6 +10,7 @@ export function ExecutionExplorer() {
|
|||||||
const { toSearchRequest, offset, limit, setOffset } = useExecutionSearch();
|
const { toSearchRequest, offset, limit, setOffset } = useExecutionSearch();
|
||||||
const searchRequest = toSearchRequest();
|
const searchRequest = toSearchRequest();
|
||||||
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest);
|
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest);
|
||||||
|
const { data: stats } = useExecutionStats();
|
||||||
|
|
||||||
const total = data?.total ?? 0;
|
const total = data?.total ?? 0;
|
||||||
const results = data?.data ?? [];
|
const results = data?.data ?? [];
|
||||||
@@ -42,8 +43,8 @@ export function ExecutionExplorer() {
|
|||||||
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={`from current search`} />
|
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={`from current search`} />
|
||||||
<StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" />
|
<StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" />
|
||||||
<StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" />
|
<StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" />
|
||||||
<StatCard label="P99 Latency" value="--" accent="green" change="stats endpoint coming soon" />
|
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs}ms` : '--'} accent="green" change="last hour" />
|
||||||
<StatCard label="Active Now" value="--" accent="blue" change="stats endpoint coming soon" />
|
<StatCard label="Active Now" value={stats ? stats.activeCount.toString() : '--'} accent="blue" change="running executions" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
|
|||||||
Reference in New Issue
Block a user