mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
fix(studio): make worker logs actually load in the worker tab
Worker rows only exist on the OTEL logs endpoint; the BigQuery query builder has no worker sources. The tab was routed by the otelUnifiedLogs flag, so with the flag off (or not yet loaded) it queried an endpoint that can never return a worker row. Add a useOtel override to the unified logs query hooks and download button, and force it on for the worker tab. Default the tab's time range to the last 24 hours (as master's Activity tab did) instead of the unified logs' last hour, so one-off lifecycle events such as deploys and builds are visible when the tab opens. The seeded range is read by the first fetch and synced into the URL so the picker, the query and the "Open in Logs" link agree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5ev3QGKtXVansc1cz5uzR
This commit is contained in:
8 files changed
+108
-34
No files matched your search
@@ -4,10 +4,12 @@ import { useQueryState } from 'nuqs'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
|
||||
import type { QuerySearchParamsType } from './UnifiedLogs.types'
|
||||
import { useUnifiedLogsChartQuery } from '@/data/logs/unified-logs-chart-query'
|
||||
import { useUnifiedLogsCountQuery } from '@/data/logs/unified-logs-count-query'
|
||||
import { useUnifiedLogsInfiniteQuery } from '@/data/logs/unified-logs-infinite-query'
|
||||
import {
|
||||
useUnifiedLogsInfiniteQuery,
|
||||
type UnifiedLogsVariables,
|
||||
} from '@/data/logs/unified-logs-infinite-query'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
@@ -16,16 +18,10 @@ import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
* timeline chart) plus the flattened + de-duplicated row list. Shared by the
|
||||
* Logs page and the embedded, worker-scoped logs tab.
|
||||
*/
|
||||
export const useUnifiedLogsData = ({
|
||||
projectRef,
|
||||
search,
|
||||
}: {
|
||||
projectRef?: string
|
||||
search: QuerySearchParamsType
|
||||
}) => {
|
||||
const logs = useUnifiedLogsInfiniteQuery({ projectRef, search })
|
||||
const counts = useUnifiedLogsCountQuery({ projectRef, search })
|
||||
const chart = useUnifiedLogsChartQuery({ projectRef, search })
|
||||
export const useUnifiedLogsData = ({ projectRef, search, useOtel }: UnifiedLogsVariables) => {
|
||||
const logs = useUnifiedLogsInfiniteQuery({ projectRef, search, useOtel })
|
||||
const counts = useUnifiedLogsCountQuery({ projectRef, search, useOtel })
|
||||
const chart = useUnifiedLogsChartQuery({ projectRef, search, useOtel })
|
||||
|
||||
const rawFlatData = useMemo(() => {
|
||||
return logs.data?.pages?.flatMap((page) => page.data ?? []) ?? []
|
||||
|
||||
@@ -37,9 +37,11 @@ const DEFAULT_DURATION = '1'
|
||||
|
||||
interface DownloadLogsButtonProps {
|
||||
searchParameters: QuerySearchParamsType
|
||||
/** Forces the OTEL endpoint for log types that only exist there (see UnifiedLogsVariables). */
|
||||
useOtel?: boolean
|
||||
}
|
||||
|
||||
export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps) => {
|
||||
export const DownloadLogsButton = ({ searchParameters, useOtel }: DownloadLogsButtonProps) => {
|
||||
const { ref } = useParams()
|
||||
const pathname = usePathname()
|
||||
const isLogs = pathname?.includes?.('/logs') ?? false
|
||||
@@ -88,6 +90,7 @@ export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps
|
||||
search: searchParameters,
|
||||
limit: Number(numRows),
|
||||
hoursAgo: !hasSpecificTimeRange ? Number(numHours) : undefined,
|
||||
useOtel,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useMemo, useState } from 'react'
|
||||
import { Button, cn, ResizablePanel, ResizablePanelGroup } from 'ui'
|
||||
|
||||
import { WorkerCommandLine } from '../WorkerCommandLine'
|
||||
import { buildWorkerLogsSearchParameters } from '../Workers.utils'
|
||||
import { buildWorkerLogsColumnFilters, buildWorkerLogsSearchParameters } from '../Workers.utils'
|
||||
import { WorkerLogStreamToggle } from './WorkerLogStreamToggle'
|
||||
import {
|
||||
generateDynamicColumns,
|
||||
@@ -29,10 +29,7 @@ import { ServiceFlowPanel } from '@/components/interfaces/UnifiedLogs/ServiceFlo
|
||||
import { CHART_CONFIG } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.chart-config'
|
||||
import { SEARCH_PARAMS_PARSER } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.constants'
|
||||
import { filterFields as defaultFilterFields } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.fields'
|
||||
import {
|
||||
buildDefaultColumnFilters,
|
||||
buildFilterSearchUpdate,
|
||||
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
|
||||
import { buildFilterSearchUpdate } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
|
||||
import {
|
||||
useFilterSearchSync,
|
||||
useLiveMode,
|
||||
@@ -70,12 +67,16 @@ const DEFAULT_COLUMN_VISIBILITY: VisibilityState = { select: false }
|
||||
|
||||
const serializeLogsSearch = createSerializer(SEARCH_PARAMS_PARSER)
|
||||
|
||||
// Worker logs only exist on the OTEL endpoint; the BigQuery path has no worker
|
||||
// sources, so the `otelUnifiedLogs` flag must not route this tab there.
|
||||
const USE_OTEL = true
|
||||
|
||||
export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
|
||||
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() =>
|
||||
buildDefaultColumnFilters(search)
|
||||
buildWorkerLogsColumnFilters(search)
|
||||
)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useState<VisibilityState>(DEFAULT_COLUMN_VISIBILITY)
|
||||
@@ -87,8 +88,8 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
|
||||
|
||||
// Always scoped to this worker, whatever the URL says — see buildWorkerLogsSearchParameters.
|
||||
const searchParameters = useMemo(
|
||||
() => buildWorkerLogsSearchParameters(search, workerName),
|
||||
[search, workerName]
|
||||
() => buildWorkerLogsSearchParameters(search, workerName, columnFilters),
|
||||
[search, workerName, columnFilters]
|
||||
)
|
||||
|
||||
const {
|
||||
@@ -109,7 +110,7 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
|
||||
isFetchingChart,
|
||||
refetchAll,
|
||||
isRefetching,
|
||||
} = useUnifiedLogsData({ projectRef, search: searchParameters })
|
||||
} = useUnifiedLogsData({ projectRef, search: searchParameters, useOtel: USE_OTEL })
|
||||
|
||||
const liveMode = useLiveMode(flatData)
|
||||
|
||||
@@ -201,7 +202,7 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
|
||||
<div className="ml-auto flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||
<WorkerLogStreamToggle />
|
||||
<RefreshButton isLoading={isRefetching} onRefresh={refetchAll} />
|
||||
<DownloadLogsButton searchParameters={searchParameters} />
|
||||
<DownloadLogsButton searchParameters={searchParameters} useOtel={USE_OTEL} />
|
||||
<LiveButton
|
||||
fetchPreviousPage={fetchPreviousPage}
|
||||
searchParamsParser={SEARCH_PARAMS_PARSER}
|
||||
@@ -264,7 +265,8 @@ export const WorkerLogsTab = ({ workerName }: WorkerLogsTabProps) => {
|
||||
<div className="mx-auto max-w-md space-y-3 py-16 text-center">
|
||||
<p className="text-sm text-foreground">No logs in the selected time range</p>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Follow them from the Supabase CLI while you wait for traffic.
|
||||
Widen the time range to find earlier deploys and builds, or follow new logs
|
||||
from the Supabase CLI.
|
||||
</p>
|
||||
<div className="pt-1 text-left">
|
||||
<WorkerCommandLine
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { getWorkerStateMeta, WORKER_NAME_WORDS, workerUrl } from './Workers.constants'
|
||||
import type { Worker } from './Workers.types'
|
||||
import {
|
||||
buildWorkerLogsColumnFilters,
|
||||
buildWorkerLogsSearchParameters,
|
||||
filterWorkers,
|
||||
formatResources,
|
||||
@@ -252,4 +253,36 @@ describe('buildWorkerLogsSearchParameters', () => {
|
||||
'worker:eq:embed',
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to the seeded date column filter until the URL carries a range', () => {
|
||||
const range = [new Date('2026-09-08T10:00:00Z'), new Date('2026-09-09T10:00:00Z')]
|
||||
const seeded = buildWorkerLogsSearchParameters(search, 'embed', [{ id: 'date', value: range }])
|
||||
expect(seeded.date).toEqual(range)
|
||||
|
||||
const urlRange = [new Date('2026-09-09T09:00:00Z'), new Date('2026-09-09T10:00:00Z')]
|
||||
const fromUrl = buildWorkerLogsSearchParameters({ ...search, date: urlRange }, 'embed', [
|
||||
{ id: 'date', value: range },
|
||||
])
|
||||
expect(fromUrl.date).toEqual(urlRange)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildWorkerLogsColumnFilters', () => {
|
||||
const now = new Date('2026-09-09T10:00:00Z')
|
||||
|
||||
it('defaults the time range to the last 24 hours when the URL has none', () => {
|
||||
expect(buildWorkerLogsColumnFilters({ filter: null, date: null }, now)).toEqual([
|
||||
{ id: 'date', value: [new Date('2026-09-08T10:00:00Z'), now] },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps a range and filters that came from the URL', () => {
|
||||
const range = [new Date('2026-09-01T00:00:00Z'), new Date('2026-09-02T00:00:00Z')]
|
||||
expect(
|
||||
buildWorkerLogsColumnFilters({ filter: ['event_message:ilike:boom'], date: range }, now)
|
||||
).toEqual([
|
||||
{ id: 'event_message', value: { operator: '~~*', values: ['boom'] } },
|
||||
{ id: 'date', value: range },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { ColumnFiltersState } from '@tanstack/react-table'
|
||||
|
||||
import { RUNTIMES, WORKER_NAME_WORDS, type RuntimeMeta } from './Workers.constants'
|
||||
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
|
||||
import { parseLogsFilterUrlParams } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
|
||||
import {
|
||||
buildDefaultColumnFilters,
|
||||
parseLogsFilterUrlParams,
|
||||
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.filters'
|
||||
import type {
|
||||
QuerySearchParamsType,
|
||||
SearchParamsType,
|
||||
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.types'
|
||||
import { toQuerySearchParameters } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
|
||||
import { isArrayOfDates } from '@/components/ui/DataTable/DataTable.utils'
|
||||
import {
|
||||
WORKER_LOG_SOURCES,
|
||||
WORKER_LOG_STREAM_SEARCH_PARAM,
|
||||
@@ -96,21 +102,44 @@ export const getVisibleWorkerLogStreams = (
|
||||
): WorkerLogStream[] =>
|
||||
WORKER_LOG_STREAMS.filter((stream) => search[WORKER_LOG_STREAM_SEARCH_PARAM[stream]] !== false)
|
||||
|
||||
// Unified logs default to the last hour, which is too narrow for a worker: its
|
||||
// lifecycle events (deploy, build) happen once, so open on the last day instead.
|
||||
export const DEFAULT_WORKER_LOGS_WINDOW_HOURS = 24
|
||||
|
||||
// Seeds the table's column filters from the URL, defaulting the time range when
|
||||
// the URL doesn't carry one. The seeded range is synced back to the URL by the
|
||||
// regular filter sync, so the picker, the query and the link all agree.
|
||||
export const buildWorkerLogsColumnFilters = (
|
||||
search: Pick<SearchParamsType, 'filter' | 'date'>,
|
||||
now: Date = new Date()
|
||||
): ColumnFiltersState => {
|
||||
const filters = buildDefaultColumnFilters(search)
|
||||
if (filters.some((filter) => filter.id === 'date')) return filters
|
||||
const from = new Date(now.getTime() - DEFAULT_WORKER_LOGS_WINDOW_HOURS * 60 * 60 * 1000)
|
||||
return [...filters, { id: 'date', value: [from, now] }]
|
||||
}
|
||||
|
||||
// The worker logs tab is always scoped to one worker: whatever the URL says, the
|
||||
// query only ever sees the `workers` log type and this worker's rows. Any
|
||||
// `log_type` / `worker` filters coming from the URL are dropped so they can't
|
||||
// widen (or duplicate) that scope.
|
||||
// widen (or duplicate) that scope. Until the seeded default time range has been
|
||||
// synced into the URL, it is read from the column filters so the very first
|
||||
// fetch already uses it.
|
||||
export const buildWorkerLogsSearchParameters = (
|
||||
search: SearchParamsType,
|
||||
workerName: string
|
||||
workerName: string,
|
||||
columnFilters: ColumnFiltersState = []
|
||||
): QuerySearchParamsType => {
|
||||
const parameters = toQuerySearchParameters(search)
|
||||
const otherFilters = (parameters.filter ?? []).filter((raw) => {
|
||||
const parsed = parseLogsFilterUrlParams([raw])[0]
|
||||
return parsed !== undefined && parsed.column !== 'log_type' && parsed.column !== 'worker'
|
||||
})
|
||||
const dateFilter = columnFilters.find((filter) => filter.id === 'date')?.value
|
||||
const date = parameters.date ?? (isArrayOfDates(dateFilter) ? dateFilter : undefined)
|
||||
return {
|
||||
...parameters,
|
||||
...(date ? { date } : {}),
|
||||
filter: [...otherFilters, 'log_type:eq:workers', `worker:eq:${workerName}`],
|
||||
}
|
||||
}
|
||||
@@ -157,13 +157,14 @@ export type UnifiedLogsChartData = Awaited<ReturnType<typeof getUnifiedLogsChart
|
||||
export type UnifiedLogsChartError = ResponseError
|
||||
|
||||
export const useUnifiedLogsChartQuery = <TData = UnifiedLogsChartData>(
|
||||
{ projectRef, search }: UnifiedLogsVariables,
|
||||
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<UnifiedLogsChartData, UnifiedLogsChartError, TData> = {}
|
||||
) => {
|
||||
const useOtel = useFlag('otelUnifiedLogs')
|
||||
const otelFlag = useFlag('otelUnifiedLogs')
|
||||
const useOtel = useOtelOverride ?? otelFlag
|
||||
return useQuery<UnifiedLogsChartData, UnifiedLogsChartError, TData>({
|
||||
queryKey: [...logsKeys.unifiedLogsChart(projectRef, search), { otel: useOtel }],
|
||||
queryFn: ({ signal }) => getUnifiedLogsChart({ projectRef, search, useOtel }, signal),
|
||||
|
||||
@@ -84,13 +84,14 @@ export type UnifiedLogsCountData = Awaited<ReturnType<typeof getUnifiedLogsCount
|
||||
export type UnifiedLogsCountError = ResponseError
|
||||
|
||||
export const useUnifiedLogsCountQuery = <TData = UnifiedLogsCountData>(
|
||||
{ projectRef, search }: UnifiedLogsVariables,
|
||||
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<UnifiedLogsCountData, UnifiedLogsCountError, TData> = {}
|
||||
) => {
|
||||
const useOtel = useFlag('otelUnifiedLogs')
|
||||
const otelFlag = useFlag('otelUnifiedLogs')
|
||||
const useOtel = useOtelOverride ?? otelFlag
|
||||
return useQuery<UnifiedLogsCountData, UnifiedLogsCountError, TData>({
|
||||
queryKey: [...logsKeys.unifiedLogsCount(projectRef, search), { otel: useOtel }],
|
||||
queryFn: ({ signal }) => getUnifiedLogsCount({ projectRef, search, useOtel }, signal),
|
||||
|
||||
@@ -27,7 +27,15 @@ export const UNIFIED_LOGS_QUERY_OPTIONS = {
|
||||
|
||||
export type UnifiedLogsData = any
|
||||
export type UnifiedLogsError = ResponseError
|
||||
export type UnifiedLogsVariables = { projectRef?: string; search: QuerySearchParamsType }
|
||||
export type UnifiedLogsVariables = {
|
||||
projectRef?: string
|
||||
search: QuerySearchParamsType
|
||||
/**
|
||||
* Overrides the `otelUnifiedLogs` flag. Some log types (e.g. workers) only
|
||||
* exist on the OTEL endpoint, so their surfaces pass `true` regardless of the flag.
|
||||
*/
|
||||
useOtel?: boolean
|
||||
}
|
||||
|
||||
export const getUnifiedLogsISOStartEnd = (
|
||||
search: QuerySearchParamsType,
|
||||
@@ -140,7 +148,7 @@ export async function getUnifiedLogs(
|
||||
}
|
||||
|
||||
export const useUnifiedLogsInfiniteQuery = <TData = UnifiedLogsData>(
|
||||
{ projectRef, search }: UnifiedLogsVariables,
|
||||
{ projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
@@ -152,7 +160,8 @@ export const useUnifiedLogsInfiniteQuery = <TData = UnifiedLogsData>(
|
||||
PageParam | null
|
||||
> = {}
|
||||
) => {
|
||||
const useOtel = useFlag('otelUnifiedLogs')
|
||||
const otelFlag = useFlag('otelUnifiedLogs')
|
||||
const useOtel = useOtelOverride ?? otelFlag
|
||||
return useInfiniteQuery({
|
||||
queryKey: [...logsKeys.unifiedLogsInfinite(projectRef, search), { otel: useOtel }],
|
||||
queryFn: ({ signal, pageParam }) => {
|
||||
|
||||
Reference in new issue
Block a user