Files
supabase__supabase/apps/studio/data/edge-functions/edge-functions-last-hour-stats-query.ts
Jordi Enric 982d860123 feat(functions): migrate last-hour stats query to OTEL ClickHouse (#47693)
## What

Migrates the `edge-functions-last-hour-stats` query (requests + server
error counts shown on the Edge Functions list) from the BigQuery-style
`logs.all` endpoint to the OTEL/ClickHouse `logs.all.otel` endpoint.

<img width="2378" height="958" alt="CleanShot 2026-07-07 at 16 24 43@2x"
src="https://github.com/user-attachments/assets/5bf3f04c-43e1-44a3-af28-d53feee27f68"
/>

## How

- Adds an OTEL SQL builder that reads from the single `logs` table
(`source = 'function_edge_logs'`), using `log_attributes['function_id']`
and `toInt32OrZero(log_attributes['response.status_code'])` instead of
`cross join unnest(metadata)`.
- Gated by the `otelLegacyLogs` flag, matching the rest of the logs
code. The BigQuery path is preserved when the flag is off, and the two
paths cache under separate query keys.

## Testing

- Unit tests cover both endpoints and assert the generated SQL for each
path.
- Go to edge fns list
- stats load correctly

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for using the OTEL logs backend for edge function
last-hour stats when enabled.
* Queries and caching now automatically distinguish between the standard
and OTEL-backed data sources.
* **Bug Fixes**
* Ensured stats results are fetched from the correct endpoint based on
the selected logging backend.
  * Added coverage to verify OTEL-specific SQL and response behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-08 17:16:08 +02:00

160 lines
4.8 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useFlag } from 'common'
import dayjs from 'dayjs'
import { edgeFunctionsKeys } from './keys'
import { handleError } from '@/data/fetchers'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
import {
analyticsLiteral,
joinSqlFragments,
safeSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type EdgeFunctionsLastHourStatsVariables = {
projectRef?: string
functionIds?: string[]
useOtel?: boolean
}
export type EdgeFunctionLastHourStats = {
functionId: string
requestsCount: number
serverErrorCount: number
errorRate: number
}
export type EdgeFunctionsLastHourStatsResponse = Record<string, EdgeFunctionLastHourStats>
function getEdgeFunctionsLastHourStatsSqlBq(functionIds: string[]): SafeLogSqlFragment {
const functionIdFilter: SafeLogSqlFragment =
functionIds.length > 0
? safeSql` and function_id in (${joinSqlFragments(functionIds.map(analyticsLiteral), ', ')})\n`
: safeSql``
return safeSql`
-- edge-functions-last-hour-stats
select
function_id,
count(distinct id) as requests_count,
count(distinct case when response.status_code >= 500 then id end) as server_err_count
from
function_edge_logs
cross join unnest(metadata) as m
cross join unnest(m.response) as response
where
function_id is not null
${functionIdFilter}group by
function_id
`
}
function getEdgeFunctionsLastHourStatsSqlOtel(functionIds: string[]): SafeLogSqlFragment {
const functionIdFilter: SafeLogSqlFragment =
functionIds.length > 0
? safeSql` and log_attributes['function_id'] in (${joinSqlFragments(functionIds.map(analyticsLiteral), ', ')})\n`
: safeSql``
return safeSql`
-- edge-functions-last-hour-stats
select
log_attributes['function_id'] as function_id,
count(distinct id) as requests_count,
count(distinct case when toInt32OrZero(log_attributes['response.status_code']) >= 500 then id end) as server_err_count
from logs
where
source = 'function_edge_logs'
and log_attributes['function_id'] != ''
${functionIdFilter}group by
function_id
`
}
function getEdgeFunctionsLastHourStatsSql(
functionIds: string[],
useOtel: boolean
): SafeLogSqlFragment {
return useOtel
? getEdgeFunctionsLastHourStatsSqlOtel(functionIds)
: getEdgeFunctionsLastHourStatsSqlBq(functionIds)
}
export async function getEdgeFunctionsLastHourStats(
{ projectRef, functionIds = [], useOtel = false }: EdgeFunctionsLastHourStatsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
if (functionIds.length === 0) return {}
const endDate = dayjs().toISOString()
const startDate = dayjs().subtract(1, 'hour').toISOString()
const data = await executeAnalyticsSql({
projectRef,
endpoint: logsAllEndpointUrl(useOtel),
sql: getEdgeFunctionsLastHourStatsSql(functionIds, useOtel),
iso_timestamp_start: startDate,
iso_timestamp_end: endDate,
key: 'last-hour-stats',
signal,
})
if (data?.error) handleError(data.error)
const result = (data?.result ?? []) as {
function_id: string
requests_count: number | string
server_err_count: number | string
}[]
return result.reduce<EdgeFunctionsLastHourStatsResponse>((acc, row) => {
const toSafeNumber = (v: number | string | undefined) => {
const n = Number(v ?? 0)
return Number.isFinite(n) ? n : 0
}
const safeRequestsCount = toSafeNumber(row.requests_count)
const safeServerErrorCount = toSafeNumber(row.server_err_count)
acc[row.function_id] = {
functionId: row.function_id,
requestsCount: safeRequestsCount,
serverErrorCount: safeServerErrorCount,
errorRate: safeRequestsCount > 0 ? (safeServerErrorCount / safeRequestsCount) * 100 : 0,
}
return acc
}, {})
}
export type EdgeFunctionsLastHourStatsData = Awaited<
ReturnType<typeof getEdgeFunctionsLastHourStats>
>
export type EdgeFunctionsLastHourStatsError = ResponseError
export const useEdgeFunctionsLastHourStatsQuery = <TData = EdgeFunctionsLastHourStatsData>(
{ projectRef, functionIds = [] }: EdgeFunctionsLastHourStatsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<
EdgeFunctionsLastHourStatsData,
EdgeFunctionsLastHourStatsError,
TData
> = {}
) => {
const useOtel = useFlag('otelLegacyLogs')
return useQuery<EdgeFunctionsLastHourStatsData, EdgeFunctionsLastHourStatsError, TData>({
queryKey: edgeFunctionsKeys.lastHourStats(projectRef, functionIds, useOtel),
queryFn: ({ signal }) =>
getEdgeFunctionsLastHourStats({ projectRef, functionIds, useOtel }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && functionIds.length > 0,
staleTime: 60 * 1000,
retry: false,
...options,
})
}