diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts index a8bea182e51..d9f66bd51b0 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts @@ -124,29 +124,29 @@ describe('UnifiedLogs.queries (OTEL flat)', () => { }) describe('getLogsCountQuery', () => { - it('emits one UNION ALL branch per log_type bucket and per level', () => { + it('emits a single-scan arrayJoin explode over all dimensions (no UNION ALL)', () => { const sql = getLogsCountQuery(baseSearch) - // Per-log-type counts - for (const lt of ['edge', 'postgrest', 'storage', 'postgres', 'edge function', 'auth']) { - expect(sql).toContain(`'${lt}'`) - } - // Per-level counts - for (const lvl of ['success', 'warning', 'error']) { - expect(sql).toContain(`'${lvl}'`) - } - // Bundled via UNION ALL — multiple occurrences expected - expect(sql.match(/UNION ALL/g)?.length ?? 0).toBeGreaterThan(5) + // One multi-dimensional pass, not a stack of UNION ALL branches. + expect(sql).not.toContain('UNION ALL') + expect(sql).toContain( + `arrayJoin(['total','log_type','level','method','status','pathname'])` + ) + // multiIf picks each dimension's value off the exploded dimension alias. + expect(sql).toContain('multiIf(') + expect(sql).toContain(`dimension = 'total', 'all'`) + // Single FROM logs scan with a top-level GROUP BY and empty-bucket drop. + expect(sql.match(/FROM logs/g)?.length ?? 0).toBe(1) + expect(sql).toContain('GROUP BY dimension, value') + expect(sql).toContain(`HAVING value != ''`) }) - it('honours an active log_type filter in the total count branch', () => { + it('honours an active log_type filter in the shared WHERE', () => { const sql = getLogsCountQuery(withFilters('log_type:eq:edge')) - // The first branch is the total — its WHERE must include the edge - // log_type predicate, otherwise the total badge would over-count - // when a log_type filter is active. - const totalBranch = sql.split(/\bUNION ALL\b/)[0] - expect(totalBranch).toContain(`'total'`) - expect(totalBranch).toContain(`source = 'edge_logs'`) - expect(totalBranch).not.toContain(`source = 'postgres_logs'`) + // The single WHERE backs every dimension (including the total badge), so + // it must carry the edge log_type predicate and exclude postgres. + const where = sql.split(/\bWHERE\b/)[1] ?? '' + expect(where).toContain(`source = 'edge_logs'`) + expect(where).not.toContain(`source = 'postgres_logs'`) }) }) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts index 5d111ac7ec2..1f92d15114d 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts @@ -358,58 +358,54 @@ LIMIT ${lit(MAX_FACETS_QUANTITY)} } /** - * Bundled count query — UNION ALL of (dimension, value, count) rows so the - * frontend can render facet counts and total in one round trip. + * Bundled count query — single-scan `arrayJoin` "explode". + * + * Replaces the previous 13× UNION ALL with one pass over `logs`: `arrayJoin` + * fans each row out into one row per dimension, and `multiIf` picks that + * dimension's value (or '' to drop it via HAVING). Emits the same + * (dimension, value, count) shape the UNION ALL version did, so the consumer + * in `unified-logs-count-query.ts` is unchanged. + * + * Shape is dictated by what the Logflare OTEL endpoint tolerates (empirically + * verified): no CTEs (a `WITH` collides with the endpoint's own `WITH`), no + * GROUPING SETS, no window functions / `LIMIT BY`, no tuples / array indexing, + * no outer aggregation over a `FROM logs` subquery. A flat single-level + * arrayJoin + top-level GROUP BY is the only construct that yields a single + * multi-dimensional pass. + * + * Tradeoffs vs. the UNION ALL version (both inherent to a single scan): + * - One shared WHERE means we cannot exclude each facet's own filter from + * its own counts. Counts are conjunctive — an active filter on a dimension + * also narrows that dimension's buckets. + * - Per-dimension top-N can't be enforced (no `LIMIT BY` / window fns), so we + * bound the whole payload with a global ORDER BY + LIMIT instead. The + * `total` row is exempt so the total badge is never dropped. */ export const getLogsCountQuery = (search: QuerySearchParamsType): SafeLogSqlFragment => { - // When no predicates remain, fall back to `1` so we emit a valid - // tautology rather than a bare `WHERE`. - const baseFiltersFor = (excludeField?: string): SafeLogSqlFragment => { - const predicates = buildBaseWhere(search, excludeField) - return predicates.length > 0 ? joinSqlFragments(predicates, ' AND ') : safeSql`1` - } + const predicates = buildBaseWhere(search) - // The "total" badge should reflect the user's *current* filter set, - // including any active log_type filter. Pass no excludeField so the - // log_type predicate is included. - const totalSql = safeSql` -SELECT 'total' AS dimension, 'all' AS value, count() AS count + const MAX_ROWS = 200 + + return safeSql` +SELECT + arrayJoin(['total','log_type','level','method','status','pathname']) AS dimension, + multiIf( + dimension = 'total', 'all', + dimension = 'log_type', (${LOG_TYPE_EXPR}), + dimension = 'level', (${LEVEL_EXPR}), + dimension = 'method', ${ATTR.method}, + dimension = 'status', (${STATUS_EXPR}), + dimension = 'pathname', ${ATTR.path}, + '' + ) AS value, + count() AS count FROM logs -WHERE ${baseFiltersFor()} +${whereClause(predicates)} +GROUP BY dimension, value +HAVING value != '' +ORDER BY dimension = 'total' DESC, count DESC +LIMIT ${lit(MAX_ROWS)} ` - - const logTypeBranches = joinSqlFragments( - Object.entries(LOG_TYPE_PREDICATE).map( - ([logType, predicate]) => - safeSql` -SELECT 'log_type' AS dimension, ${lit(logType)} AS value, countIf(${predicate}) AS count -FROM logs -WHERE ${baseFiltersFor('log_type')} -` - ), - ' UNION ALL ' - ) - - const levelBranches = joinSqlFragments( - (['success', 'warning', 'error'] as const).map( - (lvl) => - safeSql` -SELECT 'level' AS dimension, ${lit(lvl)} AS value, countIf((${LEVEL_EXPR}) = ${lit(lvl)}) AS count -FROM logs -WHERE ${baseFiltersFor('level')} -` - ), - ' UNION ALL ' - ) - - const facetBranches = joinSqlFragments( - (['method', 'status', 'pathname'] as const).map( - (facet) => safeSql`(${getFacetCountQuery({ search, facet })})` - ), - ' UNION ALL ' - ) - - return joinSqlFragments([totalSql, logTypeBranches, levelBranches, facetBranches], ' UNION ALL ') } /**