Files
supabase/apps/studio/components/ui/Logs/LogsExplorerHeader.tsx
2aa1b52234 feat(studio): add feature to rewrite queries DEBUG-145 (#47266)
## 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>
2026-06-29 14:31:18 +02:00

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>
)
}