mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
be030229bf
## Problem Unified Logs does not expose Workers logs, so users cannot search Workers ingress, runtime, or build events alongside other services. ## Fix Add a Workers log type that classifies all three Workers OTEL streams. Gate the option and any persisted Workers filters with the existing Workers feature flag. ## How to test - Enable the Workers feature flag and open Unified Logs. - Select Workers from the Log Type filter. - Expected result: Unified Logs shows ingress, runtime, and build events with the Workers icon. - Disable the Workers feature flag and load a URL containing `log_type:eq:workers`. - Expected result: the Workers option and filter are removed, and Workers logs are not queried. - Run `./node_modules/.bin/vitest --run components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts components/interfaces/UnifiedLogs/UnifiedLogs.utils.test.ts data/workers/worker-logs-query.test.ts` from `apps/studio`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Workers as a selectable log type in Unified Logs. * Unified Logs now combines worker ingress, guest, and API streams under the Workers category. * Added a dedicated Workers icon and worker log filtering. * **Improvements** * Worker filters and URL parameters respect feature availability. * Worker details show relevant metadata while omitting unavailable HTTP fields. * Improved handling of worker log levels, statuses, and raw data. * Added stronger validation for unified log data. * **Tests** * Added coverage for worker routing, filtering, feature visibility, parsing, and metadata redaction. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
import { InfiniteData, keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
|
|
import { useFlag } from 'common'
|
|
|
|
import { executeAnalyticsSql } from './execute-analytics-sql'
|
|
import { logsKeys } from './keys'
|
|
import { logsAllEndpointUrl, pickLogsQueryBuilder } from './logs-endpoint'
|
|
import { analyticsLiteral, safeSql } from './safe-analytics-sql'
|
|
import { mapUnifiedLogRow, parseUnifiedLogsQueryRows } from './unified-logs.utils'
|
|
import { getUnifiedLogsQuery } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.queries'
|
|
import { getUnifiedLogsQuery as getUnifiedLogsQueryBq } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.queries.bq'
|
|
import {
|
|
PageParam,
|
|
QuerySearchParamsType,
|
|
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.types'
|
|
import { handleError } from '@/data/fetchers'
|
|
import type { ResponseError, UseCustomInfiniteQueryOptions } from '@/types'
|
|
|
|
const LOGS_PAGE_LIMIT = 50
|
|
|
|
export const UNIFIED_LOGS_QUERY_OPTIONS = {
|
|
refetchOnWindowFocus: false,
|
|
refetchOnMount: false,
|
|
refetchOnReconnect: false,
|
|
refetchInterval: 0,
|
|
staleTime: 1000 * 60 * 5, // 5 minutes,
|
|
}
|
|
|
|
export type UnifiedLogsData = any
|
|
export type UnifiedLogsError = ResponseError
|
|
export type UnifiedLogsVariables = { projectRef?: string; search: QuerySearchParamsType }
|
|
|
|
export const getUnifiedLogsISOStartEnd = (
|
|
search: QuerySearchParamsType,
|
|
endHoursFromNow: number = 1
|
|
) => {
|
|
// Extract date range from search or use default (last hour)
|
|
let isoTimestampStart: string
|
|
let isoTimestampEnd: string
|
|
|
|
if (search.date && search.date.length === 2) {
|
|
const parseDate = (d: string | Date) => (d instanceof Date ? d : new Date(d))
|
|
isoTimestampStart = parseDate(search.date[0]).toISOString()
|
|
isoTimestampEnd = parseDate(search.date[1]).toISOString()
|
|
} else {
|
|
const now = new Date()
|
|
isoTimestampEnd = now.toISOString()
|
|
const nHoursAgo = new Date(now.getTime() - 60 * 60 * (endHoursFromNow * 1000))
|
|
isoTimestampStart = nHoursAgo.toISOString()
|
|
}
|
|
|
|
return { isoTimestampStart, isoTimestampEnd }
|
|
}
|
|
|
|
export async function getUnifiedLogs(
|
|
{
|
|
projectRef,
|
|
search,
|
|
pageParam,
|
|
useOtel = false,
|
|
}: UnifiedLogsVariables & { pageParam: PageParam | null; useOtel?: boolean },
|
|
signal?: AbortSignal,
|
|
headersInit?: HeadersInit
|
|
) {
|
|
if (typeof projectRef === 'undefined')
|
|
throw new Error('projectRef is required for getUnifiedLogs')
|
|
|
|
/**
|
|
* [Joshen] RE infinite loading pagination logic for unified logs, these all really should live in the API
|
|
* but for now we're doing these on the FE to move quickly while figuring out what data we need before we
|
|
* migrate this logic to the BE. Just thought to leave a small explanation on the logic here:
|
|
*
|
|
* We're leveraging on the log's timestamp to essentially fetch the next page
|
|
* Given that the logs are ordered descending (latest logs come first, and we're fetching older logs as we scroll down)
|
|
* Hence why the cursor is basically the last row's timestamp from the latest page
|
|
*
|
|
* iso_timestamp_start will always be the current timestamp
|
|
* iso_timestamp_end will default to the last hour for the first page, followed by the last row's timestamp from
|
|
* the previous page.
|
|
*
|
|
* However, just note that this isn't a perfect solution as there's always the edge case where by there's multiple rows
|
|
* with identical timestamps, hence why FE will need a de-duping logic (in UnifiedLogs.tsx) unless we can figure a cleaner
|
|
* solution when we move all this logic to the BE (e.g using composite columns for the cursor like timestamp + id)
|
|
*
|
|
*/
|
|
|
|
const { isoTimestampStart, isoTimestampEnd } = getUnifiedLogsISOStartEnd(search)
|
|
const buildQuery = pickLogsQueryBuilder(useOtel, getUnifiedLogsQuery, getUnifiedLogsQueryBq)
|
|
const sql = safeSql`${buildQuery(search)} ORDER BY timestamp DESC, id DESC LIMIT ${analyticsLiteral(LOGS_PAGE_LIMIT)}`
|
|
|
|
const cursorValue = pageParam?.cursor
|
|
const cursorDirection = pageParam?.direction
|
|
|
|
let timestampEnd: string
|
|
|
|
if (cursorDirection === 'prev') {
|
|
// Live mode: fetch logs newer than the cursor
|
|
timestampEnd = new Date().toISOString()
|
|
} else if (cursorDirection === 'next') {
|
|
// Regular pagination: fetch logs older than the cursor.
|
|
// The cursor is stored as milliseconds (set below from `date.getTime()`),
|
|
// so we can convert it directly without worrying about the wire format.
|
|
timestampEnd =
|
|
cursorValue !== null && cursorValue !== undefined
|
|
? new Date(Number(cursorValue)).toISOString()
|
|
: isoTimestampEnd
|
|
} else {
|
|
timestampEnd = isoTimestampEnd
|
|
}
|
|
|
|
const endpoint = logsAllEndpointUrl(useOtel)
|
|
const data = await executeAnalyticsSql({
|
|
projectRef,
|
|
endpoint,
|
|
sql,
|
|
iso_timestamp_start: isoTimestampStart,
|
|
iso_timestamp_end: timestampEnd,
|
|
signal,
|
|
headers: headersInit,
|
|
})
|
|
|
|
if (data.error) handleError(new Error(data.error as string))
|
|
|
|
const resultData = parseUnifiedLogsQueryRows(data?.result)
|
|
const result = resultData.map(mapUnifiedLogRow)
|
|
|
|
const firstRow = result.length > 0 ? result[0] : null
|
|
const lastRow = result.length > 0 ? result[result.length - 1] : null
|
|
const hasMore = result.length >= LOGS_PAGE_LIMIT - 1
|
|
|
|
// Cursors are stored as milliseconds (Date.getTime()) so the OTEL endpoint's
|
|
// wire format (ISO string vs numeric microseconds) doesn't bleed into pagination.
|
|
const nextCursor = lastRow ? lastRow.date.getTime() : null
|
|
const prevCursor = firstRow ? firstRow.date.getTime() : new Date().getTime()
|
|
|
|
return {
|
|
data: result,
|
|
nextCursor: hasMore ? nextCursor : null,
|
|
prevCursor,
|
|
}
|
|
}
|
|
|
|
export const useUnifiedLogsInfiniteQuery = <TData = UnifiedLogsData>(
|
|
{ projectRef, search }: UnifiedLogsVariables,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: UseCustomInfiniteQueryOptions<
|
|
UnifiedLogsData,
|
|
UnifiedLogsError,
|
|
InfiniteData<TData>,
|
|
readonly unknown[],
|
|
PageParam | null
|
|
> = {}
|
|
) => {
|
|
const useOtel = useFlag('otelUnifiedLogs')
|
|
return useInfiniteQuery({
|
|
queryKey: [...logsKeys.unifiedLogsInfinite(projectRef, search), { otel: useOtel }],
|
|
queryFn: ({ signal, pageParam }) => {
|
|
return getUnifiedLogs({ projectRef, search, pageParam, useOtel }, signal)
|
|
},
|
|
enabled: enabled && typeof projectRef !== 'undefined',
|
|
placeholderData: keepPreviousData,
|
|
getPreviousPageParam: (firstPage) => {
|
|
if (!firstPage.prevCursor) return null
|
|
return { cursor: firstPage.prevCursor, direction: 'prev' } as const
|
|
},
|
|
initialPageParam: null,
|
|
getNextPageParam(lastPage) {
|
|
if (!lastPage.nextCursor || lastPage.data.length === 0) return null
|
|
return { cursor: lastPage.nextCursor, direction: 'next' } as const
|
|
},
|
|
...UNIFIED_LOGS_QUERY_OPTIONS,
|
|
...options,
|
|
})
|
|
}
|