Files
supabase/apps/studio/components/interfaces/Observability/useServiceHealthMetrics.ts

227 lines
5.7 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
import { LogsTableName } from '../Settings/Logs/Logs.constants'
import { genChartQuery } from '../Settings/Logs/Logs.utils'
import {
calculateAggregatedMetrics,
calculateDateRange,
calculateHealthMetrics,
transformToBarChartData,
type RawChartData,
} from './useServiceHealthMetrics.utils'
import { get } from '@/data/fetchers'
import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
import useTimeseriesUnixToIso from '@/hooks/analytics/useTimeseriesUnixToIso'
export type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' | 'postgrest'
export type ServiceHealthData = {
total: number
errorRate: number
successRate: number
errorCount: number
warningCount: number
okCount: number
eventChartData: LogsBarChartDatum[]
isLoading: boolean
error: unknown | null
refresh: () => void
}
type ServiceConfig = {
table: LogsTableName
enabled: boolean
}
const SERVICE_CONFIG: Record<ServiceKey, ServiceConfig> = {
db: { table: LogsTableName.POSTGRES, enabled: true },
auth: { table: LogsTableName.AUTH, enabled: true },
functions: { table: LogsTableName.FN_EDGE, enabled: true },
storage: { table: LogsTableName.STORAGE, enabled: true },
realtime: { table: LogsTableName.REALTIME, enabled: true },
postgrest: { table: LogsTableName.POSTGREST, enabled: true },
}
type ChartQueryResult = {
timestamp: string | number
ok_count: number
warning_count: number
error_count: number
}
/**
* Fetches service health metrics using the same logic as the logs pages
*/
const fetchServiceHealthMetrics = async (
projectRef: string,
table: LogsTableName,
startDate: string,
endDate: string,
signal?: AbortSignal
): Promise<ChartQueryResult[]> => {
const sql = genChartQuery(
table,
{
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
},
{}
)
const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
params: {
path: { ref: projectRef },
query: {
sql,
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
},
},
signal,
})
if (error ?? data?.error) {
throw error ?? data?.error
}
return (data?.result ?? []) as ChartQueryResult[]
}
/**
* Hook to fetch health metrics for a single service
*/
const useServiceHealthQuery = ({
projectRef,
serviceKey,
startDate,
endDate,
enabled,
}: {
projectRef: string
serviceKey: ServiceKey
startDate: string
endDate: string
enabled: boolean
}) => {
const config = SERVICE_CONFIG[serviceKey]
const table = config.table
const queryResult = useQuery({
queryKey: ['service-health-metrics', projectRef, serviceKey, startDate, endDate, table],
queryFn: ({ signal }) =>
fetchServiceHealthMetrics(projectRef, table, startDate, endDate, signal),
enabled: enabled && config.enabled && Boolean(projectRef),
staleTime: 1000 * 60, // 1 minute
})
// Convert unix microseconds to ISO timestamps
const normalizedData = useTimeseriesUnixToIso(queryResult.data ?? [], 'timestamp')
// Fill gaps in timeseries
const { data: filledData } = useFillTimeseriesSorted({
data: normalizedData,
timestampKey: 'timestamp',
valueKey: 'ok_count',
defaultValue: 0,
startDate,
endDate,
})
// Transform to LogsBarChartDatum format
const eventChartData: LogsBarChartDatum[] = useMemo(
() => transformToBarChartData(filledData as RawChartData[]),
[filledData]
)
// Calculate metrics
const metrics = useMemo(() => calculateHealthMetrics(eventChartData), [eventChartData])
return {
...metrics,
eventChartData,
isLoading: queryResult.isLoading,
error: queryResult.error,
refresh: queryResult.refetch,
}
}
/**
* Hook to fetch observability overview data for all services using logs page queries
*/
export const useServiceHealthMetrics = (
projectRef: string,
interval: '1hr' | '1day' | '7day',
refreshKey: number
) => {
// Calculate date range based on interval
// refreshKey is intentionally included to force recalculation when user refreshes
// eslint-disable-next-line react-hooks/exhaustive-deps
const { startDate, endDate } = useMemo(() => calculateDateRange(interval), [interval, refreshKey])
const enabled = Boolean(projectRef)
// Fetch metrics for each service
const db = useServiceHealthQuery({ projectRef, serviceKey: 'db', startDate, endDate, enabled })
const auth = useServiceHealthQuery({
projectRef,
serviceKey: 'auth',
startDate,
endDate,
enabled,
})
const functions = useServiceHealthQuery({
projectRef,
serviceKey: 'functions',
startDate,
endDate,
enabled,
})
const storage = useServiceHealthQuery({
projectRef,
serviceKey: 'storage',
startDate,
endDate,
enabled,
})
const realtime = useServiceHealthQuery({
projectRef,
serviceKey: 'realtime',
startDate,
endDate,
enabled,
})
const postgrest = useServiceHealthQuery({
projectRef,
serviceKey: 'postgrest',
startDate,
endDate,
enabled,
})
const services: Record<ServiceKey, ServiceHealthData> = useMemo(
() => ({
db,
auth,
functions,
storage,
realtime,
postgrest,
}),
[db, auth, functions, storage, realtime, postgrest]
)
// Calculate aggregated metrics
const aggregated = useMemo(() => calculateAggregatedMetrics(Object.values(services)), [services])
const isLoading = Object.values(services).some((s) => s.isLoading)
return {
services,
aggregated,
isLoading,
endDate,
}
}