// CIL[BEGIN chunk_id="ServicesPage_module_header_1785000051" type="module_header" identity="module_header" parent="none"] // CIL[DOC-MACRO purpose="Render the Workbench Services observability page"] // CIL[DOC-RESPONSIBILITY responsibility="Show service health derived from trace root_span_name aggregates"] // CIL[DOC-CONTEXT module="workbench.pages.observe.ServicesPage" layer="frontend-page" related_files="[\"custom/apps/workbench/functions/custom_workbench-observability.ts\",\"custom/apps/workbench/pages/observe/UptimePage.tsx\"]"] // CIL[DOC-CONTRACT inputs="none" outputs="JSX Services page" errors="fetch errors rendered inline" side_effects="calls /api/custom_workbench-observability"] // CIL[DOC-MICRO logic_summary="Provides a time range selector, fetches trace stats, aggregates health by root_span_name, and renders summary cards and a service health table" notable_decisions=["Treats each distinct root_span_name as a service","Health is based on error rate thresholds"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] import { useEffect, useMemo, useState, Fragment } from 'react' import { apiFetch } from '@core/lib/api' import { formatDateTime, formatNumber } from '@core/lib/utils' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { NativeSelect } from '@core/components/ui/native-select' import { LoadingSpinner } from '@core/components/ui/LoadingSpinner' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@core/components/ui/table' import { Pagination } from '../../components/Pagination' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, CartesianGrid, } from 'recharts' import { Server, Activity, AlertTriangle, CheckCircle, ChevronDown, ChevronRight } from 'lucide-react' // CIL[END chunk_id="ServicesPage_module_header_1785000051"] // CIL[BEGIN chunk_id="ServicesPage_servicesPage_1785000051" type="function" identity="ServicesPage" parent="ServicesPage_module_header_1785000051"] // CIL[DOC-MACRO purpose="Services observability page implementation"] // CIL[DOC-RESPONSIBILITY responsibility="Aggregate trace stats by service and display health"] // CIL[DOC-CONTEXT module="workbench.pages.observe.ServicesPage" layer="frontend-page" related_files="[\"custom/apps/workbench/functions/custom_workbench-observability.ts\"]"] // CIL[DOC-CONTRACT inputs="none" outputs="JSX" errors="fetch errors rendered inline" side_effects="network requests on mount and range change"] // CIL[DOC-MICRO logic_summary="Fetches trace stats for the selected range, aggregates counts and durations by root_span_name, and renders health cards and a sorted service table" notable_decisions=["Services with >5% error rate are degraded, >20% are erroring"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] const RANGE_OPTIONS = [ { label: 'Last hour', value: '1h', ms: 60 * 60 * 1000 }, { label: 'Last 24 hours', value: '24h', ms: 24 * 60 * 60 * 1000 }, { label: 'Last 7 days', value: '7d', ms: 7 * 24 * 60 * 60 * 1000 }, { label: 'Last 30 days', value: '30d', ms: 30 * 24 * 60 * 60 * 1000 }, ] function rangeDates(value: string) { const now = new Date() const option = RANGE_OPTIONS.find((o) => o.value === value) || RANGE_OPTIONS[1] return { from: new Date(now.getTime() - option.ms).toISOString(), to: now.toISOString(), } } interface TraceRow { trace_id: string root_span_name: string started_at: string duration_ms: number span_count: number error_count: number outcome: string } interface ServiceHealth { name: string traces: number errors: number avg_duration_ms: number last_started_at: string error_rate: number } function ServiceTraces({ serviceName, traces }: { serviceName: string; traces: TraceRow[] }) { const serviceTraces = traces .filter((t) => (t.root_span_name || 'unknown') === serviceName) .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()) .slice(0, 10) if (serviceTraces.length === 0) { return

