feat(redisext,cachext): go-redis v6 补 OTel 追踪,与 APM 并存 - #744
Closed
guohuachan wants to merge 1 commit into
Closed
Conversation
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
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
配套 #743(那个解决 DB 侧)。这个 PR 解决 Redis 侧。
问题
extensions/redisext和extensions/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),因此两者能叠加:WrapProcess是c.process = fn(c.process)的组合式修改。安全前提(已在 v6.15.9 源码核实)
在共享的基础 client 上挂钩子会跨请求累积,是内存泄漏 + span 绑到过期 ctx。核实结论:
Client值嵌入baseClient,而process是baseClient的字段clone()是cp := *c值拷贝 → 副本上WrapProcess不影响原 clientinit()只做setProcessor(c.Process),不重置processTestWrapClientDoesNotLeakToBaseClient把这一点钉死了:连续包装 5 个副本后,用基础 client 执行命令必须零 span,且每请求副本恰好 1 个 span(没有重复挂钩)。span 约定
对齐
redisotel/v9,便于 v6 / v9 两条路径的数据在同一套看板里查询:命令名作 span 名、SpanKind=Client、db.system=redis、db.statement限 32 参数 × 每参数 64 字节截断(与rediscmd.AppendCmd同口径)、redis.Nil不记为错误。一处有意的差异:仅当 ctx 已有「有效且被采样」的 span 上下文时才产出 span,与
entext里otelsql的SpanFilter口径一致。理由是避免在无 trace 上下文时产生大量孤儿 root span —— Redis 调用频次远高于 SQL,这个量级差异不能忽略。接入点
各一处,覆盖完整:
extensions/redisext/ext.goClient(ctx)EvalLuaextensions/cachext/backend/redis/redis.gowithContext(ctx)apmgoredis那条链一行未动,APM 数据不受影响。测试
不依赖 Redis(
observability/redisotelv6/tracing_test.go,客户端指向死端口,命令必然失败但 span 照常产出):TestWrapClientEmitsSpan—— span 名 / kind /db.system/db.statement/ 错误状态TestWrapClientSkipsWithoutSampledContext—— 无采样上下文时零 spanTestWrapClientDoesNotLeakToBaseClient—— 上面那条安全前提TestAppendArgTruncates—— 截断口径TestCoexistsWithAPM—— 与apmgoredis叠加,一条命令两个 span:需要真实 Redis(
extensions/redisext/tracing_test.go,走完整的RedisExt.Client(ctx)):TestClientEmitsBothSpans—— 实测 APM 3 个 redis span,OTelset/get/del各 1 个四象限矩阵(
observability/...+redisext+redisext/v9+cachext,均 ok):外加全模块
go build ./.../go vet ./.../gofmt干净。合入前建议关注
sdktrace.NewBatchSpanProcessor(traceExporter)全默认参数(MaxQueueSize 2048),队列溢出时丢弃只在global.Debug里计数、业务侧无感。灰度时务必同步调大OTEL_BSP_MAX_QUEUE_SIZE并监控 dropped。db.statement会带上参数值(截断到 64 字节),即缓存 key 和 value 前 64 字节会进 trace。这是对齐 v9 的默认行为;若认为不合适,改WrapClient里semconv.DBStatement(cmdString(cmd))一处即可。Client(ctx)多两个闭包分配。现有的 apmgoredis 已有同样量级的开销,但 Redis QPS 高,值得跑个 benchmark 确认。tail_samplingdecision_wait 10s,扩副本前需先上loadbalancingexporter 做 trace ID 亲和。🤖 Generated with Claude Code
https://claude.ai/code/session_01T4eaUum99JDRLs5wrJETp8