mirror of
https://github.com/supabase/supabase.git
synced 2026-06-14 23:25:16 +08:00
## Problem High availability (Multigres) projects don't expose Multigres service logs in the Studio logs UI, so users on HA projects have no entry point to inspect them. ## Fix Add a `Multigres` logs collection, gated behind the `multigresLogs` ConfigCat flag **and** the project's `high_availability` flag (`useShowMultigresLogs`): - New `Multigres` entry in the logs sidebar `Collections`, linking to a new `multigres-logs` page that queries the `multigres_logs` table. - Wire `multigres_logs` through the logs constants, types, table SQL, query type, and service labels. - Row formatting: parse the JSON `event_message` and render `level` through `SeverityFormatter` and `msg` through `TextFormatter`, matching the other service collections (instead of dumping raw JSON). - `WARN` severity is now styled like `WARNING` (amber), since Multigres emits `level: WARN`. - Log detail drawer: parse the JSON `event_message` and spread its keys onto the log so each field (level, msg, query, error, connection_id, etc.) renders as its own collapsible row. - Single-log query omits the `metadata` column for `multigres_logs` (the table has no such column), fixing an `INVALID_ARGUMENT` error when opening the detail drawer. - Event chart: parse the level out of `event_message` via `JSON_VALUE` so error/warning bars are counted (the table has no top-level level column). - Add the `multigres_logs` source schema to the Field Reference drawer, same gating. ## Why a feature flag `high_availability` is an existing product feature that predates Multigres, so existing HA projects could otherwise see a broken collection querying a `multigres_logs` table they don't have. Requiring the `multigresLogs` flag ships the feature dark and decouples rollout from HA status. The flag must be created in ConfigCat before enabling; until then `useFlag` returns false and the feature stays hidden. ## How to test - Enable the `multigresLogs` flag (or override locally) and open a project where `high_availability` is `true`. - Navigate to `Logs`. Confirm a `Multigres` entry appears under `Collections` (after `Replication`). - Open it: the page loads at `/project/<ref>/logs/multigres-logs` and queries `multigres_logs`. - Confirm rows show a colored severity pill (including amber `WARN`) and a readable message rather than raw JSON. - Confirm the chart counts error/warning bars correctly. - Click a row: the detail drawer shows each parsed field as its own row, with no error. - Open the Field Reference drawer and confirm `Multigres` is listed as a source. - With the flag off, or on a non-HA project, confirm the collection and Field Reference source are both hidden. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Multigres added as a dedicated log source with its own Logs page, sidebar entry, and query type. * Log list and preview now parse Multigres payloads to surface timestamp, severity, and formatted message. * Multigres integrated into charting, prompt labels, and field-reference UI (hidden unless enabled). * New hook controls showing Multigres UI only when feature flag + HA project condition are met. * **Bug Fixes** * Severity rendering treats "WARN" the same as "WARNING". * **Tests** * Unit tests added for Multigres parsing and the show-Multigres hook. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
import { Check, Copy, MousePointerClick, X } from 'lucide-react'
|
|
import { useEffect, useState } from 'react'
|
|
import {
|
|
Button,
|
|
cn,
|
|
copyToClipboard,
|
|
Tabs_Shadcn_,
|
|
TabsContent_Shadcn_,
|
|
TabsList_Shadcn_,
|
|
TabsTrigger_Shadcn_,
|
|
} from 'ui'
|
|
import { CodeBlock } from 'ui-patterns/CodeBlock'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import type { LogData, PreviewLogData, QueryType } from './Logs.types'
|
|
import { apiKey, role as extractRole, jwtAPIKey, parseMultigresEventMessage } from './Logs.utils'
|
|
import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
|
|
export interface LogSelectionProps {
|
|
log?: LogData
|
|
onClose: () => void
|
|
queryType?: QueryType
|
|
projectRef: string
|
|
isLoading: boolean
|
|
error?: string | object
|
|
}
|
|
|
|
const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => {
|
|
const [showCopied, setShowCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!showCopied) return
|
|
const timer = setTimeout(() => setShowCopied(false), 2000)
|
|
return () => clearTimeout(timer)
|
|
}, [showCopied])
|
|
|
|
const LogDetails = () => {
|
|
if (error) return <LogErrorState error={error} />
|
|
if (!log) return <LogDetailEmptyState />
|
|
|
|
switch (queryType) {
|
|
case 'api':
|
|
const status = log?.metadata?.[0]?.response?.[0]?.status_code
|
|
const method = log?.metadata?.[0]?.request?.[0]?.method
|
|
const path = log?.metadata?.[0]?.request?.[0]?.path
|
|
const search = log?.metadata?.[0]?.request?.[0]?.search
|
|
const user_agent = log?.metadata?.[0]?.request?.[0]?.headers[0].user_agent
|
|
const error_code = log?.metadata?.[0]?.response?.[0]?.headers?.[0]?.x_sb_error_code
|
|
const apikey = jwtAPIKey(log?.metadata) ?? apiKey(log?.metadata)
|
|
const role = extractRole(log?.metadata)
|
|
|
|
const { id, metadata, timestamp, event_message, ...rest } = log
|
|
|
|
const apiLog = {
|
|
id,
|
|
status,
|
|
method,
|
|
path,
|
|
search,
|
|
user_agent,
|
|
timestamp,
|
|
event_message,
|
|
metadata,
|
|
...(apikey ? { apikey } : null),
|
|
...(error_code ? { error_code } : null),
|
|
...(role ? { role } : null),
|
|
...rest,
|
|
}
|
|
|
|
return <DefaultPreviewSelectionRenderer log={apiLog} />
|
|
|
|
case 'multigres': {
|
|
const parsedMultigresMessage = parseMultigresEventMessage(log.event_message)
|
|
// Spread the log last so its canonical fields (id, timestamp, event_message)
|
|
// always win over any same-named keys inside the parsed event_message.
|
|
const multigresLog = (
|
|
parsedMultigresMessage ? { ...parsedMultigresMessage, ...log } : log
|
|
) as PreviewLogData
|
|
return <DefaultPreviewSelectionRenderer log={multigresLog} />
|
|
}
|
|
|
|
case 'database':
|
|
const hint = log?.metadata?.[0]?.parsed?.[0]?.hint
|
|
const detail = log?.metadata?.[0]?.parsed?.[0]?.detail
|
|
const query = log?.metadata?.[0]?.parsed?.[0]?.query
|
|
const postgresLog = {
|
|
...(hint && { hint }),
|
|
...(detail && { detail }),
|
|
...(query && { query }),
|
|
...log,
|
|
}
|
|
return <DefaultPreviewSelectionRenderer log={postgresLog} />
|
|
default:
|
|
return <DefaultPreviewSelectionRenderer log={log} />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative flex h-full grow flex-col overflow-y-scroll bg-surface-100 border-t">
|
|
<div className="relative grow flex flex-col h-full">
|
|
<Tabs_Shadcn_ defaultValue="details" className="flex flex-col h-full">
|
|
<TabsList_Shadcn_ className="px-2 pt-2 relative">
|
|
<TabsTrigger_Shadcn_ className="px-3" value="details">
|
|
Details
|
|
</TabsTrigger_Shadcn_>
|
|
<TabsTrigger_Shadcn_ disabled={!log} className="px-3" value="raw">
|
|
Raw
|
|
</TabsTrigger_Shadcn_>
|
|
|
|
<div className="*:px-1.5 *:text-foreground-lighter ml-auto flex gap-1 absolute right-2 top-2">
|
|
<ButtonTooltip
|
|
disabled={!log || isLoading}
|
|
type="text"
|
|
tooltip={{
|
|
content: {
|
|
side: 'left',
|
|
text: isLoading ? 'Loading log...' : 'Copy as JSON',
|
|
},
|
|
}}
|
|
icon={showCopied ? <Check /> : <Copy />}
|
|
onClick={() => {
|
|
setShowCopied(true)
|
|
copyToClipboard(JSON.stringify(log, null, 2))
|
|
}}
|
|
/>
|
|
|
|
<Button type="text" onClick={onClose}>
|
|
<X size={14} strokeWidth={2} />
|
|
</Button>
|
|
</div>
|
|
</TabsList_Shadcn_>
|
|
<div className="flex-1 h-full">
|
|
{isLoading ? (
|
|
<div className="p-4">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<TabsContent_Shadcn_ className="space-y-6 h-full" value="details">
|
|
<LogDetails />
|
|
</TabsContent_Shadcn_>
|
|
<TabsContent_Shadcn_ value="raw">
|
|
<CodeBlock
|
|
hideLineNumbers
|
|
language="json"
|
|
className="prose w-full pt-0 max-w-full border-none"
|
|
>
|
|
{JSON.stringify(log, null, 2)}
|
|
</CodeBlock>
|
|
</TabsContent_Shadcn_>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Tabs_Shadcn_>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default LogSelection
|
|
|
|
function LogDetailEmptyState({
|
|
title = 'Select an Event',
|
|
message = 'Select an Event to view the complete JSON payload',
|
|
}: {
|
|
title?: string
|
|
message?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex h-full w-full flex-col items-center justify-center gap-2 overflow-y-scroll text-center transition-all px-4'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'flex w-full max-w-sm flex-col items-center justify-center gap-6 text-center transition-all delay-300 duration-500'
|
|
)}
|
|
>
|
|
<div className="relative flex h-4 w-32 items-center rounded-sm border border-control px-2">
|
|
<div className="h-0.5 w-2/3 rounded-full bg-surface-300"></div>
|
|
<div className="absolute right-1 -bottom-4">
|
|
<MousePointerClick size="24" strokeWidth={1} />
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-1">
|
|
<h3 className="text-sm text-foreground">{title}</h3>
|
|
<p className="text-xs text-foreground-lighter">{message}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LogErrorState({ error }: { error?: string | object }) {
|
|
return <pre>{JSON.stringify(error, null, 2)}</pre>
|
|
}
|