Files
supabase/apps/studio/components/interfaces/UnifiedLogs/ServiceFlowPanel.tsx
T
Jordi EnricandClaude Sonnet 4.6 9c572a56fd feat(logs): add realtime, supavisor, and pgbouncer to unified logs (#46786)
## Problem

The unified logs view was missing three log sources that the backend
already returns: Realtime (`realtime_logs`), Supavisor
(`supavisor_logs`), and PgBouncer (`pgbouncer_logs`). Users had no way
to filter or view logs from these services in the unified view.

## Fix

- Added `realtime`, `supavisor`, and `pgbouncer` to `LOG_TYPE_PREDICATE`
and `LOG_TYPE_EXPR` in the OTEL query builder so rows from these sources
are matched and labeled correctly.
- Added the three types to `LOG_TYPES` so filter chips appear in the UI.
- Added icons (`Realtime` from the icons package, `Cable` from
lucide-react for both pooler types) in `LogTypeIcon.tsx`.
- Added display labels in `formatServiceTypeForDisplay` (`Realtime`,
`Supavisor`, `PgBouncer`).
- Fixed a pre-existing unsafe cast in `ServiceFlowPanel.tsx` where any
log type not in the service flow allow-list (like the new types) would
produce a truthy `serviceFlowType` and trigger a runtime error on row
click. The fix checks against `SERVICE_FLOW_TYPES` before casting.

Note: `pg_cron_logs` is not included because the backend otel query does
not yet return that source.

## How to test

1. Open unified logs for a project that has Realtime activity.
2. In the log type filter, confirm `realtime`, `supavisor`, and
`pgbouncer` chips are visible.
3. Toggle on `realtime` and confirm Realtime logs appear with the
correct icon.
4. Click a realtime log row and confirm the detail panel opens on the
raw JSON tab without a runtime error.
5. Toggle on `supavisor` or `pgbouncer` and confirm pooler logs appear
if the project has connection pooler activity.

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

* **New Features**
* Added support for three new log types — Realtime, Supavisor, and
PgBouncer — in Unified Logs with display labels, icons, and
filtering/viewing support.

* **Bug Fixes / UI Behavior**
* Service Flow “Overview” tab now only shows when the service type is
recognized, preventing irrelevant overview content for unsupported log
sources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:46:37 +02:00

268 lines
9.6 KiB
TypeScript

import { useParams } from 'common'
import { Check, Clock, Copy } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import {
Button,
copyToClipboard,
ResizableHandle,
ResizablePanel,
Skeleton,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { PostgresFlowDetail } from './ServiceFlow/components/PostgresFlowDetail'
import {
MemoizedEdgeFunctionBlock,
MemoizedGoTrueBlock,
MemoizedNetworkBlock,
MemoizedPostgresBlock,
MemoizedPostgRESTBlock,
MemoizedStorageBlock,
} from './ServiceFlow/components/ServiceBlocks'
import { ServiceFlowPanelControls } from './ServiceFlow/components/ServiceFlowPanelControls'
import { DetailSectionHeader } from './ServiceFlow/components/shared/DetailSection'
import { ColumnSchema } from './UnifiedLogs.schema'
import { QuerySearchParamsType } from './UnifiedLogs.types'
import { getRowTimestampMs } from './UnifiedLogs.utils'
import { useDataTable } from '@/components/ui/DataTable/providers/DataTableProvider'
import {
SERVICE_FLOW_TYPES,
ServiceFlowType,
useUnifiedLogInspectionQuery,
} from '@/data/logs/unified-log-inspection-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
interface ServiceFlowPanelProps {
dock: 'bottom' | 'right'
setDock: (value: 'bottom' | 'right') => void
selectedRow?: ColumnSchema
selectedRowKey: string
searchParameters: QuerySearchParamsType
}
export function ServiceFlowPanel({
dock,
setDock,
selectedRow,
selectedRowKey,
searchParameters,
}: ServiceFlowPanelProps) {
const { table, filterFields } = useDataTable()
const { ref: projectRef } = useParams()
const [activeTab, setActiveTab] = useState('overview')
const [jsonCopied, setJsonCopied] = useState(false)
const overviewScrollRef = useRef<HTMLDivElement>(null)
const jsonScrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
overviewScrollRef.current?.scrollTo({ top: 0 })
jsonScrollRef.current?.scrollTo({ top: 0 })
}, [selectedRowKey])
const logType = selectedRow?.log_type
const normalizedLogType = logType === 'edge function' ? 'edge-function' : logType
const serviceFlowType: ServiceFlowType | undefined = SERVICE_FLOW_TYPES.includes(
normalizedLogType as ServiceFlowType
)
? (normalizedLogType as ServiceFlowType)
: undefined
const shouldShowServiceFlow = !!serviceFlowType
useEffect(() => {
if (!shouldShowServiceFlow && activeTab === 'overview') {
setActiveTab('raw-json')
}
}, [shouldShowServiceFlow, activeTab])
const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
// Query the logs API directly
const {
data: serviceFlowData,
isPending: isLoading,
error,
} = useUnifiedLogInspectionQuery(
{
projectRef: projectRef,
logId: selectedRow?.id,
type: serviceFlowType,
search: searchParameters,
},
{ enabled: Boolean(selectedRow?.id) && Boolean(serviceFlowType) }
)
if (!selectedRowKey || !selectedRow) return null
const timestampMs = getRowTimestampMs(selectedRow)
const formattedTime = timestampMs ? new Date(timestampMs).toLocaleString() : null
// Prepare JSON data for Raw JSON tab
const jsonData =
shouldShowServiceFlow && serviceFlowData?.result?.[0] ? serviceFlowData.result[0] : selectedRow
const formattedJsonData =
!logsMetadata && 'raw_log_data' in jsonData && 'metadata' in jsonData.raw_log_data
? {
...jsonData,
raw_log_data: { ...jsonData.raw_log_data, metadata: undefined },
}
: jsonData
return (
<>
<ResizableHandle withHandle />
<ResizablePanel
id="log-sidepanel"
defaultSize={400}
minSize={dock === 'bottom' ? 300 : 400}
className="bg-dash-sidebar"
>
<div className="flex h-full flex-col overflow-hidden">
<Tabs
defaultValue={shouldShowServiceFlow ? 'overview' : 'raw-json'}
value={activeTab}
onValueChange={setActiveTab}
className="flex h-full w-full flex-col"
>
<div className="flex items-center justify-between px-4 border-b border-border">
<TabsList className="flex h-auto gap-x-4 rounded-none border-none!">
{shouldShowServiceFlow && (
<TabsTrigger
value="overview"
className="border-b py-3 font-mono text-xs uppercase"
>
Overview
</TabsTrigger>
)}
<TabsTrigger value="raw-json" className="border-b py-3 font-mono text-xs uppercase">
Raw JSON
</TabsTrigger>
</TabsList>
<ServiceFlowPanelControls dock={dock} setDock={setDock} />
</div>
{shouldShowServiceFlow && (
<TabsContent
ref={overviewScrollRef}
value="overview"
className="mt-0 grow overflow-auto py-2"
>
{error ? (
<div className="py-8 text-center text-destructive">Error: {error.toString()}</div>
) : serviceFlowType === 'postgres' ? (
<PostgresFlowDetail
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
) : (
<div>
<DetailSectionHeader
title="Request started"
icon={Clock}
summary={formattedTime ?? undefined}
/>
<MemoizedNetworkBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
{serviceFlowType === 'auth' ? (
<MemoizedGoTrueBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
) : serviceFlowType === 'edge-function' ? (
<MemoizedEdgeFunctionBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
) : serviceFlowType === 'storage' ? (
<MemoizedStorageBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
) : (
<>
<MemoizedPostgRESTBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
<MemoizedPostgresBlock
data={selectedRow}
enrichedData={serviceFlowData?.result?.[0]}
isLoading={isLoading}
filterFields={filterFields}
table={table}
/>
</>
)}
</div>
)}
</TabsContent>
)}
<TabsContent
ref={jsonScrollRef}
value="raw-json"
className="mt-0 grow overflow-auto bg-surface-100/50"
>
{isLoading && shouldShowServiceFlow && (
<div className="flex items-center gap-3 border-b border-border bg-surface-100 p-3 text-foreground-light">
<Skeleton className="h-4 w-4 animate-pulse rounded-full" />
<span className="text-sm">Enriching log...</span>
</div>
)}
<div className="sticky top-2 z-10 flex justify-end px-2 -mb-9 pointer-events-none">
<Button
size="tiny"
type="default"
className="pointer-events-auto px-1.5"
icon={jsonCopied ? <Check size={12} /> : <Copy size={12} />}
onClick={() => {
copyToClipboard(JSON.stringify(formattedJsonData, null, 2))
setJsonCopied(true)
setTimeout(() => setJsonCopied(false), 1000)
}}
>
{jsonCopied ? 'Copied' : ''}
</Button>
</div>
<CodeBlock
language="json"
hideCopy
wrapperClassName="!overflow-visible bg-surface-100/50 [&_pre]:!bg-surface-100/50"
className="rounded-none border-none [&_code]:!leading-tight [&_pre]:!leading-tight"
>
{JSON.stringify(formattedJsonData, null, 2)}
</CodeBlock>
</TabsContent>
</Tabs>
</div>
</ResizablePanel>
</>
)
}