Skip to content

feat(redisext,cachext): go-redis v6 补 OTel 追踪,与 APM 并存 - #744

Closed
guohuachan wants to merge 1 commit into
shanbay:masterfrom
guohuachan:feat/redisext-otel-coexist
Closed

feat(redisext,cachext): go-redis v6 补 OTel 追踪,与 APM 并存#744
guohuachan wants to merge 1 commit into
shanbay:masterfrom
guohuachan:feat/redisext-otel-coexist

Conversation

@guohuachan

Copy link
Copy Markdown

配套 #743(那个解决 DB 侧)。这个 PR 解决 Redis 侧。

问题

extensions/redisextextensions/cachext/backend/redis(都是 go-redis v6 版)只 import 了 go.elastic.co/apm/module/apmgoredis零 otel 引用。OTel 插桩只存在于 v9 变体里。

后果:OTel 侧完全看不到 Redis 操作。排查慢请求时,应用的 Server span 表现为没有子 span 的叶子节点,看不出时间耗在哪。

为什么不迁 go-redis v9

Elastic APM 没有 v9 模块 —— apmgoredis 只覆盖 v6/v7。迁到 redisext/v9 等于用 Redis 的 APM 数据换 OTel 数据,而 APM 目前不能关。

而且 redisext 用的是 go-redis v6.15.9(不是 v8),迁 v9 意味着每个命令都要加 ctx 参数 —— 单是 words-learning 一个服务就有 60 处调用点。

方案

新增 observability/redisotelv6,在 v6 上补 OTel,与 apmgoredis 并存

ctx 怎么传进去

v6 的命令方法不接收 context,唯一能拿到 ctx 的地方是 (*redis.Client).WithContext 返回的每请求副本。本包在该副本上用 WrapProcess / WrapProcessPipeline 挂钩子、闭包捕获 ctx —— 这与 apmgoredis 的做法完全一致(见其 contextClient.WithContext),因此两者能叠加:WrapProcessc.process = fn(c.process) 的组合式修改。

安全前提(已在 v6.15.9 源码核实)

在共享的基础 client 上挂钩子会跨请求累积,是内存泄漏 + span 绑到过期 ctx。核实结论:

  • Client 值嵌入 baseClient,而 processbaseClient 的字段
  • clone()cp := *c 值拷贝 → 副本上 WrapProcess 不影响原 client
  • init() 只做 setProcessor(c.Process)不重置 process

TestWrapClientDoesNotLeakToBaseClient 把这一点钉死了:连续包装 5 个副本后,用基础 client 执行命令必须零 span,且每请求副本恰好 1 个 span(没有重复挂钩)。

span 约定

对齐 redisotel/v9,便于 v6 / v9 两条路径的数据在同一套看板里查询:命令名作 span 名、SpanKind=Clientdb.system=redisdb.statement32 参数 × 每参数 64 字节截断(与 rediscmd.AppendCmd 同口径)、redis.Nil 不记为错误。

一处有意的差异:仅当 ctx 已有「有效且被采样」的 span 上下文时才产出 span,与 entextotelsqlSpanFilter 口径一致。理由是避免在无 trace 上下文时产生大量孤儿 root span —— Redis 调用频次远高于 SQL,这个量级差异不能忽略。

接入点

各一处,覆盖完整:

文件 位置 覆盖
extensions/redisext/ext.go Client(ctx) 所有 Redis 调用,含 EvalLua
extensions/cachext/backend/redis/redis.go withContext(ctx) 所有 Cache 操作

apmgoredis 那条链一行未动,APM 数据不受影响。

测试

不依赖 Redisobservability/redisotelv6/tracing_test.go,客户端指向死端口,命令必然失败但 span 照常产出):

  • TestWrapClientEmitsSpan —— span 名 / kind / db.system / db.statement / 错误状态
  • TestWrapClientSkipsWithoutSampledContext —— 无采样上下文时零 span
  • TestWrapClientDoesNotLeakToBaseClient —— 上面那条安全前提
  • TestAppendArgTruncates —— 截断口径
  • TestCoexistsWithAPM —— 与 apmgoredis 叠加,一条命令两个 span:
    APM  span: name="GET"  type="db"  subtype="redis"
    OTel span: name="get"  kind=client
    

需要真实 Redisextensions/redisext/tracing_test.go,走完整的 RedisExt.Client(ctx)):

  • TestClientEmitsBothSpans —— 实测 APM 3 个 redis span,OTel set/get/del 各 1 个

