diff --git a/.claude/skills/clickhouse-logs-queries/SKILL.md b/.claude/skills/clickhouse-logs-queries/SKILL.md index 601726abca8..91960c73179 100644 --- a/.claude/skills/clickhouse-logs-queries/SKILL.md +++ b/.claude/skills/clickhouse-logs-queries/SKILL.md @@ -49,9 +49,11 @@ in `log_attributes`. precision, no trailing `Z`). In the Logs Explorer the selected time range is applied for you, so you rarely need to write a `timestamp` filter by hand. -A minimal, well-formed query: +A minimal, well-formed query. Lead with a comment naming the query, filter by +`source`, and always `limit`: ```sql +-- recent edge requests select timestamp, event_message from logs where source = 'edge_logs' @@ -155,8 +157,9 @@ rejects `count(*)` and `select *` — use `count()` and list the columns you nee These keep queries correct and cheap. Log tables are large; an unbounded scan reads far more data than you need. +- **Start every query with an identifying comment** (e.g. `-- errors since last deploy`). It labels the query in logs and review, and makes each of several queries in a file easy to tell apart. - **Always include a `LIMIT`.** Even for aggregates while you iterate. -- **Always filter by `source`.** It scopes the query to one service. +- **Always query `from logs where source = '...'`.** There is no per-service table (no `edge_logs`, `postgres_logs`, etc. table) — there is one `logs` table, and `source` scopes it to a service. Filtering by `source` is required, not just an optimization. - **Keep the time range tight.** A smaller window returns results faster. - **Filter on the real columns** (`source`, `timestamp`) before reaching into `log_attributes`. diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx index d99ba2ef0fe..c58c800761e 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.tsx @@ -94,7 +94,8 @@ export const EdgeFunctionRecentErrors = ({ iso_timestamp_start: isoTimestampStart, iso_timestamp_end: isoTimestampEnd, }, - isQueryEnabled + isQueryEnabled, + { useOtel: true } ) const recentErrorGroupsBase = useMemo( @@ -112,7 +113,8 @@ export const EdgeFunctionRecentErrors = ({ iso_timestamp_start: isoTimestampStart, iso_timestamp_end: isoTimestampEnd, }, - Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart) + Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart), + { useOtel: true } ) const relatedExecutionIds = useMemo( @@ -136,7 +138,8 @@ export const EdgeFunctionRecentErrors = ({ iso_timestamp_start: isoTimestampStart, iso_timestamp_end: isoTimestampEnd, }, - Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart) + Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart), + { useOtel: true } ) const queryError = toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts index 5200cdda492..6486cd69e1b 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.test.ts @@ -10,6 +10,7 @@ import { getNoErrorsSinceLastDeployMessage, getRecentErrorGroups, getRecentErrorGroupsBase, + getRecentErrorInvocationsSql, getRelatedExecutionIds, getSinceLastDeployInvocationCount, getSinceLastDeployInvocationCountSql, @@ -47,10 +48,37 @@ describe('EdgeFunctionRecentErrors.utils', () => { executionIds: ['exec_1', "exec_'2"], limit: 25, }) - ) - .toBe(`select id, function_logs.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from function_logs -cross join unnest(metadata) as metadata -where metadata.function_id = 'fn_''123' and metadata.execution_id in ('exec_1', 'exec_''2') + ).toBe(`-- runtime logs for error groups +select + toUnixTimestamp64Micro(timestamp) as timestamp, + event_message, + log_attributes['level'] as level, + log_attributes['event_type'] as event_type, + log_attributes['function_id'] as function_id, + log_attributes['execution_id'] as execution_id +from logs +where + source = 'function_logs' + and log_attributes['function_id'] = 'fn_''123' + and log_attributes['execution_id'] in ('exec_1', 'exec_''2') +order by timestamp desc +limit 25`) + }) + + it('builds recent error invocations SQL and escapes the function id', () => { + expect(getRecentErrorInvocationsSql("fn_'123", 25)).toBe(`-- errors since last deploy +select + toUnixTimestamp64Micro(timestamp) as timestamp, + event_message, + log_attributes['request.method'] as method, + log_attributes['response.status_code'] as status_code, + toFloat64OrZero(log_attributes['execution_time_ms']) as execution_time_ms, + log_attributes['execution_id'] as execution_id +from logs +where + source = 'function_edge_logs' + and log_attributes['function_id'] = 'fn_''123' + and toInt32OrZero(log_attributes['response.status_code']) >= 500 order by timestamp desc limit 25`) }) @@ -83,9 +111,11 @@ limit 25`) it('builds the since-deploy invocation count query and empty-state message', () => { expect(getSinceLastDeployInvocationCountSql()).toContain( - 'SELECT count(*) as count FROM function_edge_logs' + "select count() as count from logs where source = 'function_edge_logs'" + ) + expect(getSinceLastDeployInvocationCountSql()).toContain( + "log_attributes['function_id'] = '__pending__'" ) - expect(getSinceLastDeployInvocationCountSql()).toContain("(`function_id` = '__pending__')") expect( getSinceLastDeployInvocationCount([ diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts index d404963428b..a52fa09cdb4 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionRecentErrors.utils.ts @@ -2,11 +2,8 @@ import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { parseEdgeFunctionEventMessage } from '../EdgeFunctionRecentInvocations.utils' -import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants' import type { LogData } from '@/components/interfaces/Settings/Logs/Logs.types' import { - genCountQuery, - genDefaultQuery, isUnixMicro, unixMicroToIsoTimestamp, } from '@/components/interfaces/Settings/Logs/Logs.utils' @@ -196,20 +193,32 @@ export const getStatusBadgeVariant = (statusCode?: string) => { export const getRecentErrorInvocationsSql = ( functionId?: string, limit = RECENT_ERROR_INVOCATIONS_LIMIT -) => - genDefaultQuery( - LOGS_TABLES.fn_edge, - { - function_id: functionId ?? '__pending__', - 'status_code.error': true, - }, - limit - ) +): string => { + const id = escapeSqlString(functionId ?? '__pending__') + return ` +-- errors since last deploy +select + toUnixTimestamp64Micro(timestamp) as timestamp, + event_message, + log_attributes['request.method'] as method, + log_attributes['response.status_code'] as status_code, + toFloat64OrZero(log_attributes['execution_time_ms']) as execution_time_ms, + log_attributes['execution_id'] as execution_id +from logs +where + source = 'function_edge_logs' + and log_attributes['function_id'] = '${id}' + and toInt32OrZero(log_attributes['response.status_code']) >= 500 +order by timestamp desc +limit ${limit} +`.trim() +} -export const getSinceLastDeployInvocationCountSql = (functionId?: string) => - genCountQuery(LOGS_TABLES.fn_edge, { - function_id: functionId ?? '__pending__', - }) +export const getSinceLastDeployInvocationCountSql = (functionId?: string): string => { + const id = escapeSqlString(functionId ?? '__pending__') + return `-- invocation count since last deploy +select count() as count from logs where source = 'function_edge_logs' and log_attributes['function_id'] = '${id}'` +} export const getSinceLastDeployInvocationCount = (invocationCountRows: LogData[]) => { const count = Number(invocationCountRows[0]?.count ?? 0) @@ -238,16 +247,29 @@ export const getFunctionRuntimeLogsSql = ({ functionId?: string executionIds: string[] limit?: number -}) => { +}): string => { if (!functionId || executionIds.length === 0) return '' + const escapedFunctionId = escapeSqlString(functionId) const escapedExecutionIds = executionIds.map((id) => `'${escapeSqlString(id)}'`).join(', ') - return `select id, function_logs.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from function_logs -cross join unnest(metadata) as metadata -where metadata.function_id = '${escapeSqlString(functionId)}' and metadata.execution_id in (${escapedExecutionIds}) + return ` +-- runtime logs for error groups +select + toUnixTimestamp64Micro(timestamp) as timestamp, + event_message, + log_attributes['level'] as level, + log_attributes['event_type'] as event_type, + log_attributes['function_id'] as function_id, + log_attributes['execution_id'] as execution_id +from logs +where + source = 'function_logs' + and log_attributes['function_id'] = '${escapedFunctionId}' + and log_attributes['execution_id'] in (${escapedExecutionIds}) order by timestamp desc -limit ${limit}` +limit ${limit} +`.trim() } export const getRecentErrorGroupsBase = (