openobserve 是一个 Rust 写的开源可观测性平台(日志、指标、追踪一体),性能好、资源占用低,我在用它承接日志与指标的过程中读了不少源码,前后定位了三个问题、提了三个 PR:#15144、#15143、#15142(两个已合并,一个经维护者说明后撤回)。写完回顾才发现,三个问题有一个共同的名字:边界条件。这篇文章把三个问题的完整过程记录下来。

问题一:Grafana 直方图不渲染(#15144)

现象

一条很常见的直方图查询:

SELECT histogram(_timestamp) AS x, count(*) AS y
FROM t GROUP BY x ORDER BY x LIMIT 100000

被系统判定 is_histogram_eligible=false,于是官方 Grafana 插件拒绝把它渲染成直方图。用户陷入两难:不加 LIMIT 会被默认 limit 截断结果;加了 LIMIT,直方图渲染又被禁用——死路。

根因:一票否决

定位到 is_eligible_for_histogram(src/config/src/utils/sql/eligible_for_histogram.rs):它对任何带 LIMIT 的查询直接判不合格,完全没有考虑这个查询是不是直方图查询。但对直方图来说,输出行数是由 histogram(_timestamp[, interval]) 决定的,LIMIT 在这里只是防止被默认 limit 上限截断,并不改变直方图语义。

方案

规则改成:只有不使用 histogram(...) 的查询才因 LIMIT 被否决。实现上是一个小访问器(HistogramDetector),在 AST 里检查是否存在名为 histogram 的 Expr::Function。这样既救回了 Grafana 插件的查询形态,又保住了原有意图——distinct/CTE/join/union/普通 LIMIT 仍然不是直方图,现有测试一个没动。

验证:Grafana 插件形态和 5 分钟间隔变体保持 eligible,SELECT * FROM t LIMIT 100 依旧 ineligible;cargo test -p config --lib 3857 个测试全过。改动 1 个文件,+51 −2。

→ PR #15144

结果:撤回。维护者说明,LIMIT 在这里被排除是有意为之——is_histogram_eligible 的含义不是「这条查询是不是直方图」,而是「Logs 页能否为这条查询绘制时间分布图」;那张图靠把查询重写为 SELECT histogram(_timestamp), count(*) … 得到(见 convert_to_histogram_query),所以它与 DISTINCT/CTE/JOIN/UNION 一样,排除 LIMIT 才是正确契约。一个反例一锤定音:SELECT histogram(_timestamp) AS x, * FROM t LIMIT 10——它带 histogram,却无法被重写成时间分布图。明白这一点后,我主动关闭了这个 PR。

问题二:_around 传 size=1,却返回 2000 行(#15143)

现象

上下文查询接口 POST /api/{org}/{stream}/_around?key=...&size=1,期望拿 1 行,实际每个方向返回约 1000 行(合计约 2000 行);而且 size=0、负数也静默成功。

根因:没有下限,0 触发「默认 limit」回退

src/api/search/src/search/around.rs 把用户传入的 around_size 对半分后直接传给两个子查询,没有下限保护。size=1 时对半得到 0,而下游搜索层把非正数 size 解释为「使用默认 limit(1000)」,两个子查询双双膨胀。负数同理静默通过。

方案:钳制 + 校验 + 顺手修一个溢出

抽出 around_subquery_sizes(around_size):

测试覆盖 0/-5 → 错误;1→(1,1), 2→(1,1), 3→(2,1), 10→(5,5),以及 i64::MAX 不溢出;cargo test -p openobserve-api-search --lib 226 全过。响应结构不变,唯一行为变化是非法 size<=0 从「静默给默认 limit」变成「400」——这是更正确的语义。这个 PR 还顺带关联修复了一个 issue。

→ PR #15143

问题三:quantile 遇到 +Inf 返回 NaN,而 Prometheus 返回 +Inf(#15142)

现象

对一个包含 +Inf 样本的序列执行 quantile(0.75, m):OpenObserve 返回 NaN,Prometheus 返回 +Inf。文档(直方图查询、告警)依赖的是 Prometheus 语义,结果对不上。

根因:插值公式在无穷边界上翻车

src/promql/src/common.rs 用的插值公式是:

lower + (upper - lower) * fraction

当边界是无穷时,Inf + (Inf - Inf) * w = Inf + NaN * w = NaN。而 Prometheus 用的是:

lower * (1 - fraction) + upper * fraction

同样的输入是 Inf * 0.5 + Inf * 0.5 = Inf——无穷正确传播。

方案:对齐 Prometheus 的公式

重写为 Prometheus 形式。有限值的结果逐位相同(只是分布项重排);只有边界为无穷时行为变化——从 NaN 变成正确传播 ±Inf。(索引恰好落在端点上、w==0 时 Inf*0 两种公式都会得到 NaN,该情况不变,与 Prometheus 一致。)

测试严格走 RED/GREEN:修复前 Some(NaN) vs 期望 Some(inf) 确认失败,修复后通过;有限输入逐位不变也有断言兜底。cargo test -p promql --lib 630 全过。改动 1 个文件,+22 −1。

→ PR #15142

三个问题的共同模式

回头看,三个问题本质上是同一件事的三种变形:

边界条件 bug 有个共同特点:主路径上一切正常,单元测试大概率全绿,只有在「恰好踩到边界」的真实数据上才炸。这也是它们值得专门提 PR 的原因——不修,下一个用户还会踩。

在 Rust 大项目里提 PR 的几点体会

写在最后

三个 PR 目前都在评审中,合并进展欢迎到仓库围观:

项目地址:github.com/openobserve/openobserve