这个测试里有个坑值得记一笔:OTEL_ENABLE=truegobay 自身会在 CreateApp 里调 otel.SetTracerProvider,所以测试用的 TracerProvider 必须在 CreateApp 之后设置,否则会被覆盖。

四象限矩阵observability/... + redisext + redisext/v9 + cachext,均 ok):

组合 结果
APM=true OTEL=true ok
APM=true OTEL=false ok
APM=false OTEL=true ok
都不设 ok

外加全模块 go build ./... / go vet ./... / gofmt 干净。

合入前建议关注

  1. span 量会显著放大。Redis 调用频次远高于 SQL,这是比 feat(entext,redisext,cachext): APM 与 OTel 并存插桩,补齐 DB 与 Redis 链路 #743 更猛的放大器。gobay 用的是 sdktrace.NewBatchSpanProcessor(traceExporter) 全默认参数(MaxQueueSize 2048),队列溢出时丢弃只在 global.Debug 里计数、业务侧无感。灰度时务必同步调大 OTEL_BSP_MAX_QUEUE_SIZE 并监控 dropped。
  2. db.statement 会带上参数值(截断到 64 字节),即缓存 key 和 value 前 64 字节会进 trace。这是对齐 v9 的默认行为;若认为不合适,改 WrapClientsemconv.DBStatement(cmdString(cmd)) 一处即可。
  3. 每次 Client(ctx) 多两个闭包分配。现有的 apmgoredis 已有同样量级的开销,但 Redis QPS 高,值得跑个 benchmark 确认。
  4. collector 侧需要先扩容。otel-collector 目前单副本、tail_sampling decision_wait 10s,扩副本前需先上 loadbalancing exporter 做 trace ID 亲和。

🤖 Generated with Claude Code

https://claude.ai/code/session_01T4eaUum99JDRLs5wrJETp8

redisext 和 cachext 的 v6 后端只接了 apmgoredis、零 otel 代码,所以 OTel 侧
完全看不到 Redis 操作。排查慢请求时,应用 span 表现为没有子 span 的叶子节点。

不走「迁 go-redis v9」这条路:Elastic APM 没有 v9 模块(apmgoredis 只覆盖
v6/v7),迁过去等于用 Redis 的 APM 数据换 OTel 数据,而 APM 目前不能关。
何况 redisext 用的是 v6.15.9 不是 v8,迁 v9 要给每个命令加 ctx 参数,
调用点数量可观。

新增 observability/redisotelv6:

v6 的命令方法不接收 context,唯一能拿到 ctx 的地方是 (*redis.Client).WithContext
返回的每请求副本。本包在该副本上用 WrapProcess / WrapProcessPipeline 挂钩子、
闭包捕获 ctx —— 与 apmgoredis 的做法完全一致,因此两者能叠加在同一个副本上。

安全前提(已在 go-redis v6.15.9 源码核实):Client 值嵌入 baseClient,而 process
是 baseClient 的字段;clone() 是值拷贝、init() 不重置 process。所以在副本上挂钩子
既不会污染基础 client,也不会跨请求累积。TestWrapClientDoesNotLeakToBaseClient
把这一点钉死了。

span 名与属性对齐 redisotel/v9(命令名 / SpanKind=Client / db.system=redis /
db.statement 限 32 参数 × 64 字节截断 / redis.Nil 不记为错误),便于 v6 与 v9
两条路径的数据在同一套看板里查询。

一处有意的差异:仅当 ctx 已有「有效且被采样」的 span 上下文时才产出 span,
与 entext 里 otelsql 的 SpanFilter 口径一致。避免在无 trace 上下文时产生大量
孤儿 root span——Redis 调用频次远高于 SQL,这个量级差异不能忽略。

接入点各一处,覆盖完整:
- redisext.Client(ctx)          —— 所有 Redis 调用,含 EvalLua
- cachext redis backend 的 withContext(ctx) —— 所有 Cache 操作

apmgoredis 那条链一行未动,APM 数据不受影响。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4eaUum99JDRLs5wrJETp8
@guohuachan

Copy link
Copy Markdown
Author

已合并进 #743 —— 两处都是「APM 开着时 OTel 看不到」的同一类问题,放在一起便于统一测试与 review。本 PR 的全部改动(observability/redisotelv6 新包 + redisext.Client / cachext.withContext 两个接入点 + 5 个测试)已原样 cherry-pick 到 #743 的分支,commit 内容一致。

@guohuachan guohuachan closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant