From 28c5a828596519e8762211e2c4658a89d805fa95 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Tue, 18 Feb 2025 21:36:55 +1000 Subject: [PATCH] assistant log query --- .../ui/AIAssistantPanel/AIAssistant.types.ts | 1 + .../ui/AIAssistantPanel/MessageMarkdown.tsx | 7 + .../components/ui/QueryBlock/QueryBlock.tsx | 60 +++- apps/studio/pages/api/ai/sql/generate-v3.ts | 21 ++ apps/studio/pages/api/ai/sql/tools.ts | 289 ++++++++++++++++++ 5 files changed, 363 insertions(+), 15 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts index 9f847ae2c8c..bd3648205a3 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts @@ -5,6 +5,7 @@ export interface AssistantSnippetProps { title?: string isChart?: string runQuery?: string + logs?: string xAxis?: string yAxis?: string name?: string diff --git a/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx b/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx index 019eab1197a..59af0de26ca 100644 --- a/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx @@ -62,6 +62,7 @@ const MemoizedQueryBlock = memo( isLoading, isDraggable, runQuery, + logs, onRunQuery, onDragStart, onUpdateChartConfig, @@ -74,6 +75,7 @@ const MemoizedQueryBlock = memo( isLoading: boolean isDraggable: boolean runQuery: boolean + logs?: boolean onRunQuery: (queryType: 'select' | 'mutation') => void onDragStart: (e: DragEvent) => void onUpdateChartConfig?: ({ @@ -97,6 +99,7 @@ const MemoizedQueryBlock = memo( lockColumns label={title} sql={sql} + logs={logs} chartConfig={{ type: 'bar', cumulative: false, @@ -162,6 +165,9 @@ export const MarkdownPre = ({ children }: { children: any }) => { const title = snippetProps.title || (language === 'edge' ? 'Edge Function' : 'SQL Query') const isChart = snippetProps.isChart === 'true' const runQuery = snippetProps.runQuery === 'true' + const isLogs = snippetProps.logs === 'true' + + console.log('raw:', rawContent, snippetProps, isLogs) // Strip props from the content for both SQL and edge functions const cleanContent = rawContent.replace(/(?:--|\/\/)\s*props:\s*\{[^}]+\}/, '').trim() @@ -213,6 +219,7 @@ export const MarkdownPre = ({ children }: { children: any }) => { isLoading={isLoading} isDraggable={isDraggableToReports} runQuery={runQuery} + logs={isLogs} onRunQuery={onRunQuery} onUpdateChartConfig={({ chartConfig: config }) => { chartConfig.current = { ...chartConfig.current, ...config } diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index 8a69fb168a2..b4464f18408 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -9,6 +9,7 @@ import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartC import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import useLogsQuery from 'hooks/analytics/useLogsQuery' import { Parameter, parseParameters } from 'lib/sql-parameters' import { Dashboards } from 'types' import { @@ -28,6 +29,7 @@ import { BlockViewConfiguration } from './BlockViewConfiguration' import { EditQueryButton } from './EditQueryButton' import { ParametersPopover } from './ParametersPopover' import { getCumulativeResults } from './QueryBlock.utils' +import dayjs from 'dayjs' export const DEFAULT_CHART_CONFIG: ChartConfig = { type: 'bar', @@ -60,6 +62,8 @@ interface QueryBlockProps { isLoading?: boolean /** Override to prevent running the SQL query provided */ runQuery?: boolean + /** Whether this is a logs query */ + logs?: boolean /** Prevent updating of columns for X and Y axes in the chart view */ lockColumns?: boolean /** Max height set to render results / charts (Defaults to 250) */ @@ -109,6 +113,7 @@ export const QueryBlock = ({ isChart = false, isLoading = false, runQuery = false, + logs = false, lockColumns = false, draggable = false, isRefreshing = false, @@ -143,22 +148,40 @@ export const QueryBlock = ({ onSuccess: (data) => setQueryResult(data.result), }) + const { + logData, + error: logsError, + isLoading: isLoadingLogs, + runQuery: runLogsQuery, + } = useLogsQuery( + ref || '', + { + sql: sql || '', + iso_timestamp_start: dayjs().subtract(7, 'day').toISOString(), + iso_timestamp_end: dayjs().toISOString(), + }, + logs && runQuery && Boolean(ref) + ) + const handleExecute = () => { if (!sql || isLoading) return - if (!isReadOnlySelectSQL) { + // Skip SQL validation for log queries since they use BigQuery + if (!logs && !isReadOnlySelectSQL) { const hasUnknownFunctions = containsUnknownFunction(sql) return setShowWarning(hasUnknownFunctions ? 'hasUnknownFunctions' : 'hasWriteOperation') } try { - // [Joshen] This is for when we introduced the concept of parameters into our reports - // const processedSql = processParameterizedSql(sql, combinedParameterValues) - execute({ - projectRef: ref, - connectionString: project?.connectionString, - sql, - }) + if (logs) { + runLogsQuery() + } else { + execute({ + projectRef: ref, + connectionString: project?.connectionString, + sql, + }) + } } catch (error: any) { toast.error(`Failed to execute query: ${error.message}`) } @@ -168,21 +191,28 @@ export const QueryBlock = ({ setChartSettings(chartConfig) }, [chartConfig]) + useEffect(() => { + if (logs && logData && !isLoading) { + setQueryResult(logData) + } + }, [logData, isLoading]) + // Run once on mount to parse parameters and notify parent useEffect(() => { if (!!sql && onSetParameter) { const params = parseParameters(sql) onSetParameter(params) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sql]) + }, [sql, onSetParameter]) useEffect(() => { - if (!!sql && !isLoading && runQuery && isReadOnlySelect(sql) && !!project) { + const shouldExecute = !!sql && !isLoading && runQuery && !!project + const isValidQuery = logs || (!logs && isReadOnlySelect(sql ?? '')) + + if (shouldExecute && isValidQuery) { handleExecute() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sql, isLoading, runQuery, project]) + }, [sql, isLoading, runQuery, project?.connectionString]) useEffect(() => { if (isRefreshing) handleExecute() @@ -193,7 +223,7 @@ export const QueryBlock = ({ draggable={draggable} showDragHandle={draggable} tooltip={tooltip} - loading={isExecuting} + loading={isExecuting || isLoadingLogs} onDragStart={(e: DragEvent) => onDragStart?.(e)} icon={ } - loading={isExecuting || isLoading} + loading={isExecuting || isLoadingLogs || isLoading} disabled={isLoading} onClick={() => { handleExecute() diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts index d0b280c4e4a..6c61b575bd9 100644 --- a/apps/studio/pages/api/ai/sql/generate-v3.ts +++ b/apps/studio/pages/api/ai/sql/generate-v3.ts @@ -135,6 +135,27 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { # You convert sql to supabase-js client code Use the convertSqlToSupabaseJs tool to convert select sql to supabase-js client code. Only provide js code snippets if explicitly asked. If conversion isn't supported, build a postgres function instead and suggest using supabase-js to call it via "const { data, error } = await supabase.rpc('echo', { say: '👋'})" + # You write log queries for the Supabase Logs Explorer + Your purpose is to help users write effective queries for the Supabase Logs Explorer. + - First, always use the getLogsExplorerKnowledge tool to get knowledge about how to write log queries + - When writing log queries, follow these guidelines: + - Always use DATETIME() to format timestamps in a human-readable way + - Always include appropriate UNNEST joins when accessing nested fields + - Always include a LIMIT clause (max 1000 rows) + - Always include appropriate timestamp filters to optimize performance + - Avoid selecting entire nested objects, select specific fields instead + - When outputting queries, always include a props comment in the first line where logs is set to true: + -- props: {"title": "Log Query", "logs": "true", "runQuery": "true"} + - The Logs Explorer uses BigQuery syntax and supports all BigQuery functions and operators + - Available log sources include: + - auth_logs: Authentication/authorization activity + - edge_logs: Edge network requests and responses + - function_edge_logs: Edge function network activity + - function_logs: Edge function console logs + - postgres_logs: Database statements + - realtime_logs: Client connection information + - storage_logs: Object upload and retrieval + # For all your abilities, follow these instructions: - First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. - If the question is about users or involves creating a users table, also retrieve the auth schema. diff --git a/apps/studio/pages/api/ai/sql/tools.ts b/apps/studio/pages/api/ai/sql/tools.ts index a0da432e704..aa1dd9febc5 100644 --- a/apps/studio/pages/api/ai/sql/tools.ts +++ b/apps/studio/pages/api/ai/sql/tools.ts @@ -568,5 +568,294 @@ export const getTools = ({ ` }, }), + getLogsExplorerKnowledge: tool({ + description: 'Get knowledge about how to write queries for the Supabase Logs Explorer', + parameters: z.object({}), + execute: async ({}) => { + return stripIndent` + # Querying Logs in Supabase Logs Explorer + + You're an expert in writing SQL queries for the Supabase Logs Explorer. The Logs Explorer uses BigQuery and supports all available SQL functions and operators. + + ## Available Log Sources + + The following log sources are available from the Sources drop-down: + + * \`auth_logs\`: GoTrue server logs containing authentication/authorization activity + * \`edge_logs\`: Edge network logs containing request and response metadata from Cloudflare + * \`function_edge_logs\`: Edge network logs for edge functions, containing network requests and response metadata + * \`function_logs\`: Function internal logs containing any console logging from edge functions + * \`postgres_logs\`: Postgres database logs containing statements executed by connected applications + * \`realtime_logs\`: Realtime server logs containing client connection information + * \`storage_logs\`: Storage server logs containing object upload and retrieval information + + ## Working with Timestamps + + Each log entry has a \`timestamp\` field of type \`TIMESTAMP\`. Important considerations: + + 1. Raw timestamps are displayed in unix microseconds + 2. Use \`DATETIME()\` to convert timestamps to human-readable ISO-8601 format: + + \`\`\`sql + -- Raw timestamp (unix microseconds) + SELECT timestamp FROM edge_logs; + -- Returns: 1664270180000 + + -- Formatted timestamp + SELECT DATETIME(timestamp) FROM edge_logs; + -- Returns: 2022-09-27T09:17:10.439Z + \`\`\` + + ## Working with Nested Arrays + + Log events store metadata as nested arrays of objects. To query nested data: + + 1. Use \`UNNEST()\` for each array level you want to access + 2. Add joins for each level of nesting + 3. Reference nested objects using aliases + + Example querying edge logs: + + \`\`\`sql + SELECT + DATETIME(timestamp), + request.method, + header.cf_ipcountry + FROM + edge_logs AS t + CROSS JOIN UNNEST(t.metadata) AS metadata + CROSS JOIN UNNEST(metadata.request) AS request + CROSS JOIN UNNEST(request.headers) AS header; + \`\`\` + + ## Best Practices + + 1. Always include a filter on \`timestamp\` to optimize query performance + 2. Avoid selecting large nested objects - select individual values instead + 3. Use \`LIMIT\` to reduce returned rows (max 1000 rows per query) + + Examples: + + ### Edge function 500 error + \`\`\`sql + select id, function_edge_logs.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from function_edge_logs + cross join unnest(metadata) as m + cross join unnest(m.response) as response + cross join unnest(m.request) as request + where (response.status_code between 500 and 599) + order by timestamp desc + limit 100 + \`\`\` + ### ❌ Avoid selecting entire metadata objects + \`\`\`sql + SELECT + DATETIME(timestamp), + m AS metadata -- Selects entire metadata object + FROM + edge_logs AS t + CROSS JOIN UNNEST(t.metadata) AS m; + \`\`\` + + ### ✅ Select specific needed values + \`\`\`sql + SELECT + DATETIME(timestamp), + r.method -- Select only required fields + FROM + edge_logs AS t + CROSS JOIN UNNEST(t.metadata) AS m + CROSS JOIN UNNEST(m.request) AS r; + \`\`\` + + ## Common Query Examples + + ### Get user IP addresses from edge logs: + \`\`\`sql + SELECT + DATETIME(timestamp), + h.x_real_ip + FROM + edge_logs + CROSS JOIN UNNEST(metadata) AS m + CROSS JOIN UNNEST(m.request) AS r + CROSS JOIN UNNEST(r.headers) AS h + WHERE + h.x_real_ip IS NOT NULL + AND r.method = "GET" + LIMIT 1000; + \`\`\` + + ### Filter logs by time range: + \`\`\`sql + SELECT + DATETIME(timestamp) as time, + event_message + FROM postgres_logs + WHERE + timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) + ORDER BY timestamp DESC + LIMIT 1000; + \`\`\` + + Remember: + - The Logs Explorer has a maximum of 1000 rows per query + - Log retention depends on your project's pricing plan + - Use appropriate timestamp filters to optimize performance + - Avoid selecting entire nested objects when possible + + ## Logs Field Reference + + Below is the complete field reference for each log source. To access nested fields, you'll need to use the appropriate UNNEST joins as shown in the examples above. + + ### Function Edge Logs (\`function_edge_logs\`) + + | Field | Type | + |-------|------| + | event_message | string | + | id | string | + | timestamp | datetime | + | metadata.deployment_id | string | + | metadata.execution_time_ms | number | + | metadata.function_id | string | + | metadata.project_ref | string | + | metadata.request.headers.accept | string | + | metadata.request.headers.content_length | string | + | metadata.request.headers.host | string | + | metadata.request.headers.user_agent | string | + | metadata.request.host | string | + | metadata.request.method | string | + | metadata.request.pathname | string | + | metadata.request.protocol | string | + | metadata.request.url | string | + | metadata.response.headers.content_length | string | + | metadata.response.headers.content_type | string | + | metadata.response.headers.date | string | + | metadata.response.headers.server | string | + | metadata.response.headers.vary | string | + | metadata.response.status_code | number | + | metadata.version | string | + + ### API Edge Logs (\`edge_logs\`) + + | Field | Type | + |-------|------| + | id | string | + | timestamp | datetime | + | event_message | string | + | identifier | string | + | metadata.load_balancer_redirect_identifier | string | + | metadata.request.cf.asOrganization | string | + | metadata.request.cf.asn | number | + | metadata.request.cf.botManagement.corporateProxy | boolean | + | metadata.request.cf.botManagement.detectionIds | number[] | + | metadata.request.cf.botManagement.ja3Hash | string | + | metadata.request.cf.botManagement.score | number | + | metadata.request.cf.botManagement.staticResource | boolean | + | metadata.request.cf.botManagement.verifiedBot | boolean | + | metadata.request.cf.city | string | + | metadata.request.cf.clientTcpRtt | number | + | metadata.request.cf.clientTrustScore | number | + | metadata.request.cf.colo | string | + | metadata.request.cf.continent | string | + | metadata.request.cf.country | string | + | metadata.request.cf.edgeRequestKeepAliveStatus | number | + | metadata.request.cf.httpProtocol | string | + | metadata.request.cf.latitude | string | + | metadata.request.cf.longitude | string | + | metadata.request.cf.metroCode | string | + | metadata.request.cf.postalCode | string | + | metadata.request.cf.region | string | + | metadata.request.cf.timezone | string | + | metadata.request.cf.tlsCipher | string | + | metadata.request.cf.tlsClientAuth.certPresented | string | + | metadata.request.cf.tlsClientAuth.certRevoked | string | + | metadata.request.cf.tlsClientAuth.certVerified | string | + | metadata.request.cf.tlsExportedAuthenticator.clientFinished | string | + | metadata.request.cf.tlsExportedAuthenticator.clientHandshake | string | + | metadata.request.cf.tlsExportedAuthenticator.serverFinished | string | + | metadata.request.cf.tlsExportedAuthenticator.serverHandshake | string | + | metadata.request.cf.tlsVersion | string | + | metadata.request.headers.cf_connecting_ip | string | + | metadata.request.headers.cf_ipcountry | string | + | metadata.request.headers.cf_ray | string | + | metadata.request.headers.host | string | + | metadata.request.headers.referer | string | + | metadata.request.headers.x_client_info | string | + | metadata.request.headers.x_forwarded_proto | string | + | metadata.request.headers.x_real_ip | string | + | metadata.request.host | string | + | metadata.request.method | string | + | metadata.request.path | string | + | metadata.request.protocol | string | + | metadata.request.search | string | + | metadata.request.url | string | + | metadata.response.headers.cf_cache_status | string | + | metadata.response.headers.cf_ray | string | + | metadata.response.headers.content_location | string | + | metadata.response.headers.content_range | string | + | metadata.response.headers.content_type | string | + | metadata.response.headers.date | string | + | metadata.response.headers.sb_gateway_version | string | + | metadata.response.headers.transfer_encoding | string | + | metadata.response.headers.x_kong_proxy_latency | string | + | metadata.response.origin_time | number | + | metadata.response.status_code | number | + + ### PostgREST Logs (\`postgrest_logs\`) + + | Field | Type | + |-------|------| + | event_message | string | + | id | string | + | identifier | string | + | timestamp | datetime | + | metadata.host | string | + + ### Auth Logs (\`auth_logs\`) + + | Field | Type | + |-------|------| + | event_message | string | + | id | string | + | timestamp | datetime | + | metadata.auth_event.action | string | + | metadata.auth_event.actor_id | string | + | metadata.auth_event.actor_username | string | + | metadata.auth_event.log_type | string | + | metadata.auth_event.traits.provider | string | + | metadata.auth_event.traits.user_email | string | + | metadata.auth_event.traits.user_id | string | + | metadata.auth_event.traits.user_phone | string | + | metadata.component | string | + | metadata.duration | number | + | metadata.host | string | + | metadata.level | string | + | metadata.method | string | + | metadata.msg | string | + | metadata.path | string | + | metadata.referer | string | + | metadata.remote_addr | string | + | metadata.status | number | + | metadata.timestamp | string | + + Example query using nested fields: + \`\`\`sql + -- Query auth logs for specific user actions + SELECT + DATETIME(timestamp) as time, + metadata.auth_event.action, + metadata.auth_event.actor_username, + metadata.auth_event.traits.user_email + FROM auth_logs + CROSS JOIN UNNEST(metadata.auth_event) as auth_event + WHERE + metadata.auth_event.action = 'login' + AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR) + ORDER BY timestamp DESC + LIMIT 1000; + \`\`\` + ` + }, + }), } }