Supabase 日志查询实战:ClickHouse logs 表、log_attributes 映射与 BigQuery 迁移
Supabase 日志查询实战ClickHouse logs 表、log_attributes 映射与 BigQuery 迁移【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabaseSupabase 的日志体系已经从一个基于 BigQuery 的每服务一张表模型演进为统一的 ClickHouselogs表由logs.all.otel分析端点对外提供服务。本指南基于仓库中的技能文档 .claude/skills/clickhouse-logs-queries/SKILL.md 及其配套参考资料展开覆盖三个核心能力在 Logs Explorer 或代码中编写规范的 ClickHouse 日志查询、正确读取log_attributes结构化字段、以及把旧版 BigQuerycross join unnest(metadata)日志查询机械地翻译为 ClickHouse 写法。读完本文你将能独立完成日志 SQL 的编写、评审并在 Studio 代码库中以安全的方式集成新的日志查询。整体模型一张 logs 表取代全部服务表旧模型中每个服务Postgres、Auth、Edge 等各有一张表结构化字段藏在嵌套的metadata列里读取时必须反复cross join unnest(...)。ClickHouse 模型将其折叠为单一事实表栈内每一行日志都是logs表中的一行用source列标记来源服务每个服务特有的结构化字段全部平铺进log_attributes这个Map(String, String)列以点分路径为键查询入口是logs.all.otel分析端点Logs Explorer 即构建在其上。logs表的真实列很少完整结构如下引自 SKILL.md列名类型说明idString唯一日志标识符timestampDateTime64UTC日志产生时间可直接排序/比较event_messageString原始日志行severity_textString日志级别若来源服务设置了sourceString日志来源服务必须作为过滤条件log_attributesMap(String, String)按来源服务的结构化字段键为点分路径timestamp的格式形如2026-06-22T09:34:06.215000ISO 8601微秒精度无尾部Z。在 Logs Explorer 中时间范围是选择器自动注入的所以手写的timestamp过滤很少需要。一条最小化但合格的查询形态是以注释开头标明查询意图、按source过滤、并且永远带limit-- recent edge requests select timestamp, event_message from logs where source edge_logs order by timestamp desc limit 100;source 取值日志来源清单source决定你查的是哪个服务。常见取值及语义edge_logs— API 网关的请求与响应postgres_logs— 数据库语句与错误pg_cron 的日志也归在这里这是迁移时一个易踩的例外;auth_logs— 认证与授权活动function_edge_logs— Edge Function 的请求与响应function_logs— Edge Function 内部的console输出storage_logs— 对象上传与下载活动realtime_logs— Realtime 客户端连接postgrest_logs、supavisor_logs、pgbouncer_logs— 字段较少基本只有id、timestamp、event_message。各来源实际设置的字段以 Logs Explorer 的Field Reference抽屉为准。当不确定某个键是否存在时不要凭猜测从真实数据中探测见下文键发现一节。各来源常见的log_attributes键source常用键edge_logsrequest.method、request.path、request.search、response.status_code、identifierpostgres_logsparsed.error_severity、parsed.detail、parsed.hint、parsed.query、identifierauth_logslevel、status、path、msg、errorfunction_edge_logsresponse.status_code、request.method、request.pathname、function_id、execution_id、execution_time_msfunction_logsevent_type、function_id、execution_id、level读取 log_attributes点分键名与类型规则用方括号取值键名保留完整前缀log_attributes是字符串到字符串的映射读取字段用方括号访问不再需要任何 unnest 连接select log_attributes[request.method] as method, log_attributes[request.path] as path, log_attributes[response.status_code] as status from logs where source edge_logs键名规则是迁移中最容易出错的地方BigQuery 里通过嵌套 struct 表达的路径metadata.request.method在 ClickHouse 中变成log_attributes[request.method]——丢弃metadata根但保留完整点分前缀。也就是说request.cf.country对应log_attributes[request.cf.country]而不是log_attributes[cf.country]。数值字段一律是字符串映射值永远是字符串。要比较或聚合数值字段用toInt32OrZero包裹——它对缺失或非数值的值返回0不会在部分数据上抛错select count() as server_errors from logs where source edge_logs and toInt32OrZero(log_attributes[response.status_code]) between 500 and 599从真实数据中发现键名与其猜测键名不如读取最近行的mapKeysselect arrayJoin(mapKeys(log_attributes)) as key, count() as n from logs where source postgres_logs group by key order by n desc limit 100;arrayJoin(mapKeys(...))把映射的键拆成每键一行从而可以按出现频率排序。Studio 代码库正是用这一招实现 Field Reference 抽屉并把这些真实键喂给 AI 改写功能的——对应实现在 apps/studio/data/logs/otel-log-keys-query.tsconst sql safeSqlSELECT arrayJoin(mapKeys(log_attributes)) AS key, count() AS n FROM logs WHERE source ${analyticsLiteral(source)} GROUP BY key ORDER BY n DESC LIMIT 500该模块以 7 天回看窗口查询LOOKBACK_HOURS 24 * 7并带 5 分钟staleTime的 React Query 缓存供订阅组件和提交时的queryClient.fetchQuery调用共享。这是小的、正确加品牌的 OTEL 查询的典范实现。ClickHouse 与 BigQuery 函数对照表最容易踩坑的函数替换如下需求BigQueryClickHouse计数count(*)count()正则匹配regexp_contains(x, p)match(x, p)子串匹配x like %p%x ilike %p%不区分大小写或like数值转换cast(x as int64)toInt32OrZero(x)读取时间戳cast(timestamp as datetime)直接用timestamp列映射键无用 unnestmapKeys(log_attributes)还有一个硬性约束logs.all.otel端点以及其上的 Logs Explorer拒绝count(*)和select *必须用count()并显式列出所需列。这是日志查询入口的限制原生 ClickHouse 两者都支持。最佳实践清单日志表很大无界扫描会读走远超需要的数据。保持查询正确且便宜的做法以标识性注释开头如-- errors since last deploy在日志与评审中标记查询意图方便区分同文件多条查询永远带LIMIT迭代阶段的聚合查询也不例外永远from logs where source ...——不存在每服务独立的表source过滤是正确性要求不只是优化时间窗口尽量收紧小窗口返回更快优先用真实列source、timestamp过滤再深入到log_attributesorder by timestamp desc让最新日志在前用count()而不是count(*)或select *。实战查询示例按状态码统计请求select toInt32OrZero(log_attributes[response.status_code]) as status, count() as count from logs where source edge_logs group by status order by count desc limit 50Auth 错误select timestamp, event_message, log_attributes[msg] as message from logs where source auth_logs and log_attributes[level] in (error, fatal) order by timestamp desc limit 100在原始消息中搜索如死锁select timestamp, event_message from logs where source postgres_logs and event_message ilike %deadlock% order by timestamp desc limit 100按严重级别聚合 Postgres 错误经典的 unnest 转映射写法select log_attributes[parsed.error_severity] as severity, count() as count from logs where source postgres_logs and log_attributes[parsed.error_severity] in (ERROR, FATAL, PANIC) group by severity order by count desc limit 100把存量 BigQuery 查询迁移到 ClickHouse参考资料 .claude/skills/clickhouse-logs-queries/references/bigquery-migration.md 给出了一套机械化的五步法用户贴来旧 BigQuery 查询时应当转换而不是直接执行用logs表加source过滤替换原表。旧表名就是source值from postgres_logs as t变成from logs where source postgres_logs。绝不要从每服务表名查询。例外pg_cron 日志归在source postgres_logs下。删除全部 unnest 连接——cross join unnest(metadata) as m、cross join unnest(m.parsed) as p、left join unnest(...) on true等一律删除它们在扁平映射里没有对应物。把每个 unnest 别名列改写成映射取值。来自unnest(metadata)的字段变成log_attributes[field]来自嵌套 struct如unnest(m.parsed)的字段变成log_attributes[parsed.field]——保留 struct 名作为点分前缀丢弃metadata根和所有别名。数值字段包一层toInt32OrZero(...)再比较或聚合因为映射值是字符串。按对照表替换函数。全程保留原始查询的 select 列表、过滤、group by、order by 与 limit 意图。完整迁移前后对照BigQuery 原文select count(t.timestamp) as count, p.error_severity from postgres_logs as t cross join unnest(metadata) as m cross join unnest(m.parsed) as p where p.error_severity in (ERROR, FATAL, PANIC) group by p.error_severity order by count desc limit 100;ClickHouse 结果select count() as count, log_attributes[parsed.error_severity] as error_severity from logs where source postgres_logs and log_attributes[parsed.error_severity] in (ERROR, FATAL, PANIC) group by log_attributes[parsed.error_severity] order by count desc limit 100变化点count(t.timestamp)变count()两条 unnest 连接消失p.error_severity变log_attributes[parsed.error_severity]from/where指向单表。最常见的转换错误是丢掉点分前缀——真实键是log_attributes[request.headers.x_real_ip]时写成了log_attributes[x_real_ip]或把request.cf.country写成cf.country。不确定时就用前文的arrayJoin(mapKeys(...))查询从数据中发现真实键再把旧嵌套路径逐一对应上去。Logs Explorer 也内置了Rewrite to ClickHouse动作用 AI 完成一次性转换适合在仪表盘里临时使用。在 Studio 代码库中集成日志查询当需要修改构建或执行日志查询的 TypeScript而不仅是写 UI 查询时按 references/codebase-integration.md 的约定执行。安全 SQLSafeLogSqlFragment 品牌类型所有分析日志 SQL 必须是SafeLogSqlFragment由 apps/studio/data/logs/safe-analytics-sql.ts 中的助手构建并受 eslint 强制约束。从源码结构看这套设计与 pg-meta 的SafeSqlFragmentPostgres 专用刻意品牌隔离Postgres 安全的转义E…字符串、::jsonb转换、双引号标识符对 BigQuery/ClickHouse 不安全反之亦然两个品牌不可互相提升防止跨引擎误拼接出危险 SQL。核心导出safeSql— 标签模板只接受SafeLogSqlFragment插值普通字符串和 Postgres 品牌类型在编译期被拒绝analyticsLiteral(value)— 把 string/number/boolean 变成安全转义的 литерал 片段单引号与反斜杠按 ClickHouse/BigQuery 共同约定转义与\\所有动态值尤其是source都必须走它joinSqlFragments(fragments, separator)— 以固定的结构分隔符 and 、, 等拼接已安全的片段keyword(value, allowed)— 按编译期允许清单解析值如AND/OR运算符只返回清单内片段绝不返回原始输入quotedIdent(value)— 逐段校验[A-Za-z_][A-Za-z0-9_]*后对点分标识符路径加反引号例如request.method变为request.method。该文件刻意不导出任何 raw 逃生口。典型用法import { analyticsLiteral, safeSql } from data/logs/safe-analytics-sql const source edge_logs const sql safeSql select timestamp, event_message from logs where source ${analyticsLiteral(source)} order by timestamp desc limit 100 同一文件还定义了用户输入的信任边界编辑器里的用户 SQL 以untrustedLogSql()标记为UntrustedLogSqlFragment只允许展示与保存只有acceptUntrustedLogsSql()安全边界能把它提升为可执行的SafeLogSqlFragment而源码注释明确要求只能在用户明确触发运行的事件处理器中调用绝不能在 render 或 useEffect 中调用。按特性开关选择端点与构建器ClickHouse 路径由 PostHog 特性开关otelLegacyLogs门控useFlag(otelLegacyLogs)开关关闭时必须保持 BigQuery 路径可用。apps/studio/data/logs/logs-endpoint.ts 中的两个助手表达了这个分叉export const logsAllEndpointUrl (useOtel: boolean) useOtel ? (/platform/projects/{ref}/analytics/endpoints/logs.all.otel as const) : (/platform/projects/{ref}/analytics/endpoints/logs.all as const) export const pickLogsQueryBuilder T(useOtel: boolean, otel: T, bq: T): T useOtel ? otel : bq使用方式const useOtel useFlag(otelLegacyLogs) const builder pickLogsQueryBuilder(useOtel, genDefaultQueryOtel, genDefaultQuery) const endpoint logsAllEndpointUrl(useOtel) // React Query key 中包含 { otel: useOtel }让两条路径各自缓存随后把片段交给 apps/studio/data/logs/execute-analytics-sql.ts 的executeAnalyticsSql执行。该函数是分析路径的线上边界只接受SafeLogSqlFragment普通字符串编译期被拒请求体携带{ sql, iso_timestamp_start, iso_timestamp_end }默认 POST兼容迁移期遗留的 GET 调用方。端点联合类型AnalyticsSqlEndpoint目前只包含logs.all与logs.all.otel两个成员新增端点需在此扩展。沿用既有 OTEL 构建器新增查询形态时应镜像 apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts 中的生成器而非自创风格。它们已经编码了全部约定genDefaultQueryOtel、genCountQueryOtel、genChartQueryOtel、genSingleLogQueryOtel— 行/计数/图表/单条日志构建器选取真实列加按来源的log_attributes[...]取值并别名为渲染层期望的叶子名mapOtelPreviewRow、mapOtelSingleLogToLegacy、otelTimestampToMicros— JS 归一化层。由于分页游标与渲染器要求timestamp是微秒数字应复用otelTimestampToMicros而不是自己解析 ISO 字符串。集成检查清单每个动态值都经过analyticsLiteral或其他净化助手绝不字符串拼接查询按source过滤且包含LIMIT数值型log_attributes值包了toInt32OrZero端点与构建器基于useFlag(otelLegacyLogs)经logsAllEndpointUrl/pickLogsQueryBuilder选择React Query key 区分 OTEL 与 BigQuery 两条路径表格/游标消费的行timestamp已归一化为微秒存在断言生成 SQL 字符串的单测可参考Logs.utils.otel.test.ts与 apps/studio/data/logs/safe-analytics-sql.test.ts 的模式。小结ClickHouse 日志模型的核心可以浓缩为三句话一张logs表、source列分服务、log_attributes扁平映射承载一切结构化字段。掌握方括号取值 完整点分前缀 toInt32OrZero数值转换三个要点后BigQuery 的 unnest 查询转换就是机械劳动而在 Studio 代码中集成查询时SafeLogSqlFragment品牌类型、otelLegacyLogs开关与既有 OTEL 构建器则保证了安全性、双引擎兼容与风格一致。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考