Grafana Tempo 贡献者指南:从 PR 提交流程到 Go 编码规范与工具链实战
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
Grafana Tempo 是一款高吞吐、低依赖的分布式链路追踪后端(distributed tracing backend),仓库采用 Go 编写,围绕cmd/、modules/、pkg/、tempodb/等核心目录组织。本文基于仓库根目录的 CONTRIBUTING.md 编写,完整梳理向 Tempo 提交代码与文档的规范流程:AI 辅助贡献的披露与责任边界、Go 依赖管理(go get与make vendor-check)、项目目录结构、编码与可观测性规范、测试与 lint 工具链、PR 的签名提交与 changelog 条目要求、文档贡献流程,以及基于 docker-compose 的本地调试方法。读完本文,你将掌握一份可复制的开源贡献 S.O.P.,能够规范、高效地向 Tempo 提交第一个 PR。
贡献前的总体原则
Tempo 使用 GitHub 管理 PR 的评审,贡献入口非常简单:
- 如果是一个琐碎的修复或改进(trivial fix),直接创建 pull request 即可;
- 如果计划做更复杂的改动,请先在相关的 GitHub issue 上讨论你的想法,避免方向性返工。
在提交 PR 之前,必须完整阅读本指南——PR 检查清单也要求贡献者先通读全文。这保证了所有提交者(无论人类还是 AI 辅助)都遵循同一套质量与流程标准。
AI 辅助贡献:接受,但责任完全在提交者
Tempo 接受开发过程中使用了 AI 工具(如 GitHub Copilot、ChatGPT、Claude 等)的贡献,但明确声明:使用 AI 并不会降低门槛,而是把责任完全转移给了贡献者本人。
披露要求
如果 AI 工具生成了你所提交代码或文档的"实质性部分",必须在 PR 描述中说明,并指出使用了哪些工具、各用于什么目的。琐碎用途(自动补全、语法检查)无需披露;除此之外的实质使用都需要披露。
每一行都由你负责
通过提交 PR,你即认证了 Developer Certificate of Origin(DCO)——你有权在项目许可证下提交这些代码。AI 工具无法认证 DCO,无论代码如何生成,你都是记录在案(author of record)的作者。这意味着:
- 提交前要读懂每一行代码,如果无法解释它,就不要提交;
- 不允许由自主 Agent 在无人审查输出的情况下直接提交 PR;
- 不要用 AI 来写 PR 描述或 issue 评论——这些内容必须真实反映你对改动的理解。
质量与正确性自查清单
AI 生成的代码往往在特定方面出错,提交前需要逐项检查:
- 幻觉 API(Hallucinated APIs):调用了所使用库中并不存在的函数或方法;
- 伪造依赖(Fake dependencies):包名听起来合理但实际不存在,或未被代码库其他位置引入——必须核实每个新依赖真实存在且处于活跃维护状态;
- 错误的边界情况处理:AI 常产出"看起来正确"但错误路径或边界条件处理有误的代码;
- 风格漂移(Style drift):AI 输出通常不符合项目约定——提交前务必运行
make fmt和make lint并修复所有问题。
所有常规要求依然适用:测试、文档、changelog 条目、CI 通过。未经审查、浪费维护者时间的 AI 输出不算贡献。
许可与版权
AI 工具可能从训练数据中复现片段。如果使用的工具能标记与公开仓库的相似性(如 GitHub Copilot 的 code referencing),请启用该功能。若生成的片段疑似来自许可证不兼容的源码,请手动重写。
依赖管理:Go Modules 与 vendor-check
Tempo 使用 Go modules 声明go 1.26.5)以及 git。
新增或更新依赖使用go get命令:
# 选取最新的 tagged release go get example.com/some/module/pkg # 选取特定版本 go get example.com/some/module/pkg@vX.Y.Z提交前务必运行以下命令,验证所有依赖与 proto 定义的一致性:
make vendor-checkTempo 维护了仓库内的vendor/目录(可在仓库根目录看到),因此依赖变更后 vendor 一致性检查是 CI 与本地提交的关键一环。
项目结构:先看懂再动手
CONTRIBUTING.md 给出了 Tempo 的顶层目录结构,结合当前仓库可以这样理解:
cmd/ tempo/ - 主 tempo 二进制 tempo-cli/ - 用于直接检查后端 block 的 CLI 工具 tempo-vulture/ - "bird-themed" 一致性检查器(可选) tempo-query/ - jaeger-query GRPC 插件(Apache2 许可) docs/ example/ - 开始运行 Tempo 的最佳起点 docker-compose/ tk/ integration/ - e2e 测试 modules/ - Tempo 顶层组件 backend-worker/ backend-scheduler/ distributor/ overrides/ querier/ frontend/ storage/ opentelemetry-proto/ - git 子模块,proto vendoring 必需 operations/ - Tempo 部署与监控资源(Apache2 许可) jsonnet/ tempo-mixin/ pkg/ tempopb/ - 与各 Tempo 服务交互的 proto(Apache2 许可) tempodb/ - 对象存储 key/value 数据库 vendor/对照当前仓库,各目录均有大量实现:cmd/tempo/main.go、cmd/tempo-cli/main.go、cmd/tempo-vulture/main.go、cmd/tempo-query/main.go四个二进制入口齐全;modules/下除了文档列出的组件,还有distributor/、generator/、livestore/、blockbuilder/等活跃模块;docs/目录包含design-proposals/(设计提案)、internal/(内部流程内容与图表)与sources/(全部产品文档)。新贡献者可以据此快速定位"我改的东西属于哪一层"。
编码规范:Go 代码的标准姿势
Go imports 分组
imports 必须遵循"标准库、外部库、本地包"三段式格式:
import ( "context" "fmt" "github.com/gogo/protobuf/proto" "github.com/opentracing/opentracing-go" "github.com/grafana/tempo/modules/overrides" "github.com/grafana/tempo/pkg/validation" )错误处理
遵循标准 Go 错误处理模式:错误沿调用栈向上返回,不要吞掉;除真正不可恢复的场景外避免panic。
立即处理错误并提前返回——happy path 保持在正常缩进层级,不要嵌进else:
// good err := doSomething() if err != nil { level.Error(logger).Log("msg", "failed to do something", "err", err) return err } // happy path continues here at normal indentation // avoid err := doSomething() if err == nil { // happy path buried inside else } else { return err }用%w包裹错误并附上上下文:
return fmt.Errorf("failed to create tempodb: %w", err)始终附加一段简短上下文,描述当前函数正在做什么,从而构建可读的错误链,同时保留原始错误供后续检查。
在包级别定义哨兵错误(sentinel errors):
var ( ErrDoesNotExist = errors.New("does not exist") ErrEmptyTenantID = errors.New("empty tenant id") )使用errors.New创建哨兵值;当包外调用方需要检查时,以Err*形式导出。
用errors.Is和errors.As检查错误:
// 检查哨兵错误 if errors.Is(err, backend.ErrDoesNotExist) { ... } // 检查 context 取消 if errors.Is(err, context.Canceled) { ... } // 提取自定义错误类型 var parseErr *ParseError if errors.As(err, &parseErr) { ... }优先使用errors.Is/errors.As而非直接相等比较或字符串匹配——它们会遍历由%w包装形成的错误链。
为结构化错误数据定义自定义错误类型:当调用方需要检查错误字段(而不仅是身份)时,定义实现error接口的结构体;若类型包装了另一个错误,添加Unwrap()方法:
type ParseError struct { msg string line int col int } func (e *ParseError) Error() string { return fmt.Sprintf("parse error at line %d, col %d: %s", e.line, e.col, e.msg) }使用结构化 key-value 对记录错误:
level.Error(logger).Log("msg", "failed to flush block", "tenant", tenantID, "err", err) level.Warn(logger).Log("msg", "skipped span processing", "err", err)用level.Error表示意外失败,level.Warn表示预期或可恢复情况;始终以"err", err作为最后一对 key-value。对高频触发的错误使用限速日志器(log.NewRateLimitedLogger)。
defer 的使用
用defer将清理与获取配对——清理语句应紧跟在其所对应的调用之后,而不是放在函数末尾。
- 打开迭代器/读取器后立即关闭:
iter, err := block.Iterator() if err != nil { return err } defer iter.Close()- 加锁后立即解锁:
mu.Lock() defer mu.Unlock()- 创建 context 后立即取消:
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel()- 启动 span 后立即结束:
ctx, span := tracer.Start(ctx, "operationName") defer span.End()- 创建 ticker/timer 后立即停止:
ticker := time.NewTicker(interval) defer ticker.Stop()- 用匿名
defer函数处理条件化或依赖错误的清理。当清理逻辑依赖函数返回值或需要检查错误时,使用匿名函数。常见模式是仅在失败时记录 span 错误:
var err error defer func() { if err != nil { span.RecordError(err) } }()另一个常见用途是在长运行 goroutine 中从 panic 恢复:
defer func() { if r := recover(); r != nil { level.Error(logger).Log("msg", "recovered from panic", "err", r, "stack", string(debug.Stack())) err = errors.New("recovered from panic") } }()以及优雅关停——仅在启动中途失败时停止子服务:
defer func() { if err != nil && w.subservices != nil { if stopErr := services.StopManagerAndAwaitStopped(context.Background(), w.subservices); stopErr != nil { level.Error(logger).Log("msg", "failed to stop dependencies", "err", stopErr) } } }()接口实现断言
在文件顶部做编译期接口实现校验:
var _ SomeInterface = (*ConcreteType)(nil)可观测性(Instrumentation)
每个非平凡组件都应输出 metrics、logs 和 traces。新增功能时应从一开始就纳入可观测性,而不是事后补加:
- Metrics:Tempo 使用 Prometheus metrics(当前仓库
operations/tempo-mixin/下含 dashboards、alerts.jsonnet、rules.libsonnet 等监控资源)。 - Logs:Tempo 使用 go-kit log,以
key=value(logfmt)格式输出结构化日志。使用github.com/go-kit/log/level下的 level 函数,例如level.Info(logger).Log("msg", "started", "tenant", tenantID)。高频事件使用限速日志。 - Traces:Tempo 使用 OpenTelemetry 做追踪埋点。
测试:单元、本地与集成三级体系
Tempo 力求大部分功能都有充分测试:
- 单元测试:在隔离环境中测试代码功能。
*_test.go文件与被测代码放在一起;多输入场景优先使用t.Run()子测试的表驱动测试(table-driven tests)。 - 本地测试:使用 examples provided 中的
docker-compose、tanka或helm部署本地环境,验证新功能。 - 集成测试:端到端测试摄取与查询路径。这些测试位于 integration 目录——当前仓库下按领域拆分为
integration/api、integration/limits、integration/metrics-generator、integration/operations、integration/storage、integration/util等子包,对应 Makefile 中的make test-integration-*系列目标。
断言使用 testify 库(assert用于非致命检查,require用于致命检查)。
提交前运行测试:
make test查看覆盖率:
make test-with-coverCI 会在每个 PR 上运行这些测试。
格式化与 Lint
提交 PR 前运行 lint:
make lint只检查相对基分支的改动(大 PR 更快):
make lint base=main修复格式问题:
make fmt这要求gofumpt和goimports位于$PATH中。本项目使用gofumpt(gofmt的更严格超集)做格式化。可以按 gofumpt 文档 配置编辑器,或在提交前运行make fmt。
如果改动涉及 jsonnet 或 libsonnet 文件,还要运行:
make jsonnetfmt这要求jsonnetfmt二进制位于$PATH。
编译 jsonnet
编译 jsonnet 文件运行:
make jsonnet这要求jsonnet、jsonnet-bundler和tanka二进制位于$PATH。Tempo 的部署资源(operations/下的 jsonnet 与 libsonnet)依赖这套工具链。
代码生成:proto 与 TraceQL 语法
如果改动任何.proto文件,重新生成 Go 代码:
make gen-proto从 Makefile 的gen-proto目标可以看到,它先删除并重建opentelemetry-proto子模块,经 buf 中间目录修补后,使用buf/下的buf.gen.*.yaml模板分别生成 OpenTelemetry proto、Tempo proto(pkg/tempopb/tempo.proto、backendwork.proto)、backend proto(tempodb/backend/v1/v1.proto)与 frontend proto。
如果改动 TraceQL 语法(.y文件),重新生成解析器:
make gen-traceql对应目标使用goyacc -l -o pkg/traceql/expr.y.go pkg/traceql/expr.y生成解析器。
生成文件(*.pb.go、*.y.go、*.gen.go)不参与格式化与 lint——不要手工编辑它们。
Pull Request 全流程
签名提交是硬性要求
自 2026 年 6 月 22 日起,所有 Grafana Labs 仓库(包括 Tempo)要求签名提交。可参考提交签名验证说明以及检查签名状态。
注意:未签名的提交和 PR 会被拒绝并关闭,包括由 Agent 发起的 PR。
PR 描述
每个 PR 必须有清晰的描述,覆盖:
- 这个 PR 做了什么:总结改动内容及其动机;
- 修复了哪个(些)issue:使用
Fixes #<issue number>以便合并时自动关闭 issue。
PR 检查清单
在标记 PR 可评审(ready for review)之前确认:
- 为改动行为更新或新增了测试
- 新增或更新了文档(见"文档"一节)
- 在
.chloggen/下添加了 changelog 条目(见"Changelog 条目"一节)
提交消息语义化前缀
提交消息使用语义化前缀:
<type>: <short description>常见类型:
fix:— 缺陷修复feat:— 新特性enhancement:— 对现有功能的改进chore:— 维护、依赖更新、构建变更refactor:— 不改变行为地重构docs:— 仅文档
主题行保持简洁,前缀之后小写。示例:
fix: use counter instead of gauge for compactor deduped spans metric enhancement: deduplicate spans within traces during block builder chore(deps): update module google.golang.org/api to v0.267.0Changelog 条目(.chloggen)
所有改变行为的 PR(特性、增强、缺陷修复、破坏性变更)都必须包含 changelog 条目。仅依赖更新、纯文档变更、纯内部重构不需要(这类 PR 打Skip Changelog、dependencies或type/docs标签,或标题加chore:前缀)。
Tempo 使用 chloggen 管理CHANGELOG.md:不要直接编辑CHANGELOG.md,而是在.chloggen/下添加 YAML 文件,避免共享 changelog 上的合并冲突。当前仓库.chloggen/下已有大量条目文件与config.yaml、TEMPLATE.yaml、summary.tmpl等支撑文件。
创建条目:
make chlog-new # 以当前分支名命名文件 make chlog-new FILENAME=my-change # 可选:显式指定文件名文件名默认为当前分支名;在main/master或 detached HEAD 上必须传FILENAME=覆盖。编辑生成的.chloggen/<name>.yaml:
change_type: enhancement # breaking | change | feature | enhancement | bug_fix | security component: metrics-generator # 必须位于 .chloggen/config.yaml 的 components 白名单 note: Short description of the change. issues: [] # (可选)PR 编号;留空则发布时自动填充 subtext: # 可选的补充细节 user: <your-github-handle> # 渲染为 "(@your-github-handle)"change_type关键字对应渲染章节:breaking→ Breaking changes、change→ Changes、feature→ Features、enhancement→ Enhancements、bug_fix→ Bug fixes、security→ Security(具体章节标题可见.chloggen/config.yaml中的 change_types 定义)。
issues可选:留空时chlog-update会在发布时从添加该条目文件的提交中解析 PR 编号回填(详见.chloggen/README.md)。component必须在.chloggen/config.yaml的允许列表中,否则chlog-validate拒绝;引入新组件时要在同一 PR 中同步加入该列表。当前白名单覆盖distributor、querier、query-frontend、compactor、metrics-generator、block-builder、live-store、backend-scheduler、backend-worker、overrides、cache、storage、traceql、api、tempo、tempo-cli、tempo-query、tempo-vulture、operations、docs、deps。
写 note 时保持简短(至多一两句),聚焦用户影响,实现细节放subtext。例如对于 parquet 迭代器谓词下推的改动:
- ✅
Improve read performance by pushing down predicates to the parquet iterators. - ❌
Add support for pushdown predicates in the parquet iterators.
推送前校验与预览:
make chlog-validate make chlog-preview发布时(维护者操作):
make chlog-update VERSION=vX.Y.Z # 将条目汇总进 CHANGELOG.md 并删除条目文件保持 PR 同步
PR 与main失步时应rebase,不要 mergemain进分支。一旦 PR 收到评审意见,避免 force push(包括git push --force-with-lease)——重写历史会破坏 GitHub 的"自上次评审以来的变更"视图,迫使评审者重读整个 PR。应通过追加新提交回应评审意见。若确需 rebasemain解决冲突,请单独推送 rebase(不夹杂其他改动)并在 PR 评论中说明。这一点对 AI 编码 Agent 加倍适用——它们倾向于默认使用--force-with-lease。
文档贡献
任何人都可以参与 Tempo 文档:写新内容、更新现有内容或创建 issue。当前文档项目在 GitHub issues 中跟踪。
目录结构
Tempo 文档位于docs目录,包含三个子目录:
design-proposals:项目和功能提案,不随产品文档发布(当前仓库下有 2022-04 Parquet.md、2022-04 TraceQL Concepts.md、2023-11 TraceQL Metrics.md 等提案);internal:内部流程相关内容,包括图表;sources:全部产品文档所在地:helm-charts文件夹包含tempo-distributedHelm chart 的文档;tempo文件夹包含产品文档。
文档贡献方式
写作前可参考 Grafana Writer's Toolkit 获取高质量文档的编写指南与文档模板。创建文档 PR 时添加type/doc标签标识其为文档贡献。若内容需要合入之前的版本,为对应版本添加backport标签——PR 合并后该标签会触发自动流程创建额外 PR 将内容合入该版本分支。检查该 PR 中是否有不适用于该版本的内容(例如把 TraceQL 信息 backport 到 Tempo 1.5)。
本地预览文档
在仓库根目录运行:
make docs该命令使用grafana/docs镜像(内部用 Hugo 生成静态站点),站点运行于localhost:3002/docs/。make docs-test则执行文档生成测试。
注意:
make docs非常吃内存。若崩溃,请增加分配给 Docker 的内存后重试。
发布流程
Tempo 使用 CI action 将文档同步到 Grafana 网站,CI 在每次合入main的docs子目录改动时触发。helm-charts文件夹从 next 分支发布,Tempo 文档从latest分支发布。
调试
使用调试器有助于定位 Tempo 代码中的问题。仓库提供了 docker-compose 调试示例——该目录包含docker-compose.yaml、tempo.yaml配置与readme.md,演示如何在 docker-compose 内调试 Tempo(如配合 GoLand 远程调试)。
结语
从签名提交、AI 辅助贡献披露、make vendor-check依赖校验,到 import 三段式、%w错误链、defer配对清理、RED 指标与 logfmt 日志,再到.chloggen/的 YAML 条目与make docs预览——Tempo 的贡献规范把"高质量开源协作"落实成了一个个可执行的命令与可勾选的清单。对照 CONTRIBUTING.md 与本文逐项执行,你就能以维护者期望的方式,安全地把自己的第一个改动合入这个高吞吐分布式追踪后端。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考