useServiceHealthMetrics.ts226 lines · main
1import { useQuery } from '@tanstack/react-query'
2import { useMemo } from 'react'
3
4import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
5import { LogsTableName } from '../Settings/Logs/Logs.constants'
6import { genChartQuery } from '../Settings/Logs/Logs.utils'
7import {
8 calculateAggregatedMetrics,
9 calculateDateRange,
10 calculateHealthMetrics,
11 transformToBarChartData,
12 type RawChartData,
13} from './useServiceHealthMetrics.utils'
14import { get } from '@/data/fetchers'
15import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
16import useTimeseriesUnixToIso from '@/hooks/analytics/useTimeseriesUnixToIso'
17
18export type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' | 'postgrest'
19
20export 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
33type ServiceConfig = {
34 table: LogsTableName
35 enabled: boolean
36}
37
38const 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
47type 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 */
57const 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 */
95const 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 */
153export 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}