No recent traces for this service.

} return (
Trace Started Duration Errors {serviceTraces.map((row) => ( {row.trace_id?.slice(0, 8)}… {formatDateTime(row.started_at)} {row.duration_ms.toFixed(0)} ms {formatNumber(row.error_count)} ))}
) } export default function ServicesPage() { const [range, setRange] = useState('24h') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [traces, setTraces] = useState([]) const [servicePage, setServicePage] = useState(1) const [servicePageSize, setServicePageSize] = useState(10) const [serviceExpanded, setServiceExpanded] = useState(null) const load = async () => { const { from, to } = rangeDates(range) setLoading(true) setError(null) try { const res = await apiFetch(`/api/custom_workbench-observability?action=traces&date_from=${encodeURIComponent(from)}&date_to=${encodeURIComponent(to)}`) const json = await res.json() if (json.error) throw new Error(json.error) const tracePayload = json.data || { data: [], total: 0 } setTraces((tracePayload.data || []).map((r: any) => ({ ...r, duration_ms: Number(r.duration_ms || 0), span_count: Number(r.span_count || 0), error_count: Number(r.error_count || 0), }))) } catch (e: any) { setError(e.message || 'Failed to load service data') } finally { setLoading(false) } } useEffect(() => { load() setServicePage(1) }, [range]) const services = useMemo(() => { const map = new Map() traces.forEach((t) => { const name = t.root_span_name || 'unknown' const existing = map.get(name) || { count: 0, errors: 0, durationSum: 0, last: '' } existing.count += 1 existing.errors += t.error_count existing.durationSum += t.duration_ms if (!existing.last || t.started_at > existing.last) existing.last = t.started_at map.set(name, existing) }) return Array.from(map.entries()) .map(([name, data]) => ({ name, traces: data.count, errors: data.errors, avg_duration_ms: data.count ? data.durationSum / data.count : 0, last_started_at: data.last, error_rate: data.count ? data.errors / data.count : 0, })) .sort((a, b) => b.error_rate - a.error_rate || b.traces - a.traces) }, [traces]) const pagedServices = useMemo(() => { const start = (servicePage - 1) * servicePageSize return services.slice(start, start + servicePageSize) }, [services, servicePage, servicePageSize]) const chartData = useMemo( () => services .slice(0, 10) .map((s) => ({ name: s.name, error_rate: Number((s.error_rate * 100).toFixed(1)) })) .sort((a, b) => b.error_rate - a.error_rate), [services] ) const healthy = services.filter((s) => s.error_rate < 0.05) const degraded = services.filter((s) => s.error_rate >= 0.05 && s.error_rate < 0.2) const erroring = services.filter((s) => s.error_rate >= 0.2) function healthBadge(s: ServiceHealth) { if (s.error_rate >= 0.2) { return Erroring } if (s.error_rate >= 0.05) { return Degraded } return Healthy } return (

Services

Service health and trace aggregates

setRange(e.target.value)} className="w-40"> {RANGE_OPTIONS.map((o) => ( ))}
{error && (
{error}
)} {loading && !services.length && (
)}
Services
{formatNumber(services.length)}
Healthy
{formatNumber(healthy.length)}
Degraded
{formatNumber(degraded.length)}
Erroring
{formatNumber(erroring.length)}
Top Service Error Rates {chartData.length === 0 ? (

No service data for the selected range.

) : (
[`${value}%`, 'Error rate']} /> {chartData.map((entry, index) => ( = 20 ? '#ef4444' : entry.error_rate >= 5 ? '#f59e0b' : '#22c55e'} /> ))}
)}
Service Health {services.length === 0 ? (

No services recorded in the selected range.

) : (
Service Traces Errors Error Rate Avg Duration Last Seen Health {pagedServices.map((s) => ( setServiceExpanded(serviceExpanded === s.name ? null : s.name)} > {serviceExpanded === s.name ? ( ) : ( )} {s.name} {formatNumber(s.traces)} {formatNumber(s.errors)} {(s.error_rate * 100).toFixed(1)}% {s.avg_duration_ms.toFixed(0)} ms {formatDateTime(s.last_started_at)} {healthBadge(s)} {serviceExpanded === s.name && ( )} ))}
)}
) } // CIL[END chunk_id="ServicesPage_servicesPage_1785000051"]