diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts index d83902f5005..68e15428897 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts @@ -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 ?? []) ?? [] diff --git a/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx index 54a8e4f0997..6cd74febbd8 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx @@ -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, }) } diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx index f00b27b0c18..a3ecc3c599e 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerLogsTab.tsx @@ -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(() => - buildDefaultColumnFilters(search) + buildWorkerLogsColumnFilters(search) ) const [columnVisibility, setColumnVisibility] = useState(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) => {
- + {

No logs in the selected time range

- 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.

{ '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 }, + ]) + }) }) diff --git a/apps/studio/components/interfaces/Workers/Workers.utils.ts b/apps/studio/components/interfaces/Workers/Workers.utils.ts index f7515d90b02..a9c7df64c24 100644 --- a/apps/studio/components/interfaces/Workers/Workers.utils.ts +++ b/apps/studio/components/interfaces/Workers/Workers.utils.ts @@ -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, + 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}`], } } diff --git a/apps/studio/data/logs/unified-logs-chart-query.ts b/apps/studio/data/logs/unified-logs-chart-query.ts index 6a9315153d1..b893c6d32a5 100644 --- a/apps/studio/data/logs/unified-logs-chart-query.ts +++ b/apps/studio/data/logs/unified-logs-chart-query.ts @@ -157,13 +157,14 @@ export type UnifiedLogsChartData = Awaited( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { - const useOtel = useFlag('otelUnifiedLogs') + const otelFlag = useFlag('otelUnifiedLogs') + const useOtel = useOtelOverride ?? otelFlag return useQuery({ queryKey: [...logsKeys.unifiedLogsChart(projectRef, search), { otel: useOtel }], queryFn: ({ signal }) => getUnifiedLogsChart({ projectRef, search, useOtel }, signal), diff --git a/apps/studio/data/logs/unified-logs-count-query.ts b/apps/studio/data/logs/unified-logs-count-query.ts index 0a49dfbadd6..0bfaa49a441 100644 --- a/apps/studio/data/logs/unified-logs-count-query.ts +++ b/apps/studio/data/logs/unified-logs-count-query.ts @@ -84,13 +84,14 @@ export type UnifiedLogsCountData = Awaited( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { - const useOtel = useFlag('otelUnifiedLogs') + const otelFlag = useFlag('otelUnifiedLogs') + const useOtel = useOtelOverride ?? otelFlag return useQuery({ queryKey: [...logsKeys.unifiedLogsCount(projectRef, search), { otel: useOtel }], queryFn: ({ signal }) => getUnifiedLogsCount({ projectRef, search, useOtel }, signal), diff --git a/apps/studio/data/logs/unified-logs-infinite-query.ts b/apps/studio/data/logs/unified-logs-infinite-query.ts index cf564c9c81f..4e322e8b645 100644 --- a/apps/studio/data/logs/unified-logs-infinite-query.ts +++ b/apps/studio/data/logs/unified-logs-infinite-query.ts @@ -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 = ( - { projectRef, search }: UnifiedLogsVariables, + { projectRef, search, useOtel: useOtelOverride }: UnifiedLogsVariables, { enabled = true, ...options @@ -152,7 +160,8 @@ export const useUnifiedLogsInfiniteQuery = ( 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 }) => {