mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## 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 -->
[](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>
188 lines
6.7 KiB
TypeScript
188 lines
6.7 KiB
TypeScript
import { useQueryClient } from '@tanstack/react-query'
|
|
import { useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { RefreshCw } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
|
|
import { DatabaseInfrastructureSection } from './DatabaseInfrastructureSection'
|
|
import { useObservabilityOverviewData } from './ObservabilityOverview.utils'
|
|
import { ObservabilityOverviewFooter } from './ObservabilityOverviewFooter'
|
|
import { ServiceHealthTable } from './ServiceHealthTable'
|
|
import { useSlowQueriesCount } from './useSlowQueriesCount'
|
|
import ReportHeader from '@/components/interfaces/Reports/ReportHeader'
|
|
import ReportPadding from '@/components/interfaces/Reports/ReportPadding'
|
|
import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown'
|
|
import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
|
|
type ChartIntervalKey = '1hr' | '1day' | '7day'
|
|
|
|
export const ObservabilityOverview = () => {
|
|
const router = useRouter()
|
|
const { ref: projectRef } = useParams()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const queryClient = useQueryClient()
|
|
|
|
const { projectStorageAll: storageSupported } = useIsFeatureEnabled(['project_storage:all'])
|
|
|
|
const DEFAULT_INTERVAL: ChartIntervalKey = '1day'
|
|
const [interval, setInterval] = useState<ChartIntervalKey>(DEFAULT_INTERVAL)
|
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
|
|
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
|
|
|
const { datetimeFormat } = useMemo(() => {
|
|
const format = selectedInterval.format || 'MMM D, ha'
|
|
return { datetimeFormat: format }
|
|
}, [selectedInterval])
|
|
|
|
const overviewData = useObservabilityOverviewData(projectRef!, interval, refreshKey)
|
|
|
|
const { slowQueriesCount, isLoading: slowQueriesLoading } = useSlowQueriesCount(
|
|
projectRef,
|
|
refreshKey
|
|
)
|
|
|
|
const handleRefresh = useCallback(() => {
|
|
setRefreshKey((prev) => prev + 1)
|
|
queryClient.invalidateQueries({ queryKey: ['projects', projectRef, 'service-health'] })
|
|
queryClient.invalidateQueries({ queryKey: ['project-metrics'] })
|
|
queryClient.invalidateQueries({ queryKey: ['infra-monitoring'] })
|
|
queryClient.invalidateQueries({ queryKey: ['max-connections'] })
|
|
}, [queryClient, projectRef])
|
|
|
|
const serviceBase = useMemo(
|
|
() => [
|
|
{
|
|
key: 'db' as const,
|
|
name: 'Database',
|
|
reportUrl: `/project/${projectRef}/observability/database`,
|
|
logsUrl: `/project/${projectRef}/logs/postgres-logs`,
|
|
enabled: true,
|
|
hasReport: true,
|
|
},
|
|
{
|
|
key: 'auth' as const,
|
|
name: 'Auth',
|
|
reportUrl: `/project/${projectRef}/observability/auth`,
|
|
logsUrl: `/project/${projectRef}/logs/auth-logs`,
|
|
enabled: true,
|
|
hasReport: true,
|
|
},
|
|
{
|
|
key: 'functions' as const,
|
|
name: 'Edge Functions',
|
|
reportUrl: `/project/${projectRef}/observability/edge-functions`,
|
|
logsUrl: `/project/${projectRef}/logs/edge-functions-logs`,
|
|
enabled: true,
|
|
hasReport: true,
|
|
},
|
|
{
|
|
key: 'realtime' as const,
|
|
name: 'Realtime',
|
|
reportUrl: `/project/${projectRef}/observability/realtime`,
|
|
logsUrl: `/project/${projectRef}/logs/realtime-logs`,
|
|
enabled: true,
|
|
hasReport: true,
|
|
},
|
|
{
|
|
key: 'storage' as const,
|
|
name: 'Storage',
|
|
reportUrl: `/project/${projectRef}/observability/storage`,
|
|
logsUrl: `/project/${projectRef}/logs/storage-logs`,
|
|
enabled: storageSupported,
|
|
hasReport: true,
|
|
},
|
|
{
|
|
key: 'postgrest' as const,
|
|
name: 'Data API',
|
|
reportUrl: `/project/${projectRef}/observability/postgrest`,
|
|
logsUrl: `/project/${projectRef}/logs/postgrest-logs`,
|
|
enabled: true,
|
|
hasReport: true,
|
|
},
|
|
],
|
|
[projectRef, storageSupported]
|
|
)
|
|
|
|
const enabledServices = serviceBase.filter((s) => s.enabled)
|
|
|
|
const dbServiceData = overviewData.services.db
|
|
|
|
// Navigate to the log view scoped to the clicked bar's bucket window
|
|
const handleBarClick = useCallback(
|
|
(logsUrl: string) => (datum: any) => {
|
|
if (!datum?.timestamp) return
|
|
|
|
// datum.timestamp is already the UTC-truncated bucket boundary from timestamp_trunc(),
|
|
// so use it directly to avoid local-timezone startOf() misalignment (e.g. UTC+5:30).
|
|
const unit = interval === '1hr' ? 'minute' : 'hour'
|
|
const start = datum.timestamp
|
|
const end = dayjs.utc(datum.timestamp).add(1, unit).toISOString()
|
|
|
|
const queryParams = new URLSearchParams({ its: start, ite: end })
|
|
router.push(`${logsUrl}?${queryParams.toString()}`)
|
|
},
|
|
[router, interval]
|
|
)
|
|
|
|
return (
|
|
<ReportPadding>
|
|
<div className="flex flex-row justify-between items-center">
|
|
<div className="flex items-center gap-3">
|
|
<ReportHeader title="Overview" />
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Badge variant="warning">Beta</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>This page is subject to change</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button type="outline" icon={<RefreshCw size={14} />} onClick={handleRefresh}>
|
|
Refresh
|
|
</Button>
|
|
<ChartIntervalDropdown
|
|
value={interval}
|
|
onChange={(interval) => setInterval(interval as ChartIntervalKey)}
|
|
organizationSlug={organization?.slug}
|
|
dropdownAlign="end"
|
|
tooltipSide="left"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-12 mt-8">
|
|
<DatabaseInfrastructureSection
|
|
interval={interval}
|
|
refreshKey={refreshKey}
|
|
dbErrorRate={dbServiceData.errorRate}
|
|
isLoading={dbServiceData.isLoading}
|
|
slowQueriesCount={slowQueriesCount}
|
|
slowQueriesLoading={slowQueriesLoading}
|
|
/>
|
|
|
|
<ServiceHealthTable
|
|
services={enabledServices.map((service) => ({
|
|
key: service.key,
|
|
name: service.name,
|
|
description: '',
|
|
reportUrl: service.hasReport ? service.reportUrl : undefined,
|
|
logsUrl: service.logsUrl,
|
|
}))}
|
|
serviceData={overviewData.services}
|
|
onBarClick={handleBarClick}
|
|
datetimeFormat={datetimeFormat}
|
|
/>
|
|
</div>
|
|
|
|
<ObservabilityOverviewFooter />
|
|
</ReportPadding>
|
|
)
|
|
}
|