import { createHash } from "node:crypto"; import { buildAuthHeader, flattenMatrix, flattenVector, parsePanelDescriptor, resolveRangeWindow, serializePanelDescriptorInput, type PanelDescriptor, } from "./prometheus"; export type DemoDescriptor = PanelDescriptor; export interface DemoPrometheusConfig { url: string; username?: string; password?: string; bearer?: string; } export const DEFAULT_DEMO_PROMETHEUS_URL = "https://prometheus.agent-native.foo"; export const DEMO_PROMETHEUS_ENV = { url: "ANALYTICS_DEMO_PROMETHEUS_URL", username: "ANALYTICS_DEMO_PROMETHEUS_USERNAME", password: "ANALYTICS_DEMO_PROMETHEUS_PASSWORD", bearer: "ANALYTICS_DEMO_PROMETHEUS_BEARER_TOKEN", } as const; export const DEMO_QUERY_CACHE_TTL_MS = 30_000; const DEMO_QUERY_CACHE_MAX = 200; type DemoQueryResult = ReturnType; const queryCache = new Map< string, { result: DemoQueryResult; expiresAt: number } >(); const inFlightQueries = new Map>(); export function serializeDemoDescriptorInput(raw: unknown): string { return serializePanelDescriptorInput(raw); } export function parseDemoDescriptor(raw: string): DemoDescriptor { try { return parsePanelDescriptor(raw); } catch (err: any) { throw new Error( String(err?.message ?? err).replace( /^prometheus panel sql/, "demo Prometheus panel sql", ), ); } } export function resolveDemoPrometheusConfig( env: Record = process.env, ): DemoPrometheusConfig { const url = ( env[DEMO_PROMETHEUS_ENV.url]?.trim() || DEFAULT_DEMO_PROMETHEUS_URL ).replace(/\/+$/, ""); return { url, username: env[DEMO_PROMETHEUS_ENV.username]?.trim() || undefined, password: env[DEMO_PROMETHEUS_ENV.password] || undefined, bearer: env[DEMO_PROMETHEUS_ENV.bearer] || undefined, }; } function authFingerprint(config: DemoPrometheusConfig): string { return createHash("sha256") .update( JSON.stringify({ username: config.username ?? "", password: config.password ?? "", bearer: config.bearer ?? "", }), ) .digest("hex") .slice(0, 16); } function descriptorCacheKey( config: DemoPrometheusConfig, descriptor: DemoDescriptor, nowMs: number, ): string { const timeBucket = descriptor.endTime ? null : Math.floor(nowMs / DEMO_QUERY_CACHE_TTL_MS); return JSON.stringify({ url: config.url, auth: authFingerprint(config), promql: descriptor.promql, mode: descriptor.mode, range: descriptor.range ?? null, step: descriptor.step ?? null, startTime: descriptor.startTime ?? null, endTime: descriptor.endTime ?? null, timeBucket, }); } function getCachedResult(key: string, nowMs: number): DemoQueryResult | null { const cached = queryCache.get(key); if (!cached) return null; if (cached.expiresAt <= nowMs) { queryCache.delete(key); return null; } return cached.result; } function setCachedResult(key: string, result: DemoQueryResult, nowMs: number) { queryCache.set(key, { result, expiresAt: nowMs + DEMO_QUERY_CACHE_TTL_MS, }); if (queryCache.size <= DEMO_QUERY_CACHE_MAX) return; for (const [cacheKey, cached] of queryCache) { if (cached.expiresAt <= nowMs || queryCache.size > DEMO_QUERY_CACHE_MAX) { queryCache.delete(cacheKey); } if (queryCache.size <= DEMO_QUERY_CACHE_MAX) break; } } export function clearDemoQueryCache() { queryCache.clear(); inFlightQueries.clear(); } async function demoPrometheusGet( config: DemoPrometheusConfig, path: string, params: Record, ): Promise { const headers: Record = { Accept: "application/json" }; const authHeader = buildAuthHeader(config); if (authHeader) headers.Authorization = authHeader; const qs = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== "") qs.set(key, value); } const res = await fetch(`${config.url}${path}?${qs.toString()}`, { headers, }); if (!res.ok) { const text = await res.text(); throw new Error( `Demo Prometheus API error ${res.status}: ${text.slice(0, 500)}`, ); } const body = (await res.json()) as { status?: string; data?: unknown; error?: string; }; if (body?.status !== "success") { throw new Error( `Demo Prometheus query failed: ${body?.error ?? "unknown error"}`, ); } return body.data as T; } async function fetchDemoPanel( descriptor: DemoDescriptor, config: DemoPrometheusConfig, ): Promise { if (descriptor.mode === "instant") { const data = await demoPrometheusGet<{ resultType: string; result: unknown; }>(config, "/api/v1/query", { query: descriptor.promql, time: descriptor.endTime, }); return flattenVector(data); } const { startSec, endSec, stepSec } = resolveRangeWindow(descriptor); const data = await demoPrometheusGet<{ resultType: string; result: unknown; }>(config, "/api/v1/query_range", { query: descriptor.promql, start: String(startSec), end: String(endSec), step: String(stepSec), }); return flattenMatrix(data); } export async function runDemoPanelWithConfig( raw: string, config: DemoPrometheusConfig, ) { const descriptor = parseDemoDescriptor(raw); const nowMs = Date.now(); const cacheKey = descriptorCacheKey(config, descriptor, nowMs); const cached = getCachedResult(cacheKey, nowMs); if (cached) return cached; const inFlight = inFlightQueries.get(cacheKey); if (inFlight) return inFlight; const promise = fetchDemoPanel(descriptor, config) .then((result) => { setCachedResult(cacheKey, result, Date.now()); return result; }) .finally(() => { inFlightQueries.delete(cacheKey); }); inFlightQueries.set(cacheKey, promise); return promise; } export async function runDemoPanel(raw: string) { return runDemoPanelWithConfig(raw, resolveDemoPrometheusConfig()); }