// CIL[BEGIN chunk_id="custom_workbench_observability_module_header_1785000001" type="module_header" identity="module_header" parent="none"] // CIL[DOC-MACRO purpose="Expose observability metrics and traces for the Workbench DevOps dashboard"] // CIL[DOC-RESPONSIBILITY responsibility="Call SQL observability functions and return aggregated dashboard data"] // CIL[DOC-CONTEXT module="workbench.functions.custom_workbench-observability" layer="function" related_files="[\"custom/apps/workbench/pages/devops/DevopsDashboard.tsx\",\".framework/migrations/040_observability_metrics.sql\"]"] // CIL[DOC-CONTRACT inputs="query: action, date_from, date_to, metric_name, granularity, trace_id" outputs="JSON observability payload" errors="missing account context, invalid action, db rpc errors" side_effects="reads public.logs via RPC"] // CIL[DOC-MICRO logic_summary="Dispatches to SQL RPC helpers (get_log_stats, get_trace_stats, get_metric_series, get_business_outcomes, get_trace_tree) and returns a summary or specific series" notable_decisions=["Uses ctx.db.rpc to leverage existing observability migrations","Defaults date range to last 24 hours"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] import { createHandler, RequestContext } from './_shared/middleware' // CIL[END chunk_id="custom_workbench_observability_module_header_1785000001"] // CIL[BEGIN chunk_id="custom_workbench_observability_types_1785000002" type="type_definition" identity="ObservabilityAction" parent="none"] // CIL[DOC-MACRO purpose="Define supported observability endpoint actions"] // CIL[DOC-RESPONSIBILITY responsibility="Constrain the action query parameter to known values"] // CIL[DOC-CONTEXT module="workbench.functions.custom_workbench-observability" layer="function" related_files="[\"custom/apps/workbench/functions/custom_workbench-observability.ts\"]"] // CIL[DOC-CONTRACT inputs="none" outputs="string literal union" errors="none" side_effects="none"] // CIL[DOC-MICRO logic_summary="Union of actions exposed by the observability handler" notable_decisions=[]] // CIL[DOC-CONFIDENCE pass="first" score="0.95" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] type ObservabilityAction = 'summary' | 'logs' | 'traces' | 'trace' | 'metric' | 'outcomes' // CIL[END chunk_id="custom_workbench_observability_types_1785000002"] // CIL[BEGIN chunk_id="custom_workbench_observability_helpers_1785000003" type="module_exports" identity="helpers" parent="custom_workbench_observability_module_header_1785000001"] // CIL[DOC-MACRO purpose="SQL RPC wrappers for observability functions"] // CIL[DOC-RESPONSIBILITY responsibility="Encapsulate calls to get_log_stats, get_trace_stats, get_metric_series, get_business_outcomes, and get_trace_tree"] // CIL[DOC-CONTEXT module="workbench.functions.custom_workbench-observability" layer="function" related_files="[\".framework/migrations/040_observability_metrics.sql\"]"] // CIL[DOC-CONTRACT inputs="ctx and filter parameters" outputs="arrays or single rows from RPC" errors="RPC errors thrown" side_effects="none"] // CIL[DOC-MICRO logic_summary="Wraps each RPC call with account_id and optional date range; returns data or empty arrays" notable_decisions=["Casts count fields to numbers for consistent aggregation"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] function defaultRange(): { dateFrom: string; dateTo: string } { const now = new Date() const from = new Date(now.getTime() - 24 * 60 * 60 * 1000) return { dateFrom: from.toISOString(), dateTo: now.toISOString() } } function parseRange(ctx: RequestContext): { dateFrom: string | null; dateTo: string | null } { const dateFrom = ctx.query?.date_from || null const dateTo = ctx.query?.date_to || null if (!dateFrom || !dateTo) return defaultRange() return { dateFrom, dateTo } } async function getLogStats(ctx: RequestContext, dateFrom: string | null, dateTo: string | null) { const { data, error: err } = await ctx.db.rpc('get_log_stats', { p_account_id: ctx.accountId, p_level: ctx.query?.level || null, p_source: ctx.query?.source || null, p_source_type: ctx.query?.source_type || null, p_source_id: ctx.query?.source_id || null, p_person_id: ctx.query?.person_id || null, p_query: ctx.query?.query || null, p_date_from: dateFrom, p_date_to: dateTo, }) if (err) throw err const row = data?.[0] || {} return { total: Number(row.total || 0), error_count: Number(row.error_count || 0), user_action_count: Number(row.user_action_count || 0), event_type_count: Number(row.event_type_count || 0), levels: row.levels || [], sources: row.sources || [], source_types: row.source_types || [], } } async function getTraceStats( ctx: RequestContext, dateFrom: string | null, dateTo: string | null, limit: number | null = null, offset: number | null = null, sort: string = 'started' ) { const params: any = { p_account_id: ctx.accountId, p_date_from: dateFrom, p_date_to: dateTo, } if (limit !== null) params.p_limit = limit if (offset !== null) params.p_offset = offset if (sort) params.p_sort = sort const { data, error: err } = await ctx.db.rpc('get_trace_stats', params) if (err) throw err return (data || []).map((row: any) => ({ trace_id: row.trace_id, root_span_name: row.root_span_name, started_at: row.started_at, span_count: Number(row.span_count || 0), error_count: Number(row.error_count || 0), duration_ms: Number(row.duration_ms || 0), outcome: row.outcome, total: Number(row.total || 0), })) } async function getMetricSeries( ctx: RequestContext, metricName: string, granularity: string, dateFrom: string | null, dateTo: string | null ) { const { data, error: err } = await ctx.db.rpc('get_metric_series', { p_account_id: ctx.accountId, p_metric_name: metricName, p_granularity: granularity, p_date_from: dateFrom, p_date_to: dateTo, }) if (err) throw err return (data || []).map((row: any) => ({ bucket: row.bucket, value: Number(row.value || 0), })) } async function getBusinessOutcomes( ctx: RequestContext, dateFrom: string | null, dateTo: string | null, limit: number | null = null, offset: number | null = null ) { const params: any = { p_account_id: ctx.accountId, p_date_from: dateFrom, p_date_to: dateTo, } if (limit !== null) params.p_limit = limit if (offset !== null) params.p_offset = offset const { data, error: err } = await ctx.db.rpc('get_business_outcomes', params) if (err) throw err return (data || []).map((row: any) => ({ intent: row.intent, outcome: row.outcome, count: Number(row.count || 0), total: Number(row.total || 0), })) } async function getTraceTree(ctx: RequestContext, traceId: string) { const { data, error: err } = await ctx.db.rpc('get_trace_tree', { p_account_id: ctx.accountId, p_trace_id: traceId, }) if (err) throw err return data || [] } // CIL[END chunk_id="custom_workbench_observability_helpers_1785000003"] // CIL[BEGIN chunk_id="custom_workbench_observability_summary_1785000004" type="function" identity="buildSummary" parent="custom_workbench_observability_helpers_1785000003"] // CIL[DOC-MACRO purpose="Aggregate observability series into a dashboard summary"] // CIL[DOC-RESPONSIBILITY responsibility="Combine log stats, trace stats, business outcomes, and key metric series"] // CIL[DOC-CONTEXT module="workbench.functions.custom_workbench-observability" layer="function" related_files="[\"custom/apps/workbench/pages/devops/DevopsDashboard.tsx\"]"] // CIL[DOC-CONTRACT inputs="ctx and date range" outputs="{ date_from, date_to, granularity, logs, traces, outcomes, metrics }" errors="RPC errors" side_effects="none"] // CIL[DOC-MICRO logic_summary="Fetches log stats, trace stats, outcomes, and three metric series concurrently; computes trace aggregates" notable_decisions=["Returns the 20 most recent traces; aggregates counts and average duration from the full set"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] async function buildSummary(ctx: RequestContext) { const { dateFrom, dateTo } = parseRange(ctx) const granularity = ctx.query?.granularity || '1 hour' const [ logs, traces, outcomes, requestCount, errorRate, avgHandlerDuration, ] = await Promise.all([ getLogStats(ctx, dateFrom, dateTo), getTraceStats(ctx, dateFrom, dateTo), getBusinessOutcomes(ctx, dateFrom, dateTo), getMetricSeries(ctx, 'request_count', granularity, dateFrom, dateTo), getMetricSeries(ctx, 'error_rate', granularity, dateFrom, dateTo), getMetricSeries(ctx, 'avg_handler_duration', granularity, dateFrom, dateTo), ]) const traceSummary = { total: traces.length, error_count: traces.reduce((sum: number, row: any) => sum + (row.error_count || 0), 0), total_spans: traces.reduce((sum: number, row: any) => sum + (row.span_count || 0), 0), avg_duration_ms: traces.length ? traces.reduce((sum: number, row: any) => sum + (row.duration_ms || 0), 0) / traces.length : 0, recent: traces.slice(0, 20), } return { date_from: dateFrom, date_to: dateTo, granularity, logs, traces: traceSummary, outcomes, metrics: { request_count: requestCount, error_rate: errorRate, avg_handler_duration: avgHandlerDuration, }, } } // CIL[END chunk_id="custom_workbench_observability_summary_1785000004"] // CIL[BEGIN chunk_id="custom_workbench_observability_handler_1785000005" type="function" identity="handler" parent="custom_workbench_observability_helpers_1785000003"] // CIL[DOC-MACRO purpose="Route observability requests by action"] // CIL[DOC-RESPONSIBILITY responsibility="Validate action and call the appropriate helper"] // CIL[DOC-CONTEXT module="workbench.functions.custom_workbench-observability" layer="function" related_files="[\"custom/apps/workbench/pages/devops/DevopsDashboard.tsx\"]"] // CIL[DOC-CONTRACT inputs="action and query parameters" outputs="observability payload" errors="invalid action or missing trace_id" side_effects="none"] // CIL[DOC-MICRO logic_summary="Dispatches to summary, logs, traces, trace tree, metric, or outcomes handlers" notable_decisions=["Defaults to summary action"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] export const handler = createHandler(async (ctx: RequestContext) => { const action = (ctx.query?.action || 'summary') as ObservabilityAction const { dateFrom, dateTo } = parseRange(ctx) switch (action) { case 'summary': return await buildSummary(ctx) case 'logs': return await getLogStats(ctx, dateFrom, dateTo) case 'traces': { const traceLimit = parseInt(ctx.query?.limit, 10) const traceOffset = parseInt(ctx.query?.offset, 10) const traceSort = ctx.query?.sort || 'started' const traceRows = await getTraceStats( ctx, dateFrom, dateTo, isNaN(traceLimit) ? null : traceLimit, isNaN(traceOffset) ? null : traceOffset, traceSort ) return { total: traceRows[0]?.total || 0, data: traceRows, } } case 'trace': { const traceId = ctx.query?.trace_id if (!traceId) { const e: any = new Error('trace_id is required') e.statusCode = 400 throw e } return await getTraceTree(ctx, traceId) } case 'metric': { const metricName = ctx.query?.metric_name || 'request_count' const granularity = ctx.query?.granularity || '1 hour' return await getMetricSeries(ctx, metricName, granularity, dateFrom, dateTo) } case 'outcomes': { const outcomeLimit = parseInt(ctx.query?.limit, 10) const outcomeOffset = parseInt(ctx.query?.offset, 10) const outcomeRows = await getBusinessOutcomes( ctx, dateFrom, dateTo, isNaN(outcomeLimit) ? null : outcomeLimit, isNaN(outcomeOffset) ? null : outcomeOffset ) return { total: outcomeRows[0]?.total || 0, data: outcomeRows, } } default: throw new Error('Invalid action') } }) // CIL[END chunk_id="custom_workbench_observability_handler_1785000005"]