首页
/ Supabase Studio 日志查询开发指南:SafeLogSqlFragment 品牌化 SQL 与 OTEL/BigQuery 双路径接入规范

Supabase Studio 日志查询开发指南:SafeLogSqlFragment 品牌化 SQL 与 OTEL/BigQuery 双路径接入规范

2026-09-04 21:35:48作者:袁立春Spencer

本篇指南面向需要为 Supabase Studio 编写分析日志 SQL 的 TypeScript 开发者,系统讲解如何把日志查询写得安全(品牌化防注入)、路由正确(按 PostHog 特性开关选择 ClickHouse OTEL 或 BigQuery 端点)、风格一致(沿用既有 OTEL builder 约定)。读完后,你将掌握 SafeLogSqlFragment 的完整 API 与逃生边界、logsAllEndpointUrl / pickLogsQueryBuilder 的端点选择机制,以及 otelTimestampToMicros 等 JS 归一化层的用法,能够独立完成一条符合团队规范并可通过单测验证的新日志查询。

适用场景:什么时候该遵循这套规范

这套规则约束的是apps/studio 中构建或执行日志查询的 TypeScript 代码,而不仅仅是在 Logs Explorer UI 里手敲一条查询。判断标准是:只要你的改动涉及“用代码拼 SQL 并发送请求”,就必须遵循本文全部约定;只是在 UI 中为用户编写查询文本,则适用另外一组规则(用户文本走 untrustedLogSql 标记,见下文“用户手写 SQL 的晋升边界”)。

品牌化 SQL:SafeLogSqlFragment 与防注入模型

为什么 analytics SQL 也有注入风险

分析日志查询(云上 legacy 走 BigQuery,自托管 OTEL 走 ClickHouse)与 Postgres 查询承担同样的注入风险:过滤器 key、value 以及其他 SQL 片段可能来自 URL 参数、UI 输入甚至 LLM 输出,再被拼进代表项目执行的 SQL。因此 apps/studio/data/logs/safe-analytics-sql.ts 采用了一套与 pg-meta safe-SQL 同构的“proven authorship”模型:每个来自外部的值必须先经过净化 helper 再被插值,而线上边界(executeAnalyticsSql)在编译期就拒绝普通字符串

