px普贤教程PuXian
普贤教程 › 生态包

可观测与测试

日志、指标、统计、基准、测试。这一组的价值不在功能多,而在让「猜」变成「量」——本页所有输出都是本机编译产物跑出来的。

分级日志

px
import log

var lg = log_new("demo", {"level": "info"})
log_info(lg, "服务启动", {"port": 8080})
log_debug(lg, "这条不该出现")      # 被 level=info 过滤
text
2026-11-05 17:52:16 [INFO] demo: 服务启动 port=8080
在 Ma 的 px_routes 里不能用 print 打日志Ma 把脚本的 stdout 原样当 HTTP 响应,中间输出一行日志,客户端收到的是坏响应(Status: 行前面多了一行)。Web 脚本里要写 stderr:var f = open("/dev/stderr", "w") → write(f, msg + "\n") → close(f)。没有 eprint 内建。

Prometheus 指标

px
import metrics

var reg = m_registry()
m_counter(reg, "http_requests_total", "HTTP 请求数")
m_inc(reg, "http_requests_total", 3, {"path": "/api"})
m_gauge(reg, "queue_depth", "队列深度")
m_set(reg, "queue_depth", 7)
print(m_render(reg))
text
# HELP http_requests_total HTTP 请求数
# TYPE http_requests_total counter
http_requests_total{path=/api} 3.0
# HELP queue_depth 队列深度
# TYPE queue_depth gauge
queue_depth 7.0
发现一处缺陷:标签值没有加引号Prometheus 的文本格式要求标签值是带双引号的字符串({path="/api"})。上面渲染出来的 {path=/api} 会被 Prometheus 抓取端判为非法样本(text format parsing error)。在 0.2.0 里 m_label_key 直接拼 k=v,没走引号包装。用它对接真 Prometheus 前先自己 m_render 一次看清格式,或者只在内部看板上用。

描述统计

px
import statx

print(st_mean([1, 2, 3, 4]).ok())          # 2.5
print(st_median([1, 2, 3, 4]).ok())        # 2.5
print(st_stddev([1, 2, 3, 4], false).ok()) # 1.118033988749895
print(st_linreg([1, 2, 3], [2, 4, 6]).ok())# [2.0, 0.0, 1.0]  ← [斜率, 截距, r]

还有 st_quantile / st_pearson / st_mode / st_range。分析线上指标分布时,这些函数够用;做假设检验要另写。

行级 diff

px
import diff

print(df_lines("a\nb\nc", "a\nx\nc"))
# [ a, -b, +x,  c]        ← 空格=共有,-=仅左,+=仅右

print(df_ratio("hello world", "hello there"))   # 0.0
df_ratio 是「按行」的,不是按字符两句话只差一个词,但作为「一行」比较是 0% 相同。要字符级相似度用 edist 的 ed_ratio。这类「看起来该给 0.8、结果给了 0」的地方,先想清楚比较单位是行还是字符。

基准与测试

包用途典型写法
bench微基准:跑 N 次并统计bn_time(f, n) / bn_compare(fa, fb, n)
testkit断言与用例组织断言失败即计数,最后统一报
quickcheck属性测试:随机输入找反例「反转两次等于原串」这类性质
tdtest表驱动测试输入/期望表格批量跑
mockHTTP 桩服务见 Web 页

基准要跑编译轨

px
import bench

def work():
    var s = 0
    for i in range(0, 100):
        s += i
    return s

def main():
    print(str(bn_time(work, 1000)))                 # Ok({total_ns, ns_per_op, ops_per_sec})
    print(bn_format(bn_time_ms(work, 1000)))        # 跑 1000 次,格式化成 ms / μs / s
    print(str(bn_compare(work, work, 1000, "a", "b")))   # 两函数对比,ratio 是倍数
bn_time 返 Ok(统计),不是毫秒数要单个数字取 bn_time(work, 1000).ok()["ns_per_op"](纳秒/次);要能直接格式化的毫秒值用 bn_time_ms,它返 float,正好喂给 bn_format(自动选 μs / ms / s)。把 bn_time 的返回值直接交给 bn_format 会报 R1002: 无法比较: result vs float。
解释轨的基准数字没有意义同一段循环解释轨 5182–5928 ms、编译轨 10–12 ms(差约 500 倍)。基准要 px build 之后跑产物;解释轨测出来的只能是「解释器有多慢」。

观测的三层

层回答什么问题用什么
日志「这一件事发生了什么」log(或 stderr)
指标「整体现在什么水位」metrics + statx
追踪「这一请求慢在哪一段」打点时间戳手算(无内建 tracing 包)