feat(functions): migrate EdgeFunctionRecentErrors to logs.all.otel (#47489)

## 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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jordi EnricandClaude Sonnet 4.6 authored and GitHub committed 2026-07-02 13:03:29 +02:00
1 parent 91d70a8c38
commit 0bfca221e9
4 files changed
+90 -32

No files matched your search

@@ -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`.
@@ -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)
@@ -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([
@@ -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 = (