Files
supabase__supabase/apps/studio/data/reports/v2/edge-functions.config.otel.test.ts
Jordi Enric aed332b663 feat: migrate Edge Functions report to ClickHouse OTEL logs (#48756)
## Problem

The Edge Functions report (`observability/edge-functions`) only queries
BigQuery. As part of the broader Reports→ClickHouse OTEL migration
(DEBUG-73), we need each report migrated one at a time behind the
`otelReports` flag.

## Fix

Adds a ClickHouse OTEL SQL variant (`METRIC_SQL_OTEL`) for the 4 Edge
Functions metrics (TotalInvocations, ExecutionStatusCodes,
InvocationsByRegion, ExecutionTime), querying the unified `logs` table
filtered to `source = 'function_edge_logs'`, with fields read from
`log_attributes` (`function_id`, `response.status_code`,
`response.headers.x_sb_edge_region`, `execution_time_ms`) — the same
mapping already used by `edge-functions-last-hour-stats-query.ts`.

`edgeFunctionReports()` now takes a `useOtel` flag that picks between
the BQ and OTEL query sets and forwards it to `fetchLogs`. The page
wires this up via `useFlag('otelReports')`, matching the pattern used
for the Auth report. No behavior change while the flag is off — report
still fetches from BigQuery.

Also removed two pieces of dead code spotted in `report.utils.ts` while
touching it: the unused `useEdgeFnIdToName` hook and a
`STATUS_CODE_COLORS` map that was an exact duplicate of
`REPORT_STATUS_CODE_COLORS` (the one actually imported elsewhere).

This PR is standalone — no dependency on the in-flight Auth report OTEL
stack.

## How to test

- `pnpm vitest run data/reports/v2/edge-functions.config.otel.test.ts` —
9 new tests covering the OTEL SQL shape (single logs table,
unix-microsecond timestamp bucketing, field mapping, filters).
- `pnpm vitest run data/reports` and `pnpm vitest run
data/edge-functions components/interfaces/Reports` — existing suites (46
+ 54 tests) still pass, confirming no regression to the BQ path.
- Manually: with `otelReports` flag enabled, visit a project's Edge
Functions observability report and confirm charts render from the
ClickHouse endpoint.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added OpenTelemetry support for Edge Functions observability metrics,
including invocations, status codes, regional activity, and execution
time.
* Reports can now dynamically use either the standard or OpenTelemetry
logs source.

* **Bug Fixes**
* Improved filtering and timestamp handling for OpenTelemetry-based Edge
Functions metrics.
* Added coverage for status, execution time, function, and region
filters.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-05 17:13:34 +02:00

76 lines
3.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { METRIC_SQL_OTEL } from './edge-functions.config'
const sql = (fragment: { toString(): string }) => String(fragment)
describe('METRIC_SQL_OTEL', () => {
it('queries the single OTEL logs table by source, never a per-service table', () => {
const out = sql(METRIC_SQL_OTEL.TotalInvocations('1h'))
expect(out).toContain('from logs')
expect(out).toContain("source = 'function_edge_logs'")
expect(out).not.toContain('from function_edge_logs')
expect(out).not.toContain('cross join unnest')
})
it('emits 16-digit unix-microsecond timestamps bucketed by granularity', () => {
expect(sql(METRIC_SQL_OTEL.TotalInvocations('1h'))).toContain(
'toUnixTimestamp(toStartOfHour(timestamp)) * 1000000 as timestamp'
)
expect(sql(METRIC_SQL_OTEL.TotalInvocations('1d'))).toContain(
'toUnixTimestamp(toStartOfDay(timestamp)) * 1000000 as timestamp'
)
expect(sql(METRIC_SQL_OTEL.TotalInvocations('5m'))).toContain(
'toUnixTimestamp(toStartOfMinute(timestamp)) * 1000000 as timestamp'
)
})
it('reads function_id from log_attributes for total invocations', () => {
const out = sql(METRIC_SQL_OTEL.TotalInvocations('1h'))
expect(out).toContain("log_attributes['function_id'] as function_id")
expect(out).toContain('count() as count')
})
it('reads response status code from log_attributes, coerced to a number', () => {
const out = sql(METRIC_SQL_OTEL.ExecutionStatusCodes('1h'))
expect(out).toContain("toInt32OrZero(log_attributes['response.status_code']) as status_code")
})
it('reads region from the response headers attribute and drops empty values', () => {
const out = sql(METRIC_SQL_OTEL.InvocationsByRegion('1h'))
expect(out).toContain("log_attributes['response.headers.x_sb_edge_region'] as region")
expect(out).toContain("log_attributes['response.headers.x_sb_edge_region'] != ''")
})
it('averages execution_time_ms from log_attributes, coerced to a float', () => {
const out = sql(METRIC_SQL_OTEL.ExecutionTime('1h'))
expect(out).toContain(
"avg(toFloat64OrZero(log_attributes['execution_time_ms'])) as avg_execution_time"
)
})
it('filters by function_id list when functions filter is set', () => {
const out = sql(METRIC_SQL_OTEL.TotalInvocations('1h', { functions: ['fn-1', 'fn-2'] } as any))
expect(out).toContain("log_attributes['function_id'] IN ('fn-1','fn-2')")
})
it('applies the numeric status_code filter using toInt32OrZero', () => {
const out = sql(
METRIC_SQL_OTEL.ExecutionStatusCodes('1h', {
status_code: { operator: '>=', value: 500 },
} as any)
)
expect(out).toContain("AND toInt32OrZero(log_attributes['response.status_code']) >= 500")
})
it('applies the numeric execution_time filter using toFloat64OrZero', () => {
const out = sql(
METRIC_SQL_OTEL.ExecutionTime('1h', {
execution_time: { operator: '>=', value: 100 },
} as any)
)
expect(out).toContain("AND toFloat64OrZero(log_attributes['execution_time_ms']) >= 100")
})
})