Files
supabase/apps/studio/hooks/analytics/useSingleLog.tsx
Jordi Enric e0ba04caf4 feat(studio): migrate per-service log pages to OTEL endpoint behind a flag DEBUG-145 (#47264)
## Problem

The legacy per-service log pages (postgres, auth, api, edge functions,
storage, realtime, cron, etc.) and the single-log detail panel query the
BigQuery-backed `logs.all` analytics endpoint. We are moving these reads
onto the OTEL ClickHouse endpoint (`logs.all.otel`).

## Fix

- Add `Logs.utils.otel.ts`: ClickHouse query builders
(rows/count/chart/single) + row mappers that target the single `logs`
table keyed by `source`, reading fields from the `log_attributes` map
and aliasing columns to the leaf names the renderers expect.
- Parameterize `buildWhereClauses` / `genWhereStatement` in
`Logs.utils.ts` so the OTEL builders reuse the shared nested AND/OR
filter grouping. Defaults keep the BigQuery behavior unchanged.
- Gate `useLogsPreview` (rows, count, chart) and `useSingleLog` (detail)
on the new `otelLegacyLogs` flag. BigQuery stays the default when the
flag is off.
- Extract the OTEL timestamp parser into `parseOtelTimestamp`
(`otel-inspection.utils.ts`) and reuse it in
`unified-logs-infinite-query.ts` (replaces an inline copy of the same
logic; no behavior change).

## Dependencies

None. Standalone, safe to merge on its own. Behind `otelLegacyLogs` (off
by default), so no user-facing change.

Part of DEBUG-145 (split from #47087).

## How to test

- In staging, go to Legacy Logs. 
- All logs pages should work the same as before. 

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

* **New Features**
* Added OTEL-backed logs support for preview, count, chart, and
single-log details when enabled.
* **Bug Fixes**
* Improved timestamp parsing/normalization for OTEL data to ensure
correct display and pagination.
* Enhanced filtering behavior, including safer handling of unknown
filter keys and invalid values across OTEL queries.
* Improved single-log result shaping to preserve expected API/database
metadata in OTEL mode.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:31:22 +02:00

116 lines
3.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useFlag } from 'common'
import { useMemo } from 'react'
import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants'
import type {
LogData,
Logs,
LogsEndpointParams,
QueryType,
} from '@/components/interfaces/Settings/Logs/Logs.types'
import { genSingleLogQuery } from '@/components/interfaces/Settings/Logs/Logs.utils'
import {
genSingleLogQueryOtel,
mapOtelSingleLogToLegacy,
} from '@/components/interfaces/Settings/Logs/Logs.utils.otel'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
import { safeSql } from '@/data/logs/safe-analytics-sql'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
interface SingleLogHook {
data: LogData | undefined
error: string | Object | null
isLoading: boolean
refresh: () => void
}
type SingleLogParams = {
id?: string
projectRef: string
queryType?: QueryType
paramsToMerge?: Partial<LogsEndpointParams>
}
function useSingleLog({
projectRef,
id,
queryType,
paramsToMerge,
}: SingleLogParams): SingleLogHook {
const table = queryType ? LOGS_TABLES[queryType] : undefined
// When on, fetch the log from the OTEL endpoint instead of BigQuery.
const useOtel = useFlag('otelLegacyLogs')
const endpoint = logsAllEndpointUrl(useOtel)
const sql = useMemo(() => {
if (!id || !table) return safeSql``
if (useOtel) {
try {
return genSingleLogQueryOtel(id)
} catch {
// Malformed (non-uuid) id — emit nothing rather than throwing in render.
return safeSql``
}
}
return genSingleLogQuery(table, id)
}, [id, table, useOtel])
const enabled = Boolean(id && table)
const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
const {
data,
error: rcError,
isPending,
isRefetching,
refetch,
} = useQuery({
// id and queryType uniquely identify sql without having to stick the
// entire sql in the query key.
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: [
'projects',
projectRef,
'single-log',
id,
queryType,
paramsToMerge?.iso_timestamp_start,
paramsToMerge?.iso_timestamp_end,
{ otel: useOtel },
],
queryFn: async ({ signal }) => {
const data = await executeAnalyticsSql({
projectRef,
endpoint,
sql,
iso_timestamp_start: paramsToMerge?.iso_timestamp_start ?? '',
iso_timestamp_end: paramsToMerge?.iso_timestamp_end ?? '',
method: 'get',
signal,
})
return data as unknown as Logs
},
enabled,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
})
let error: null | string | object = rcError ? (rcError as any).message : null
const rawResult = data?.result ? data.result[0] : undefined
const result = rawResult && useOtel ? mapOtelSingleLogToLegacy(rawResult, queryType) : rawResult
return {
data: !!result
? { ...result, metadata: logsMetadata ? result?.metadata : undefined }
: undefined,
isLoading: (enabled && isPending) || isRefetching,
error,
refresh: () => refetch(),
}
}
export default useSingleLog