Add Metrics UI: declare metrics and view derived bucket values (ticket 012)
Replaces the Metrics ComingSoon placeholder with a form for declaring metrics (name, message_match_regex, param matchers) and a list/detail view of declared metrics whose bucket values refetch on selection or active-range change (no continuous polling, since declare's backfill already resolves synchronously with the POST). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
57d23c62e9
commit
5974070645
@@ -43,6 +43,38 @@ export interface Stats {
|
||||
last_timestamp_ms: number | null
|
||||
}
|
||||
|
||||
export interface MetricParamMatcher {
|
||||
param_name: string
|
||||
capture_group: string
|
||||
kind: 'string' | 'number' | 'timestamp'
|
||||
}
|
||||
|
||||
export interface MetricDeclaration {
|
||||
name: string
|
||||
message_match_regex: string
|
||||
params: MetricParamMatcher[]
|
||||
}
|
||||
|
||||
export interface BucketValue {
|
||||
labels: Record<string, MetricPrimitive>
|
||||
sequence: number
|
||||
timestamp_ms: number
|
||||
value: MetricPrimitive
|
||||
}
|
||||
|
||||
/// Error shape returned by API handlers on a 4xx/5xx response:
|
||||
/// `{ "error": "..." }` (see `ErrorDto` on the backend).
|
||||
export interface ApiErrorBody {
|
||||
error: string
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(path)
|
||||
if (!res.ok) {
|
||||
@@ -51,6 +83,32 @@ async function getJson<T>(path: string): Promise<T> {
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
/// POSTs a JSON body to `path`. On a non-ok response, attempts to parse the
|
||||
/// body as `{ "error": "..." }` (the shape every declaration endpoint uses
|
||||
/// for validation failures) and throws an `ApiError` with that message so
|
||||
/// callers can surface it inline; falls back to a generic message if the
|
||||
/// body isn't in that shape.
|
||||
async function postJson<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
let message = `${path} failed: ${res.status}`
|
||||
try {
|
||||
const errorBody = (await res.json()) as Partial<ApiErrorBody>
|
||||
if (errorBody && typeof errorBody.error === 'string') {
|
||||
message = errorBody.error
|
||||
}
|
||||
} catch {
|
||||
// response body wasn't JSON; fall back to the generic message
|
||||
}
|
||||
throw new ApiError(message)
|
||||
}
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
export function fetchLogs(params: {
|
||||
sinceSequence?: number
|
||||
startMs?: number
|
||||
@@ -84,3 +142,30 @@ export function fetchStats(): Promise<Stats> {
|
||||
export function fetchParamDeclarations(): Promise<ParamDeclaration[]> {
|
||||
return getJson('/api/params')
|
||||
}
|
||||
|
||||
export function fetchMetricDeclarations(): Promise<MetricDeclaration[]> {
|
||||
return getJson('/api/metrics')
|
||||
}
|
||||
|
||||
export function declareMetric(request: MetricDeclaration): Promise<MetricDeclaration> {
|
||||
return postJson('/api/metrics', request)
|
||||
}
|
||||
|
||||
export function fetchMetricBucketValues(
|
||||
name: string,
|
||||
params: {
|
||||
labels?: string
|
||||
sinceSequence?: number
|
||||
startMs?: number
|
||||
endMs?: number
|
||||
limit?: number
|
||||
} = {},
|
||||
): Promise<BucketValue[]> {
|
||||
const search = new URLSearchParams()
|
||||
if (params.labels !== undefined) search.set('labels', params.labels)
|
||||
if (params.sinceSequence !== undefined) search.set('since_sequence', String(params.sinceSequence))
|
||||
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
|
||||
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
||||
if (params.limit !== undefined) search.set('limit', String(params.limit))
|
||||
return getJson(`/api/metrics/${encodeURIComponent(name)}?${search.toString()}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user