mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
d6ca0e5900
## Problem The API Gateway and Data API observability reports still send legacy BigQuery SQL to the logs.all endpoint. The Data API shared-report hook also hardcodes logs.all, so the otelReports flag cannot move that report to ClickHouse. ## Fix - Add the ClickHouse requests-by-country query needed by API Gateway. - Select API Gateway SQL and endpoint atomically from otelReports. - Route the Data API PostgREST report through the existing tested OTEL API query builders and logs.all.otel. - Wait for ConfigCat before the Data API sends a request, avoiding an initial legacy request while the flag loads. - Keep logs.all behavior when the flag is disabled and leave other shared reports unchanged. ## How to test 1. Enable otelReports and open API Gateway, then confirm its report requests use logs.all.otel. 2. Open Data API and confirm all report requests use logs.all.otel with a request.path filter for /rest. 3. Change the date range, add a filter, and refresh each report. 4. Disable otelReports and confirm both reports use logs.all. All OTEL query shapes were tested individually against logs.all.otel. The focused query suite and lint pass locally. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - API reports can use OpenTelemetry data when enabled. - Added country-level request reporting, excluding requests without country information. - Added OpenTelemetry-backed PostgREST reports and Storage cache hit/miss metrics. - **Improvements** - Reports wait for required configuration before loading data. - Refreshing reports consistently refetches active metrics. - Improved error handling for analytics query failures. - Improved report accuracy with numeric time buckets and more precise attribute filtering. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
204 lines
7.3 KiB
TypeScript
204 lines
7.3 KiB
TypeScript
import { useQueries } from '@tanstack/react-query'
|
|
import { FeatureFlagContext, useFlag, useParams } from 'common'
|
|
import isEqual from 'lodash/isEqual'
|
|
import { useContext, useState } from 'react'
|
|
|
|
import { PRESET_CONFIG } from '@/components/interfaces/Reports/Reports.constants'
|
|
import type { ReportFilterItem } from '@/components/interfaces/Reports/Reports.types'
|
|
import { getLogsSql } from '@/components/interfaces/Reports/Reports.utils'
|
|
import {
|
|
EXPLORER_DATEPICKER_HELPERS,
|
|
getDefaultHelper,
|
|
} from '@/components/interfaces/Settings/Logs/Logs.constants'
|
|
import type { LogData, LogsEndpointParams } from '@/components/interfaces/Settings/Logs/Logs.types'
|
|
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
|
|
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
|
|
import type { SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
|
|
import { reportKeys, type ApiReportRequestParams } from '@/data/reports/keys'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
|
|
|
const apiQueryNames = [
|
|
'totalRequests',
|
|
'topRoutes',
|
|
'errorCounts',
|
|
'topErrorRoutes',
|
|
'responseSpeed',
|
|
'topSlowRoutes',
|
|
'networkTraffic',
|
|
'requestsByCountry',
|
|
] as const
|
|
|
|
const fetchApiReportMetric = async ({
|
|
projectRef,
|
|
params,
|
|
sql,
|
|
useOtel,
|
|
signal,
|
|
}: {
|
|
projectRef: string | undefined
|
|
params: ApiReportRequestParams
|
|
sql: SafeLogSqlFragment
|
|
useOtel: boolean
|
|
signal: AbortSignal
|
|
}): Promise<LogData[]> => {
|
|
if (projectRef === undefined) throw new Error('projectRef is required')
|
|
|
|
const data = await executeAnalyticsSql({
|
|
projectRef,
|
|
endpoint: logsAllEndpointUrl(useOtel),
|
|
sql,
|
|
iso_timestamp_start: params.iso_timestamp_start,
|
|
iso_timestamp_end: params.iso_timestamp_end,
|
|
method: 'get',
|
|
signal,
|
|
})
|
|
|
|
if (data?.error !== undefined) {
|
|
const message = typeof data.error === 'string' ? data.error : data.error.message
|
|
throw new Error(message)
|
|
}
|
|
return (data?.result ?? []) as LogData[]
|
|
}
|
|
|
|
export const useApiReport = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const state = useDatabaseSelectorStateSnapshot()
|
|
const flagUseOtel = useFlag('otelReports')
|
|
const { hasLoaded: hasLoadedFlags } = useContext(FeatureFlagContext)
|
|
const [filters, setFilters] = useState<ReportFilterItem[]>([])
|
|
const [requestParams, setRequestParams] = useState<ApiReportRequestParams>(() => {
|
|
const defaultDateRange = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
|
|
return {
|
|
iso_timestamp_start: defaultDateRange.calcFrom(),
|
|
iso_timestamp_end: defaultDateRange.calcTo(),
|
|
}
|
|
})
|
|
|
|
const identifier = state.selectedDatabaseId
|
|
const useOtel = IS_PLATFORM && Boolean(flagUseOtel)
|
|
const resolvedProjectRef = IS_PLATFORM ? projectRef : (projectRef ?? 'default')
|
|
const areQueriesReady =
|
|
(!IS_PLATFORM || hasLoadedFlags === true) && resolvedProjectRef !== undefined
|
|
const formattedFilters: ReportFilterItem[] = [
|
|
...filters,
|
|
...(identifier !== undefined
|
|
? [{ key: 'identifier', value: identifier, compare: 'is' } as ReportFilterItem]
|
|
: []),
|
|
]
|
|
const sql = {
|
|
totalRequests: getLogsSql(PRESET_CONFIG.api.queries.totalRequests, formattedFilters, useOtel),
|
|
topRoutes: getLogsSql(PRESET_CONFIG.api.queries.topRoutes, formattedFilters, useOtel),
|
|
errorCounts: getLogsSql(PRESET_CONFIG.api.queries.errorCounts, formattedFilters, useOtel),
|
|
topErrorRoutes: getLogsSql(PRESET_CONFIG.api.queries.topErrorRoutes, formattedFilters, useOtel),
|
|
responseSpeed: getLogsSql(PRESET_CONFIG.api.queries.responseSpeed, formattedFilters, useOtel),
|
|
topSlowRoutes: getLogsSql(PRESET_CONFIG.api.queries.topSlowRoutes, formattedFilters, useOtel),
|
|
networkTraffic: getLogsSql(PRESET_CONFIG.api.queries.networkTraffic, formattedFilters, useOtel),
|
|
requestsByCountry: getLogsSql(
|
|
PRESET_CONFIG.api.queries.requestsByCountry,
|
|
formattedFilters,
|
|
useOtel
|
|
),
|
|
}
|
|
|
|
const activeQueries = useQueries({
|
|
queries: apiQueryNames.map((queryName) => ({
|
|
queryKey: reportKeys.apiMetric(
|
|
resolvedProjectRef,
|
|
queryName,
|
|
requestParams,
|
|
sql[queryName],
|
|
useOtel
|
|
),
|
|
queryFn: ({ signal }: { signal: AbortSignal }) =>
|
|
fetchApiReportMetric({
|
|
projectRef: resolvedProjectRef,
|
|
params: requestParams,
|
|
sql: sql[queryName],
|
|
useOtel,
|
|
signal,
|
|
}),
|
|
enabled: areQueriesReady,
|
|
refetchOnWindowFocus: false,
|
|
})),
|
|
})
|
|
const [
|
|
totalRequests,
|
|
topRoutes,
|
|
errorCounts,
|
|
topErrorRoutes,
|
|
responseSpeed,
|
|
topSlowRoutes,
|
|
networkTraffic,
|
|
requestsByCountry,
|
|
] = activeQueries
|
|
|
|
const addFilter = (filter: ReportFilterItem) => {
|
|
if (filters.some((item) => isEqual(item, filter))) return
|
|
setFilters((previousFilters) =>
|
|
[...previousFilters, filter].sort((first, second) =>
|
|
first.key.toLowerCase().localeCompare(second.key.toLowerCase())
|
|
)
|
|
)
|
|
}
|
|
const removeFilter = (filter: ReportFilterItem) => removeFilters([filter])
|
|
const removeFilters = (filtersToRemove: ReportFilterItem[]) => {
|
|
setFilters((previousFilters) =>
|
|
previousFilters.filter((filter) => !filtersToRemove.some((item) => isEqual(filter, item)))
|
|
)
|
|
}
|
|
const handleRefresh = async () => {
|
|
if (!areQueriesReady) return
|
|
await Promise.all(activeQueries.map((query) => query.refetch()))
|
|
}
|
|
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
|
|
setRequestParams((previousParams) => ({
|
|
iso_timestamp_start: params.iso_timestamp_start ?? previousParams.iso_timestamp_start,
|
|
iso_timestamp_end: params.iso_timestamp_end ?? previousParams.iso_timestamp_end,
|
|
}))
|
|
}
|
|
|
|
const isLoading =
|
|
!areQueriesReady || activeQueries.some((query) => query.isPending || query.isRefetching)
|
|
|
|
return {
|
|
data: {
|
|
totalRequests: totalRequests.data ?? [],
|
|
errorCounts: errorCounts.data ?? [],
|
|
responseSpeed: responseSpeed.data ?? [],
|
|
topRoutes: topRoutes.data ?? [],
|
|
topErrorRoutes: topErrorRoutes.data ?? [],
|
|
topSlowRoutes: topSlowRoutes.data ?? [],
|
|
networkTraffic: networkTraffic.data ?? [],
|
|
requestsByCountry: requestsByCountry.data ?? [],
|
|
},
|
|
params: {
|
|
totalRequests: { ...requestParams, sql: sql.totalRequests },
|
|
errorCounts: { ...requestParams, sql: sql.errorCounts },
|
|
responseSpeed: { ...requestParams, sql: sql.responseSpeed },
|
|
topRoutes: { ...requestParams, sql: sql.topRoutes },
|
|
topErrorRoutes: { ...requestParams, sql: sql.topErrorRoutes },
|
|
topSlowRoutes: { ...requestParams, sql: sql.topSlowRoutes },
|
|
networkTraffic: { ...requestParams, sql: sql.networkTraffic },
|
|
requestsByCountry: { ...requestParams, sql: sql.requestsByCountry },
|
|
},
|
|
error: {
|
|
totalRequest: totalRequests.error?.message ?? null,
|
|
errorCounts: errorCounts.error?.message ?? null,
|
|
responseSpeed: responseSpeed.error?.message ?? null,
|
|
topRoutes: topRoutes.error?.message ?? null,
|
|
topErrorRoute: topErrorRoutes.error?.message ?? null,
|
|
topSlowRoutes: topSlowRoutes.error?.message ?? null,
|
|
networkTraffic: networkTraffic.error?.message ?? null,
|
|
requestsByCountry: requestsByCountry.error?.message ?? null,
|
|
},
|
|
mergeParams: handleSetParams,
|
|
filters,
|
|
addFilter,
|
|
removeFilter,
|
|
removeFilters,
|
|
isLoading,
|
|
refresh: handleRefresh,
|
|
}
|
|
}
|