useServiceHealthMetrics.ts226 lines · main
| 1 | import { useQuery } from '@tanstack/react-query' |
| 2 | import { useMemo } from 'react' |
| 3 | |
| 4 | import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics' |
| 5 | import { LogsTableName } from '../Settings/Logs/Logs.constants' |
| 6 | import { genChartQuery } from '../Settings/Logs/Logs.utils' |
| 7 | import { |
| 8 | calculateAggregatedMetrics, |
| 9 | calculateDateRange, |
| 10 | calculateHealthMetrics, |
| 11 | transformToBarChartData, |
| 12 | type RawChartData, |
| 13 | } from './useServiceHealthMetrics.utils' |
| 14 | import { get } from '@/data/fetchers' |
| 15 | import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted' |
| 16 | import useTimeseriesUnixToIso from '@/hooks/analytics/useTimeseriesUnixToIso' |
| 17 | |
| 18 | export type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' | 'postgrest' |
| 19 | |
| 20 | export type ServiceHealthData = { |
| 21 | total: number |
| 22 | errorRate: number |
| 23 | successRate: number |
| 24 | errorCount: number |
| 25 | warningCount: number |
| 26 | okCount: number |
| 27 | eventChartData: LogsBarChartDatum[] |
| 28 | isLoading: boolean |
| 29 | error: unknown | null |
| 30 | refresh: () => void |
| 31 | } |
| 32 | |
| 33 | type ServiceConfig = { |
| 34 | table: LogsTableName |
| 35 | enabled: boolean |
| 36 | } |
| 37 | |
| 38 | const SERVICE_CONFIG: Record<ServiceKey, ServiceConfig> = { |
| 39 | db: { table: LogsTableName.POSTGRES, enabled: true }, |
| 40 | auth: { table: LogsTableName.AUTH, enabled: true }, |
| 41 | functions: { table: LogsTableName.FN_EDGE, enabled: true }, |
| 42 | storage: { table: LogsTableName.STORAGE, enabled: true }, |
| 43 | realtime: { table: LogsTableName.REALTIME, enabled: true }, |
| 44 | postgrest: { table: LogsTableName.POSTGREST, enabled: true }, |
| 45 | } |
| 46 | |
| 47 | type ChartQueryResult = { |
| 48 | timestamp: string | number |
| 49 | ok_count: number |
| 50 | warning_count: number |
| 51 | error_count: number |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Fetches service health metrics using the same logic as the logs pages |
| 56 | */ |
| 57 | const fetchServiceHealthMetrics = async ( |
| 58 | projectRef: string, |
| 59 | table: LogsTableName, |
| 60 | startDate: string, |
| 61 | endDate: string, |
| 62 | signal?: AbortSignal |
| 63 | ): Promise<ChartQueryResult[]> => { |
| 64 | const sql = genChartQuery( |
| 65 | table, |
| 66 | { |
| 67 | iso_timestamp_start: startDate, |
| 68 | iso_timestamp_end: endDate, |
| 69 | }, |
| 70 | {} |
| 71 | ) |
| 72 | |
| 73 | const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { |
| 74 | params: { |
| 75 | path: { ref: projectRef }, |
| 76 | query: { |
| 77 | sql, |
| 78 | iso_timestamp_start: startDate, |
| 79 | iso_timestamp_end: endDate, |
| 80 | }, |
| 81 | }, |
| 82 | signal, |
| 83 | }) |
| 84 | |
| 85 | if (error ?? data?.error) { |
| 86 | throw error ?? data?.error |
| 87 | } |
| 88 | |
| 89 | return (data?.result ?? []) as ChartQueryResult[] |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Hook to fetch health metrics for a single service |
| 94 | */ |
| 95 | const useServiceHealthQuery = ({ |
| 96 | projectRef, |
| 97 | serviceKey, |
| 98 | startDate, |
| 99 | endDate, |
| 100 | enabled, |
| 101 | }: { |
| 102 | projectRef: string |
| 103 | serviceKey: ServiceKey |
| 104 | startDate: string |
| 105 | endDate: string |
| 106 | enabled: boolean |
| 107 | }) => { |
| 108 | const config = SERVICE_CONFIG[serviceKey] |
| 109 | const table = config.table |
| 110 | |
| 111 | const queryResult = useQuery({ |
| 112 | queryKey: ['service-health-metrics', projectRef, serviceKey, startDate, endDate, table], |
| 113 | queryFn: ({ signal }) => |
| 114 | fetchServiceHealthMetrics(projectRef, table, startDate, endDate, signal), |
| 115 | enabled: enabled && config.enabled && Boolean(projectRef), |
| 116 | staleTime: 1000 * 60, // 1 minute |
| 117 | }) |
| 118 | |
| 119 | // Convert unix microseconds to ISO timestamps |
| 120 | const normalizedData = useTimeseriesUnixToIso(queryResult.data ?? [], 'timestamp') |
| 121 | |
| 122 | // Fill gaps in timeseries |
| 123 | const { data: filledData } = useFillTimeseriesSorted({ |
| 124 | data: normalizedData, |
| 125 | timestampKey: 'timestamp', |
| 126 | valueKey: 'ok_count', |
| 127 | defaultValue: 0, |
| 128 | startDate, |
| 129 | endDate, |
| 130 | }) |
| 131 | |
| 132 | // Transform to LogsBarChartDatum format |
| 133 | const eventChartData: LogsBarChartDatum[] = useMemo( |
| 134 | () => transformToBarChartData(filledData as RawChartData[]), |
| 135 | [filledData] |
| 136 | ) |
| 137 | |
| 138 | // Calculate metrics |
| 139 | const metrics = useMemo(() => calculateHealthMetrics(eventChartData), [eventChartData]) |
| 140 | |
| 141 | return { |
| 142 | ...metrics, |
| 143 | eventChartData, |
| 144 | isLoading: queryResult.isLoading, |
| 145 | error: queryResult.error, |
| 146 | refresh: queryResult.refetch, |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Hook to fetch observability overview data for all services using logs page queries |
| 152 | */ |
| 153 | export const useServiceHealthMetrics = ( |
| 154 | projectRef: string, |
| 155 | interval: '1hr' | '1day' | '7day', |
| 156 | refreshKey: number |
| 157 | ) => { |
| 158 | // Calculate date range based on interval |
| 159 | // refreshKey is intentionally included to force recalculation when user refreshes |
| 160 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 161 | const { startDate, endDate } = useMemo(() => calculateDateRange(interval), [interval, refreshKey]) |
| 162 | |
| 163 | const enabled = Boolean(projectRef) |
| 164 | |
| 165 | // Fetch metrics for each service |
| 166 | const db = useServiceHealthQuery({ projectRef, serviceKey: 'db', startDate, endDate, enabled }) |
| 167 | const auth = useServiceHealthQuery({ |
| 168 | projectRef, |
| 169 | serviceKey: 'auth', |
| 170 | startDate, |
| 171 | endDate, |
| 172 | enabled, |
| 173 | }) |
| 174 | const functions = useServiceHealthQuery({ |
| 175 | projectRef, |
| 176 | serviceKey: 'functions', |
| 177 | startDate, |
| 178 | endDate, |
| 179 | enabled, |
| 180 | }) |
| 181 | const storage = useServiceHealthQuery({ |
| 182 | projectRef, |
| 183 | serviceKey: 'storage', |
| 184 | startDate, |
| 185 | endDate, |
| 186 | enabled, |
| 187 | }) |
| 188 | const realtime = useServiceHealthQuery({ |
| 189 | projectRef, |
| 190 | serviceKey: 'realtime', |
| 191 | startDate, |
| 192 | endDate, |
| 193 | enabled, |
| 194 | }) |
| 195 | const postgrest = useServiceHealthQuery({ |
| 196 | projectRef, |
| 197 | serviceKey: 'postgrest', |
| 198 | startDate, |
| 199 | endDate, |
| 200 | enabled, |
| 201 | }) |
| 202 | |
| 203 | const services: Record<ServiceKey, ServiceHealthData> = useMemo( |
| 204 | () => ({ |
| 205 | db, |
| 206 | auth, |
| 207 | functions, |
| 208 | storage, |
| 209 | realtime, |
| 210 | postgrest, |
| 211 | }), |
| 212 | [db, auth, functions, storage, realtime, postgrest] |
| 213 | ) |
| 214 | |
| 215 | // Calculate aggregated metrics |
| 216 | const aggregated = useMemo(() => calculateAggregatedMetrics(Object.values(services)), [services]) |
| 217 | |
| 218 | const isLoading = Object.values(services).some((s) => s.isLoading) |
| 219 | |
| 220 | return { |
| 221 | services, |
| 222 | aggregated, |
| 223 | isLoading, |
| 224 | endDate, |
| 225 | } |
| 226 | } |