从源码结构看,该文件与 pg-meta 的 SafeSqlFragment 品牌刻意保持隔离(safe-analytics-sql.ts#L52):Postgres 的转义方式(E'…' 字符串、::jsonb 强制转换、双引号标识符)对 BigQuery/ClickHouse 并不安全——比如 BigQuery 中双引号包裹的是字符串字面量而非标识符。两个品牌互不相通,就杜绝了“Postgres 转义过的片段被误拼进分析查询(或反过来)后静默产出不安全 SQL”的可能。

文件头部注释还说明了两引擎共享的转义约定,这解释了为什么同一套 helper 能同时服务 BigQuery 与 ClickHouse:

  • 字符串字面量:ClickHouse 与 BigQuery 约定一致——单引号翻倍('')、反斜杠翻倍(\\),外层用普通 '…' 定界;
  • 标识符:BigQuery 要求反引号,ClickHouse 两者都接受,实现中统一使用反引号形式(safe-analytics-sql.ts#L181-L196)。两种引擎下引号内的反斜杠都是转义字符,所以代码直接拒绝任何不符合 [A-Za-z_][A-Za-z0-9_]* 的输入,而不是尝试转义——列名实际上从不包含特殊字符。

核心导出 API 全解

所有 analytics 日志 SQL 都必须是 SafeLogSqlFragment,且必须用 apps/studio/data/logs/safe-analytics-sql.ts 中的 helper 构建。这一点由 eslint 强制检查,而品牌机制保证被插值的值不会演变成注入。关键导出如下:

导出 签名与行为 源码依据
safeSql\...`` 标签模板,仅接受 SafeLogSqlFragment 插值;普通字符串(以及 Postgres 品牌的 SafeSqlFragment)在编译期被拒绝,因此不可能“顺手”塞入一个原始值 safe-analytics-sql.ts#L110-L118
analyticsLiteral(value) 将 string/number/boolean 转换为安全转义的字符串字面量片段(单引号与反斜杠均被转义)。所有动态值(尤其 source)都必须走它。数字要求有限值,非有限值直接抛错;布尔转为 true/false 片段 safe-analytics-sql.ts#L138-L158
joinSqlFragments(fragments, separator) 固定的结构性分隔符(限定在 ','', '' and '' AND '' or '' OR '' union '' UNION '';\n''\n'' ' 等联合类型内)拼接已安全的片段 safe-analytics-sql.ts#L89-L136
keyword(value, allowed) 将输入值与编译期允许清单(一组已品牌化的片段)做大小写不敏感匹配,返回清单内的片段、绝不返回原始输入;匹配失败则抛错。典型用法:keyword(op, [safeSql\AND`, safeSql`OR`])` safe-analytics-sql.ts#L170-L179
quotedIdent(value) 对点分标识符路径逐段校验 [A-Za-z_][A-Za-z0-9_]* 后,每段加反引号。例如 quotedIdent('request.method')`request`.`method`。反引号两种引擎都接受,因此同时服务 BigQuery 与 ClickHouse safe-analytics-sql.ts#L181-L196

需要强调:这里刻意没有导出任何 “raw” 逃生口。源码中确实存在一个 rawSql 函数(safe-analytics-sql.ts#L126-L128),但它是内部专用的品牌化工具、未导出——外部调用者只能通过 safeSql 加上述净化 helper 组合,绝不能把任意字符串强转为品牌类型。

文档给出的最小可用示例:

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
`

真实代码中还可以看到更完整的组合方式。Logs.utils.otel.ts 顶部就定义了两个高频组合器:attr(key)safeSql\log_attributes[${lit(key)}]` 构造带转义的 map 取值,statusAsInt(key)再包一层toInt32OrZero(...)——因为 log_attributes` 里所有值都是字符串,数值字段必须显式转整型才能参与比较(Logs.utils.otel.ts#L18-L19)。

用户手写 SQL 的晋升边界

除了程序生成的 SQL,还有第二类来源:用户在编辑器里写的日志 SQL。类型系统用另一个独立品牌 UntrustedLogSqlFragment 承接它(safe-analytics-sql.ts#L65-L75),并规定唯一的晋升通道 acceptUntrustedLogsSql 只能出现在绑定了明确用户动作的事件处理器里(Run 按钮的 onClick、Cmd+Enter 的 keydown)——绝不能在 render、useEffect 或任何没有用户手势的路径上调用(safe-analytics-sql.ts#L77-L87)。该函数是“安全边界”:未经晋升的不可信 SQL 可以展示、可以存为编辑器工作文本,但永远不可执行。

按特性开关选择端点与 builder

ClickHouse 路径由 PostHog 特性开关 otelLegacyLogs 门控(通过 common 包的 useFlag('otelLegacyLogs') 读取,useLogsSqlExecution.ts 中可见实际用法),且要求开关关闭时 BigQuery 路径继续可用。apps/studio/data/logs/logs-endpoint.ts 里两个 helper 表达了这个分叉(logs-endpoint.ts):

  • logsAllEndpointUrl(useOtel)useOtel 为真时返回 logs.all.otel 端点,否则返回 legacy 的 logs.all 端点:

    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)
    
  • pickLogsQueryBuilder(useOtel, otel, bq) — 在 OTEL builder 与 BigQuery builder 之间做选择,并且用泛型 T 保留输入类型,让调用方维持原有签名:

    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 },让两条路径各自独立缓存

生成的片段最终通过 executeAnalyticsSqlexecute-analytics-sql.ts)发往对应端点。它是分析路径的“线上边界”:参数 sql 必须携带 SafeLogSqlFragment 品牌,普通字符串在编译期被拒绝。它的完整参数面比文档描述的更丰富,值得逐条了解(execute-analytics-sql.ts#L23-L47):

  • projectRef / endpoint — 端点被限定为 AnalyticsSqlEndpoint 联合类型(当前只有 logs.alllogs.all.otel 两个成员,后续端点迁移到该模式时扩展这个联合即可);
  • iso_timestamp_start / iso_timestamp_end — ISO 时间范围,随 sql 一起进 POST body 或 GET query string;
  • method — 默认 'post';迁移 legacy GET 调用者时可传 'get' 以保持线上行为;
  • key — 可选的 query-string 键,仅用于网络工具(DevTools)识别,不属于 OpenAPI schema;
  • signal / headers — 标准的取消与请求头透传。

沿用既有 OTEL builder:查询形状与归一化约定

需要新的查询形状时,应模仿 Logs.utils.otel.ts 中的生成器,而不是另起炉灶。这些生成器已经固化了团队约定,核心导出包括:

  • 四个查询 builder
    • genDefaultQueryOtel(table, filters, limit = 100)L262-L273)— 行数据预览:选择真实列,加上按 source 的 log_attributes[...] 查找并别名到渲染器期望的叶子名;
    • genCountQueryOtelL275-L278)— 计数;
    • genChartQueryOtelL291-L311)— 按时间桶(12 小时内按分钟、72 小时内按小时、更长按天,对齐 BigQuery 行为)输出 ok_count / error_count / warning_count
    • genSingleLogQueryOtel(id)L313-L323)— 按 id 取单条日志,入口先用正则拒绝非 uuid 形态的 id(注入防护)。
  • JS 归一化层mapOtelPreviewRowmapOtelSingleLogToLegacyotelTimestampToMicros。OTEL 行的 timestamp 是 ISO 字符串,而分页游标与渲染器要求微秒数字,因此必须复用 otelTimestampToMicros(实现是 parseOtelTimestamp(timestamp).getTime() * 1000L325-L327),不要自己解析 ISO 字符串。mapOtelSingleLogToLegacy 还会用 zod 校验行形状(OtelLogRowSchemaL377-L382),并按查询类型从扁平的 log_attributes 重建出详情面板期望的嵌套 metadata 结构。

OTEL_SOURCES 描述符:每表的 source、列与过滤器约定

builder 背后是一张 OTEL_SOURCES: Record<LogsTableName, OtelSourceDescriptor> 表(L126-L219),它声明了每张日志表的:

  • source 值——即 WHERE source = ... 过滤用的字面量,如 EDGE → 'edge_logs'、POSTGRES 与 PG_CRON → 'postgres_logs'(pg_cron 额外挂 baseCondition 子集条件)、FUNCTIONS → 'function_logs'、FN_EDGE → 'function_edge_logs'、AUTH → 'auth_logs'、MULTIGRES → 'multigres_logs'、POSTGREST → 'postgrest_logs'、SUPAVISOR → 'supavisor_logs'、ETL → 'etl_replication_logs',以及 auth_audit_logsrealtime_logsstorage_logspgbouncer_logspg_upgrade_logs 等;
  • columns — 除 id/timestamp/event_message 之外按 source 选择的列,统一通过 col(key, alias)log_attributes 取出并别名;
  • filterTemplates — 每表可用的过滤器模板(severity、status_code、method、product 等),未列出的 key 落到兜底 resolveUnknownOtelClause,即 log_attributes[key] = lit(value),对非标量输入直接丢弃该子句;
  • error / warning — 图表的严重度分类条件,各表共享如 HTTP_ERRORtoInt32OrZero(log_attributes['response.status_code']) >= 500)、PG_ERRORparsed.error_severity IN ('ERROR','FATAL','PANIC'))等预定义片段,保证同一行在图表与过滤器中分类一致。

写 WHERE 子句时,genOtelWhere 的模式是标准答案:先放 source = lit(desc.source),再按需追加 baseCondition,然后展开 buildWhereClauses 产出的各过滤子句,最后用 joinSqlFragments(conditions, ' AND ') 拼接(L237-L250)。

规范样例:map-key 发现查询

apps/studio/data/logs/otel-log-keys-query.ts 是“小而正确”的 OTEL 查询的标准范例——写新查询前先读它。fetchOtelLogKeysotel-log-keys-query.ts#L12-L35)展示了一次完整调用链:

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`
const data = await executeAnalyticsSql({
  projectRef,
  endpoint: logsAllEndpointUrl(true),
  sql,
  iso_timestamp_start: start.toISOString(),
  iso_timestamp_end: end.toISOString(),
  method: 'post',
  signal,
})

它同时体现了检查单上的多条要求:动态的 sourceanalyticsLiteral;查询按 source 过滤;带 LIMIT 500;时间窗口固定回看 7 天(LOOKBACK_HOURS = 24 * 7)。此外,otelLogKeysQueryOptionsuseOtelLogKeysQuery 把查询选项抽成共享工厂,让响应式 hook 与命令式 queryClient.fetchQuery 调用方复用同一份缓存(staleTime 5 分钟,key 走 logsKeys.otelLogKeys(projectRef, source))。

完成前检查单

提交新日志查询前,逐条核对(该检查单继承自 codebase-integration.md):

  • [ ] 每个动态值都经过 analyticsLiteral(或其他净化手段),绝不字符串拼接;
  • [ ] 查询按 source 过滤,且包含 LIMIT
  • [ ] 数值型 log_attributes 值都包在 toInt32OrZero 中(log_attributes 值全部是字符串,数值字面量比较会触发 ClickHouse 类型错误);
  • [ ] 端点与 builder 通过 logsAllEndpointUrl / pickLogsQueryBuilder 基于 useFlag('otelLegacyLogs') 选择;
  • [ ] React Query key 能区分 OTEL 与 BigQuery 两条路径(例如包含 { otel: useOtel });
  • [ ] 凡是被表格/分页游标消费的行,timestamp 都已归一化为微秒(复用 otelTimestampToMicros);
  • [ ] 存在一个断言“生成 SQL 字符串”的单测——可参考 Logs.utils.otel.test.tssafe-analytics-sql.test.ts 中的既有测试模式。

延伸阅读路径

关注点 文件
品牌化 SQL helper 全量实现 apps/studio/data/logs/safe-analytics-sql.ts
端点选择与 builder picker apps/studio/data/logs/logs-endpoint.ts
线上边界 executeAnalyticsSql apps/studio/data/logs/execute-analytics-sql.ts
OTEL 查询 builder 与归一化层 apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts
规范小查询范例 apps/studio/data/logs/otel-log-keys-query.ts
特性开关的实际消费方 apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.ts
生成的 SQL 字符串单测 apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.test.tsapps/studio/data/logs/safe-analytics-sql.test.ts
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384