Files
supabase/apps/studio/components/interfaces/Observability/ServiceHealthTable.tsx
Jordi EnricandClaude Sonnet 4.6 4ca7e66153 feat(observability): migrate overview to service-health endpoint (#46100)
## Problem

The observability overview page fetched service health data by making
six separate calls to the generic \`logs.all\` endpoint with
hand-crafted SQL (via \`genChartQuery\`). This coupled the overview to
SQL internals and missed out on the purpose-built \`service-health\`
endpoint that accepts structured \`lql\` filters and a \`granularity\`
parameter.

## Fix

- Added \`/platform/projects/{ref}/analytics/endpoints/service-health\`
to \`platform.d.ts\`, including the \`ProjectServiceHealthResponse\`
schema and \`UsageApiController_getProjectServiceHealth\` operation.
- Created \`apps/studio/data/analytics/service-health-query.ts\` with a
\`getServiceHealth\` fetch function and \`useServiceHealthQuery\` hook
following the same pattern as other analytics query files.
- Added a \`serviceHealth\` key factory to
\`apps/studio/data/analytics/keys.ts\`.
- Rewrote \`useServiceHealthMetrics.ts\` to call the new endpoint per
service using \`lql\` selectors (\`s:postgres_logs\`, \`s:auth_logs\`,
etc.) and a \`granularity\` value derived from the selected interval
(\`1hr\` -> \`minute\`, \`1day\` -> \`hour\`, \`7day\` -> \`day\`). The
timeseries normalisation and chart data pipeline is unchanged.
- Updated the refresh handler in \`ObservabilityOverview.tsx\` to
invalidate the new query key prefix and removed the now-unused
\`postgrest-overview-metrics\` invalidation.

## How to test

- Navigate to a project's Observability > Overview page.
- Verify that the Service Health table loads data for all six services
(Database, Auth, Edge Functions, Realtime, Storage, Data API).
- Switch between the 1hr, 1day, and 7day interval selectors and confirm
the charts update.
- Click the Refresh button and confirm the charts reload.
- Click a bar in any chart and confirm navigation to the corresponding
logs page scoped to that time window.
- Confirm no regressions in the Database Infrastructure section (CPU,
RAM, disk, connections).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Centralized service‑health fetching for consistent cross‑service
metrics and improved charting.
* New analytics key and backend endpoint for project service‑health; API
schemas added.
* Backend support for an additional log‑drain type (hidden from the UI).

* **Bug Fixes**
  * Improved refresh behavior for service‑health data.
* Clear "No requests in this period" fallback and correct charts when
totals are zero.

* **Tests**
* Added unit tests for service‑health data extraction and
transformation.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46100?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:01:54 +02:00

172 lines
5.4 KiB
TypeScript

import { ChevronRight, HelpCircle } from 'lucide-react'
import Link from 'next/link'
import { Card, CardContent, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
import { ButtonTooltip } from '../../ui/ButtonTooltip'
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
import type { ServiceKey } from './ObservabilityOverview.utils'
type ServiceConfig = {
key: ServiceKey
name: string
description: string
reportUrl?: string
logsUrl: string
}
type ServiceData = {
total: number
errorRate: number
errorCount: number
warningCount: number
eventChartData: LogsBarChartDatum[]
isLoading: boolean
}
export type ServiceHealthTableProps = {
services: ServiceConfig[]
serviceData: Record<string, ServiceData>
onBarClick: (logsUrl: string) => (datum: LogsBarChartDatum) => void
datetimeFormat: string
}
const SERVICE_DESCRIPTIONS: Record<ServiceKey, string> = {
db: 'PostgreSQL database health and performance',
auth: 'Authentication and user management',
functions: 'Serverless Edge Functions execution',
storage: 'Object storage for files and assets',
realtime: 'WebSocket connections and broadcasts',
postgrest: 'Auto-generated REST API for your database',
}
type ServiceRowProps = {
service: ServiceConfig
data: ServiceData
onBarClick: (datum: LogsBarChartDatum) => void
datetimeFormat: string
}
const ServiceRow = ({ service, data, onBarClick, datetimeFormat }: ServiceRowProps) => {
const errorRate = data.total > 0 ? data.errorRate : 0
const warningRate = data.total > 0 ? (data.warningCount / data.total) * 100 : 0
const reportUrl = service.reportUrl || service.logsUrl
return (
<Link
href={reportUrl}
className="block group py-4 px-card border-b border-default last:border-b-0 hover:bg-surface-200 transition-colors cursor-pointer"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-foreground font-medium">{service.name}</span>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => e.preventDefault()}
className="text-foreground-lighter hover:text-foreground-light transition-colors"
>
<HelpCircle size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p>{SERVICE_DESCRIPTIONS[service.key as ServiceKey] || service.description}</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<ButtonTooltip
type="text"
size="tiny"
className="p-1.5"
tooltip={{ content: { text: `Go to ${service.name} report` } }}
>
<ChevronRight
size={14}
className="text-foreground-lighter group-hover:text-foreground"
/>
</ButtonTooltip>
</div>
</div>
<div className="h-16" onClick={(e) => e.preventDefault()}>
<Loading active={data.isLoading}>
{data.isLoading ? (
<div />
) : (
<LogsBarChart
data={data.total === 0 ? [] : data.eventChartData}
DateTimeFormat={datetimeFormat}
onBarClick={onBarClick}
EmptyState={
<div className="flex items-center justify-center h-full text-xs text-foreground-lighter">
No data
</div>
}
/>
)}
</Loading>
</div>
<div className="flex items-center justify-center mt-2 text-xs text-foreground-lighter gap-4 font-mono tabular-nums tracking-tight">
{data.total > 0 ? (
<>
{errorRate > 0 && (
<span className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-destructive" />
{errorRate.toFixed(2)}% errors
</span>
)}
{warningRate > 0 && (
<span className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-warning" />
{warningRate.toFixed(2)}% warnings
</span>
)}
{errorRate === 0 && warningRate === 0 && (
<span className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-brand" />
0% errors
</span>
)}
</>
) : !data.isLoading ? (
<span>No requests in this period</span>
) : null}
</div>
</Link>
)
}
export const ServiceHealthTable = ({
services,
serviceData,
onBarClick,
datetimeFormat,
}: ServiceHealthTableProps) => {
return (
<div>
<h2 className="heading-section mb-4">Service Health</h2>
<Card>
<CardContent className="p-0">
{services.map((service) => {
const data = serviceData[service.key]
if (!data) return null
return (
<ServiceRow
key={service.key}
service={service}
data={data}
onBarClick={onBarClick(service.logsUrl)}
datetimeFormat={datetimeFormat}
/>
)
})}
</CardContent>
</Card>
</div>
)
}