news 2026/9/5 23:19:40

Supabase 日志查询实战:ClickHouse logs 表、log_attributes 映射与 BigQuery 迁移

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/supabase

Supabase 的日志体系已经从一个基于 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唯一日志标识符
timestampDateTime64(UTC)日志产生时间,可直接排序/比较
event_messageString原始日志行
severity_textString日志级别(若来源服务设置了)
sourceString日志来源服务,必须作为过滤条件
log_attributesMap(String, String)按来源服务的结构化字段,键为点分路径

timestamp的格式形如2026-06-22T09:34:06.215000(ISO 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_logssupavisor_logspgbouncer_logs— 字段较少,基本只有idtimestampevent_message

各来源实际设置的字段以 Logs Explorer 的Field Reference抽屉为准。当不确定某个键是否存在时,不要凭猜测,从真实数据中探测(见下文"键发现"一节)。

各来源常见的log_attributes键:

source常用键
edge_logsrequest.methodrequest.pathrequest.searchresponse.status_codeidentifier
postgres_logsparsed.error_severityparsed.detailparsed.hintparsed.queryidentifier
auth_logslevelstatuspathmsgerror
function_edge_logsresponse.status_coderequest.methodrequest.pathnamefunction_idexecution_idexecution_time_ms
function_logsevent_typefunction_idexecution_idlevel

读取 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

从真实数据中发现键名

与其猜测键名,不如读取最近行的mapKeys

select 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.ts:

const sql = safeSql`SELECT 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
映射键无(用 unnest)mapKeys(log_attributes)

还有一个硬性约束:logs.all.otel端点(以及其上的 Logs Explorer)拒绝count(*)select *,必须用count()并显式列出所需列。这是日志查询入口的限制,原生 ClickHouse 两者都支持。

最佳实践清单

日志表很大,无界扫描会读走远超需要的数据。保持查询正确且便宜的做法:

  • 以标识性注释开头(如-- errors since last deploy),在日志与评审中标记查询意图,方便区分同文件多条查询;
  • 永远带LIMIT,迭代阶段的聚合查询也不例外;
  • 永远from logs where source = '...'——不存在每服务独立的表,source过滤是正确性要求,不只是优化;
  • 时间窗口尽量收紧,小窗口返回更快;
  • 优先用真实列sourcetimestamp)过滤,再深入到log_attributes
  • order 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 50

Auth 错误:

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 查询时应当转换而不是直接执行

  1. logs表加source过滤替换原表。旧表名就是source值:from postgres_logs as t变成from logs where source = 'postgres_logs'。绝不要从每服务表名查询。例外:pg_cron 日志归在source = 'postgres_logs'下。
  2. 删除全部 unnest 连接——cross join unnest(metadata) as mcross join unnest(m.parsed) as pleft join unnest(...) on true等一律删除,它们在扁平映射里没有对应物。
  3. 把每个 unnest 别名列改写成映射取值。来自unnest(metadata)的字段变成log_attributes['field'];来自嵌套 struct(如unnest(m.parsed))的字段变成log_attributes['parsed.field']——保留 struct 名作为点分前缀,丢弃metadata根和所有别名。
  4. 数值字段包一层toInt32OrZero(...)再比较或聚合,因为映射值是字符串。
  5. 按对照表替换函数

全程保留原始查询的 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_severitylog_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 的约定执行。

安全 SQL:SafeLogSqlFragment 品牌类型

所有分析日志 SQL 必须是SafeLogSqlFragment,由 apps/studio/data/logs/safe-analytics-sql.ts 中的助手构建,并受 eslint 强制约束。从源码结构看,这套设计与 pg-meta 的SafeSqlFragment(Postgres 专用)刻意品牌隔离: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.alllogs.all.otel两个成员,新增端点需在此扩展。

沿用既有 OTEL 构建器

新增查询形态时,应镜像 apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts 中的生成器,而非自创风格。它们已经编码了全部约定:

  • genDefaultQueryOtelgenCountQueryOtelgenChartQueryOtelgenSingleLogQueryOtel— 行/计数/图表/单条日志构建器,选取真实列加按来源的log_attributes[...]取值,并别名为渲染层期望的叶子名;
  • mapOtelPreviewRowmapOtelSingleLogToLegacyotelTimestampToMicros— 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 23:18:36

600+ iTerm2 配色方案:终端配色新手指南

600 iTerm2 配色方案&#xff1a;终端配色新手指南 【免费下载链接】iTerm2-Color-Schemes Over 450 terminal color schemes/themes for iTerm/iTerm2. Includes ports to Terminal, Konsole, PuTTY, Xresources, XRDB, Remmina, Termite, XFCE, Tilda, FreeBSD VT, Terminato…

作者头像 李华
网站建设 2026/9/5 23:12:34

美颜相机相关功能的实现

简介&#xff1a;美颜相机功能&#xff0c;在创建界面的基础上&#xff0c;将系统的中的画笔对象传给监听器&#xff0c;在监听器中设置图片传入途径&#xff0c;通过画笔将其呈现在画板上&#xff0c;使用监听器创建美颜相机的各种功能&#xff0c;最终实现美颜相机的各种功能…

作者头像 李华
网站建设 2026/9/5 23:10:50

TLV LV-N370a东急道路清扫车模型全攻略:从开箱验货到场景收藏

这次我们来看一个比较少见的新品情报&#xff1a;TLV&#xff08;Tomica Limited Vintage&#xff09;在 8 月发售的 LV-N370a&#xff0c;东急道路清扫车。如果你平时玩的是乘用车、跑车题材的汽车模型&#xff0c;第一次听到“道路清扫车”进 TLV 可能觉得有点冷门。但实际在…

作者头像 李华
网站建设 2026/9/5 23:10:09

一次游戏版本更新的工程化拆解:新生物、改模与突变叠加

当看到“『索纳里亚世界』【26N8.8更新&#xff01;】”这样一个更新标题时&#xff0c;很多人的第一反应是&#xff1a;这又是哪款游戏发布了一个普通补丁&#xff1f;但对真正维护游戏项目、模组或长期世界观内容的人来说&#xff0c;这一行字里的信息量其实非常大。它同时暴…

作者头像 李华
网站建设 2026/9/5 23:09:43

二叉树-堆1

完美二叉树若像下图这样写当child为堆顶时&#xff0c;计算parent为0&#xff08;不会是-0.5&#xff0c;向上取整为0&#xff09;&#xff0c;while判断parent为0符合条件进入循环&#xff0c;此时if&#xff08;a[child]a[parent]&#xff09;,跳出循环。这只是程序能巧合运行…

作者头像 李华