diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
index 8981208653a..d1157bb5b2c 100644
--- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
@@ -1,6 +1,7 @@
import { IS_PLATFORM } from 'common'
import Table from 'components/to-be-cleaned/Table'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import { usePHFlag } from 'hooks/ui/useFlag'
import { DOCS_URL } from 'lib/constants'
import { BookOpen, Check, ChevronDown, Copy, ExternalLink, X } from 'lucide-react'
import Link from 'next/link'
@@ -36,6 +37,8 @@ export interface LogsQueryPanelProps {
onSelectTemplate: (template: LogTemplate) => void
onSelectSource: (source: string) => void
onDateChange: (value: DatePickerValue) => void
+ queryMode?: 'sql' | 'natural'
+ onQueryModeChange?: (mode: 'sql' | 'natural') => void
}
function DropdownMenuItemContent({ name, desc }: { name: ReactNode; desc?: string }) {
@@ -54,9 +57,12 @@ const LogsQueryPanel = ({
onSelectTemplate,
onSelectSource,
onDateChange,
+ queryMode = 'sql',
+ onQueryModeChange,
}: LogsQueryPanelProps) => {
const [showReference, setShowReference] = useState(false)
const { logsTemplates } = useIsFeatureEnabled(['logs:templates'])
+ const logsExplorerAiQuerying = usePHFlag('logsExplorerAiQuerying')
const {
projectAuthAll: authEnabled,
@@ -85,50 +91,74 @@ const LogsQueryPanel = ({
-
-
- }>
- Insert source
+ {IS_PLATFORM && logsExplorerAiQuerying && onQueryModeChange && (
+
+
-
-
- {logsTableNames
- .sort((a, b) => a.localeCompare(b))
- .map((source) => (
- onSelectSource(source)}>
-
-
- ))}
-
-
+
+
+ )}
+ {queryMode !== 'natural' && (
+ <>
+
+
+ }>
+ Insert source
+
+
+
+ {logsTableNames
+ .sort((a, b) => a.localeCompare(b))
+ .map((source) => (
+ onSelectSource(source)}>
+
+
+ ))}
+
+
- {IS_PLATFORM && logsTemplates && (
-
-
- }>
- Templates
-
-
-
- {templates
- .sort((a, b) => a.label!.localeCompare(b.label!))
- .map((template) => (
- onSelectTemplate(template)}
- >
- {template.label}
-
- ))}
-
-
+ {IS_PLATFORM && logsTemplates && (
+
+
+ }>
+ Templates
+
+
+
+ {templates
+ .sort((a, b) => a.label!.localeCompare(b.label!))
+ .map((template) => (
+ onSelectTemplate(template)}
+ >
+ {template.label}
+
+ ))}
+
+
+ )}
+ >
)}
>
+
+export const useLogsQueryGenerateMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ return useMutation({
+ mutationFn: (vars) => generateLogsQuery(vars),
+ async onSuccess(data, variables, context) {
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(error, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to generate log query: ${error.message}`)
+ } else {
+ onError(error, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/pages/api/ai/sql/logs-query-v1.ts b/apps/studio/pages/api/ai/sql/logs-query-v1.ts
new file mode 100644
index 00000000000..3e7c8bfa68c
--- /dev/null
+++ b/apps/studio/pages/api/ai/sql/logs-query-v1.ts
@@ -0,0 +1,127 @@
+import { generateText } from 'ai'
+import { source } from 'common-tags'
+import { getModel } from 'lib/ai/model'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { logConstants } from 'shared-data'
+import { z } from 'zod'
+
+const requestSchema = z.object({
+ prompt: z.string().min(1).max(2000),
+})
+
+const LOG_TABLES = [
+ { name: 'edge_logs', description: 'Logs obtained from the network edge, containing all API requests' },
+ { name: 'postgres_logs', description: 'Database logs obtained directly from Postgres' },
+ { name: 'function_logs', description: 'Function logs generated from runtime execution' },
+ { name: 'function_edge_logs', description: 'Function call logs, containing the request and response' },
+ { name: 'auth_logs', description: 'Errors, warnings, and performance details from the auth service' },
+ { name: 'auth_audit_logs', description: 'Audit records of user signups, logins, and account changes' },
+ { name: 'realtime_logs', description: 'Realtime server for Postgres logical replication broadcasting' },
+ { name: 'storage_logs', description: 'Object storage logs' },
+ { name: 'postgrest_logs', description: 'RESTful API web server logs' },
+ { name: 'supavisor_logs', description: 'Shared connection pooler logs for PostgreSQL' },
+ { name: 'pgbouncer_logs', description: 'Dedicated connection pooler for PostgreSQL' },
+ { name: 'pg_upgrade_logs', description: 'Logs generated by the Postgres version upgrade process' },
+ { name: 'pg_cron_logs', description: 'Postgres logs from pg_cron cron jobs' },
+ { name: 'etl_replication_logs', description: 'Logs from the replication process' },
+]
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
+ const parseResult = requestSchema.safeParse(req.body)
+
+ if (!parseResult.success) {
+ const errorMessage = parseResult.error.errors.map((e) => e.message).join(', ')
+ return res.status(400).json({ error: errorMessage })
+ }
+
+ const { prompt } = parseResult.data
+
+ try {
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
+ provider: 'openai',
+ routingKey: 'sql',
+ })
+
+ if (modelError) {
+ return res.status(500).json({ error: modelError.message })
+ }
+
+ const result = await generateText({
+ model,
+ providerOptions,
+ system: source`
+ You are an expert at writing SQL queries for Supabase Log Explorer.
+ The system uses BigQuery-compatible SQL syntax with standard PostgreSQL functions.
+
+ Available log tables:
+ ${JSON.stringify(LOG_TABLES, null, 2)}
+
+ Schema fields per table (nested paths require UNNEST joins to access):
+ ${JSON.stringify(
+ logConstants.schemas.map((s) => ({ table: s.reference, fields: s.fields })),
+ null,
+ 2
+ )}
+
+ Key SQL syntax rules:
+ - To access nested metadata fields, use LEFT JOIN UNNEST(metadata) AS m ON TRUE
+ - For deeper nesting (e.g. request inside metadata): LEFT JOIN UNNEST(m.request) AS req ON TRUE
+ - Use cast(timestamp as datetime) for readable timestamps
+ - Use regexp_contains(field, r'pattern') for regex matching
+ - Always include a LIMIT clause (default to 100 if not specified)
+ - If the user doesn't specify a log source, default to edge_logs
+ - Return ONLY the SQL query — no markdown code blocks, no explanation, no comments
+ `,
+ prompt,
+ })
+
+ const sql = result.text.trim()
+
+ // Strip any markdown code blocks the model may have added
+ const cleanedSql = sql
+ .replace(/^```(?:sql)?\n?/i, '')
+ .replace(/\n?```$/i, '')
+ .trim()
+
+ return res.json({ sql: cleanedSql })
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error(`AI log query generation failed: ${error.message}`)
+
+ if (error.message.includes('context_length') || error.message.includes('too long')) {
+ return res.status(400).json({
+ error:
+ 'Your query prompt is too large for Supabase Assistant to ingest. Try a shorter description.',
+ })
+ }
+ } else {
+ console.error(`Unknown error: ${error}`)
+ }
+
+ return res.status(500).json({
+ error: 'There was an unknown error generating the log query. Please try again.',
+ })
+ }
+}
+
+const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
+ apiWrapper(req, res, handler, { withAuth: true })
+
+export default wrapper
diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx
index df6375cc053..05ecd91d283 100644
--- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx
+++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx
@@ -25,6 +25,7 @@ import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import LoadingOpacity from 'components/ui/LoadingOpacity'
import ShimmerLine from 'components/ui/ShimmerLine'
+import { useLogsQueryGenerateMutation } from 'data/ai/logs-query-mutation'
import { useContentQuery } from 'data/content/content-query'
import {
UpsertContentPayload,
@@ -105,6 +106,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
const [saveModalOpen, setSaveModalOpen] = useState(false)
const [warnings, setWarnings] = useState([])
const [selectedLog, setSelectedLog] = useState(null)
+ const [queryMode, setQueryMode] = useState<'sql' | 'natural'>('sql')
+ const [nlQuery, setNlQuery] = useState('')
const [recentLogs, setRecentLogs] = useLocalStorage(
`project-content-${projectRef}-recent-log-sql`,
@@ -169,6 +172,19 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
},
})
+ const { mutate: generateLogsQuery, isPending: isGenerating } = useLogsQueryGenerateMutation({
+ onSuccess: (data) => {
+ setEditorValue(data.sql)
+ handleRun(data.sql)
+ track('log_explorer_nl_query_generated')
+ },
+ })
+
+ const handleNlSubmit = () => {
+ if (!nlQuery.trim() || isGenerating) return
+ generateLogsQuery({ prompt: nlQuery.trim() })
+ }
+
const addRecentLogSqlSnippet = (snippet: Partial) => {
const defaults: LogSqlSnippets.Content = {
schema_version: '1',
@@ -389,16 +405,44 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
templates={allTemplates.filter((template) => template.mode === 'custom')}
onSelectTemplate={onSelectTemplate}
warnings={warnings}
+ queryMode={queryMode}
+ onQueryModeChange={setQueryMode}
/>
-
- setEditorValue(v || '')}
- actions={{ runQuery: { enabled: true, callback: handleRun } }}
- />
+
+ {queryMode === 'sql' ? (
+ setEditorValue(v || '')}
+ actions={{ runQuery: { enabled: true, callback: handleRun } }}
+ />
+ ) : (
+
+ )}