mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
## Problem Moving the Logs Explorer to ClickHouse means users' saved BigQuery queries no longer run. <img width="2430" height="1010" alt="CleanShot 2026-06-29 at 11 36 04@2x" src="https://github.com/user-attachments/assets/ae0ab155-7d3d-4ae9-81c3-22bf3a88cf8c" /> ## Fix Rewrite the query with AI instead of a SQL transpiler. AI handles the long tail of nested fields and dialect differences far better than a rule-based rewriter, and it needs no extra runtime dependency. - `rewriteLogsSqlWithAI` posts the current query to `/api/ai/code/complete` with `dialect: 'clickhouse'`. The endpoint skips the Postgres schema and best-practices for that dialect and uses logs-specific instructions and model so the output is ClickHouse logs SQL (FROM `logs` + `source` filter, no `unnest` joins, nested fields read from `log_attributes['...']`). - The query's `source` is detected and its real `log_attributes` keys are fetched and passed to the model, so it maps to exact paths instead of guessing. - The rewrite runs in the background and is proposed as a side-by-side accept/discard diff in the editor. The AI Assistant panel is not opened. - Entry points: a banner shown only for legacy-looking queries (dismissal persisted), and a "Fix Query" button next to Field Reference. - The Field Reference drawers discover `log_attributes` keys from real data so the listed fields match what the source actually emits. ## Dependencies Built on top of #47265 (Logs Explorer -> OTEL endpoint) — that is the base branch of this PR. Merge #47265 first. Behind `otelLegacyLogs` (off by default). Part of DEBUG-145 (split from #47087). ## How to test - Open the Logs Explorer with a BigQuery logs query (the templates have some), click "Fix Query", and confirm the diff shows valid ClickHouse SQL. Accept it and confirm the applied query runs. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an OTEL legacy logs workflow (behind a feature flag) with an interactive banner and a “Fix Query” ClickHouse rewrite action, including an accept/discard diff review overlay. * Introduced OTEL-aware field reference rendering with dynamic discovery of `log_attributes` keys and updated OTEL source insertion behavior. * Enabled dialect-aware SQL completion for ClickHouse logs, using logs-specific instructions and output constraints. * **Bug Fixes** * Improved rewrite flow validation and handling, including log source detection and cleanup of AI-generated SQL formatting. * **Tests** * Added Vitest coverage for rewrite prompt generation, detection/classification utilities, SQL fence stripping, OTEL field mapping, and OTEL log attribute key discovery. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
247 lines
8.4 KiB
TypeScript
247 lines
8.4 KiB
TypeScript
import { useFlag, useParams } from 'common'
|
|
import { BookOpen, Check, ChevronsUpDown, Copy, ExternalLink, List, X } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useState } from 'react'
|
|
import { logConstants } from 'shared-data'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
copyToClipboard,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
SidePanel,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
|
|
import { DocsButton } from '../DocsButton'
|
|
import { LOGS_EXPLORER_DOCS_URL } from '@/components/interfaces/Settings/Logs/Logs.constants'
|
|
import {
|
|
otelFieldsFromKeys,
|
|
toOtelFieldSchemas,
|
|
} from '@/components/interfaces/Settings/Logs/Logs.fieldReference'
|
|
import Table from '@/components/to-be-cleaned/Table'
|
|
import { useOtelLogKeysQuery } from '@/data/logs/otel-log-keys-query'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
export interface LogsExplorerHeaderProps {
|
|
subtitle?: string
|
|
}
|
|
|
|
const LogsExplorerHeader = ({ subtitle }: LogsExplorerHeaderProps) => {
|
|
const [showReference, setShowReference] = useState(false)
|
|
const [open, setOpen] = useState(false)
|
|
const useOtel = useFlag('otelLegacyLogs')
|
|
const schemas = useOtel ? toOtelFieldSchemas(logConstants.schemas) : logConstants.schemas
|
|
const [selectedRef, setSelectedRef] = useState(schemas[0]?.reference)
|
|
const selectedSchema = schemas.find((s) => s.reference === selectedRef) ?? schemas[0]
|
|
|
|
const { ref: projectRef } = useParams()
|
|
const { data: discoveredKeys, isPending: isLoadingKeys } = useOtelLogKeysQuery(
|
|
{ projectRef, source: selectedRef },
|
|
{ enabled: useOtel && showReference }
|
|
)
|
|
|
|
return (
|
|
<div className="flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all pb-6 justify-between">
|
|
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
|
<div className="flex flex-row items-center gap-3">
|
|
<div className="flex h-6 w-6 items-center justify-center rounded-sm border border-brand-600 bg-brand-300 text-brand">
|
|
<List size={14} strokeWidth={3} />
|
|
</div>
|
|
|
|
<h1>Logs Explorer</h1>
|
|
</div>
|
|
{subtitle && <span className="text-2xl text-foreground-light">{subtitle}</span>}
|
|
</div>
|
|
<div className="flex flex-row gap-2">
|
|
<DocsButton href={LOGS_EXPLORER_DOCS_URL} />
|
|
|
|
<SidePanel
|
|
size="large"
|
|
header={
|
|
<div className="flex flex-row justify-between items-center">
|
|
<h3>Field Reference</h3>
|
|
<Button
|
|
variant="text"
|
|
className="px-1"
|
|
onClick={() => setShowReference(false)}
|
|
icon={<X size={18} strokeWidth={1.5} />}
|
|
/>
|
|
</div>
|
|
}
|
|
visible={showReference}
|
|
cancelText="Close"
|
|
onCancel={() => setShowReference(false)}
|
|
hideFooter
|
|
triggerElement={
|
|
<Button
|
|
variant="default"
|
|
onClick={() => setShowReference(true)}
|
|
icon={<BookOpen strokeWidth={1.5} />}
|
|
>
|
|
<span>Field Reference</span>
|
|
</Button>
|
|
}
|
|
>
|
|
<SidePanel.Content>
|
|
<div className="pt-4 pb-2 space-y-1">
|
|
{useOtel ? (
|
|
<p className="text-sm">
|
|
The following table shows the fields available on each source. Nested fields live
|
|
in the <code className="text-xs">log_attributes</code> map and are read with{' '}
|
|
<code className="text-xs">log_attributes['key']</code> — no unnesting joins
|
|
needed.
|
|
</p>
|
|
) : (
|
|
<p className="text-sm">
|
|
The following table shows all the available paths that can be queried from each
|
|
respective source. Do note that to access nested keys, you would need to perform
|
|
the necessary{' '}
|
|
<Link
|
|
href={`${DOCS_URL}/guides/platform/logs#unnesting-arrays`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-brand"
|
|
>
|
|
unnesting joins
|
|
<ExternalLink
|
|
size={14}
|
|
className="ml-1 inline translate-y-[-2px]"
|
|
strokeWidth={1.5}
|
|
/>
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</SidePanel.Content>
|
|
<SidePanel.Separator />
|
|
<div className="px-4 pb-4 flex flex-col gap-4">
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="default"
|
|
role="combobox"
|
|
size={'small'}
|
|
aria-expanded={open}
|
|
className="w-full justify-between"
|
|
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
|
>
|
|
{selectedSchema?.name ?? 'Select source...'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" sameWidthAsTrigger>
|
|
<Command>
|
|
<CommandInput placeholder="Search source..." />
|
|
<CommandList>
|
|
<CommandEmpty>No source found.</CommandEmpty>
|
|
<CommandGroup>
|
|
{schemas.map((schema) => (
|
|
<CommandItem
|
|
key={schema.reference}
|
|
value={schema.reference}
|
|
onSelect={() => {
|
|
setSelectedRef(schema.reference)
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
'mr-2 h-4 w-4',
|
|
selectedSchema?.reference === schema.reference
|
|
? 'opacity-100'
|
|
: 'opacity-0'
|
|
)}
|
|
/>
|
|
{schema.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
{useOtel && isLoadingKeys ? (
|
|
<p className="text-sm text-foreground-light py-2">Loading fields…</p>
|
|
) : (
|
|
<Table
|
|
head={[
|
|
<Table.th className="text-xs p-2!" key="path">
|
|
Path
|
|
</Table.th>,
|
|
<Table.th key="type" className="text-xs p-2!">
|
|
Type
|
|
</Table.th>,
|
|
]}
|
|
body={(() => {
|
|
const fields = useOtel
|
|
? otelFieldsFromKeys(discoveredKeys ?? [])
|
|
: selectedSchema.fields
|
|
return fields.map((field) => <Field key={field.path} field={field} />)
|
|
})()}
|
|
/>
|
|
)}
|
|
</div>
|
|
</SidePanel>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default LogsExplorerHeader
|
|
|
|
const Field = ({
|
|
field,
|
|
}: {
|
|
field: {
|
|
path: string
|
|
type: string
|
|
}
|
|
}) => {
|
|
const [isCopied, setIsCopied] = useState(false)
|
|
|
|
return (
|
|
<Table.tr>
|
|
<Table.td
|
|
className="font-mono text-xs p-2! cursor-pointer hover:text-foreground transition flex items-center space-x-2"
|
|
onClick={() =>
|
|
copyToClipboard(field.path, () => {
|
|
setIsCopied(true)
|
|
setTimeout(() => setIsCopied(false), 3000)
|
|
})
|
|
}
|
|
>
|
|
<span>{field.path}</span>
|
|
{isCopied ? (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Check size={14} strokeWidth={3} className="text-brand" />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="font-sans">
|
|
Copied
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Copy size={14} />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="font-sans">
|
|
Copy value
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</Table.td>
|
|
<Table.td className="font-mono text-xs p-2!">{field.type}</Table.td>
|
|
</Table.tr>
|
|
)
|
|
}
|