From 0bfca221e90c9df03da960ad81c1e73da44934a5 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:03:29 +0200 Subject: [PATCH] feat(functions): migrate EdgeFunctionRecentErrors to logs.all.otel (#47489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The edge function overview page (gated by the \`edgeFunctionsOverview\` flag) runs three log queries against the legacy BigQuery \`logs.all\` endpoint. These need to move to the ClickHouse-backed \`logs.all.otel\` endpoint to stay consistent with the rest of the logs migration. ## Fix Rewrote the three SQL query builders in \`EdgeFunctionRecentErrors.utils.ts\` from BigQuery syntax to ClickHouse syntax targeting the \`edge_logs\` OTEL schema. Added \`{ useOtel: true }\` to all three \`useLogsQuery\` calls to route them to the \`logs.all.otel\` endpoint. Key field mappings used: - \`metadata[0].function_id\` -> \`LogAttributes['function_id']\` - \`metadata[0].execution_id\` -> \`LogAttributes['execution_id']\` - \`metadata[0].level\` / \`metadata[0].event_type\` -> \`SeverityText\` / \`LogAttributes['event_type']\` - \`timestamp\` -> \`toUnixTimestamp64Micro(Timestamp)\` (preserves microsecond integer format expected downstream) - HTTP invocations filtered by \`LogAttributes['event_type'] = 'Request'\` - Runtime logs filtered by \`LogAttributes['event_type'] = 'Log'\` ## How to test - Enable the \`edgeFunctionsOverview\` feature flag on a project that has an edge function with recent invocations and errors - Navigate to the function overview page - The "Errors since last deploy" section should load and display error groups correctly - Each error group should show count, last seen time, method, status code, and execution time - Expanding a group should show related runtime logs beneath it - With no errors, the empty state should show the invocation count since last deploy ## Summary by CodeRabbit * **Bug Fixes** * Improved Edge Function recent errors with more accurate filtering of server-side failures. * Expanded Edge Function runtime log coverage for clearer event visibility. * Refreshed Edge Function since-deploy invocation counts to better match current log querying behavior. * **Documentation** * Refined “minimal, well-formed query” guidance, including requiring an identifying comment at the start and clearer log source scoping examples. * **Tests** * Updated unit tests to match the revised SQL/log filtering and selection logic. --------- Co-authored-by: Claude Sonnet 4.6 --- .../skills/clickhouse-logs-queries/SKILL.md | 7 +- .../EdgeFunctionRecentErrors.tsx | 9 ++- .../EdgeFunctionRecentErrors.utils.test.ts | 42 ++++++++++-- .../EdgeFunctionRecentErrors.utils.ts | 64 +++++++++++++------ 4 files changed, 90 insertions(+), 32 deletions(-) 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 = (