news 2026/9/17 13:48:36

Grafana Tempo 贡献者指南:从 PR 提交流程到 Go 编码规范与工具链实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Grafana Tempo 贡献者指南:从 PR 提交流程到 Go 编码规范与工具链实战

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 getmake 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 fmtmake 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-check

Tempo 维护了仓库内的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.gocmd/tempo-cli/main.gocmd/tempo-vulture/main.gocmd/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.Iserrors.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-composetankahelm部署本地环境,验证新功能。
  • 集成测试:端到端测试摄取与查询路径。这些测试位于 integration 目录——当前仓库下按领域拆分为integration/apiintegration/limitsintegration/metrics-generatorintegration/operationsintegration/storageintegration/util等子包,对应 Makefile 中的make test-integration-*系列目标。

断言使用 testify 库(assert用于非致命检查,require用于致命检查)。

提交前运行测试:

make test

查看覆盖率:

make test-with-cover

CI 会在每个 PR 上运行这些测试。

格式化与 Lint

提交 PR 前运行 lint:

make lint

只检查相对基分支的改动(大 PR 更快):

make lint base=main

修复格式问题:

make fmt

这要求gofumptgoimports位于$PATH中。本项目使用gofumptgofmt的更严格超集)做格式化。可以按 gofumpt 文档 配置编辑器,或在提交前运行make fmt

如果改动涉及 jsonnet 或 libsonnet 文件,还要运行:

make jsonnetfmt

这要求jsonnetfmt二进制位于$PATH

编译 jsonnet

编译 jsonnet 文件运行:

make jsonnet

这要求jsonnetjsonnet-bundlertanka二进制位于$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.protobackendwork.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.0

Changelog 条目(.chloggen)

所有改变行为的 PR(特性、增强、缺陷修复、破坏性变更)都必须包含 changelog 条目。仅依赖更新、纯文档变更、纯内部重构不需要(这类 PR 打Skip Changelogdependenciestype/docs标签,或标题加chore:前缀)。

Tempo 使用 chloggen 管理CHANGELOG.md不要直接编辑CHANGELOG.md,而是在.chloggen/下添加 YAML 文件,避免共享 changelog 上的合并冲突。当前仓库.chloggen/下已有大量条目文件与config.yamlTEMPLATE.yamlsummary.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 中同步加入该列表。当前白名单覆盖distributorquerierquery-frontendcompactormetrics-generatorblock-builderlive-storebackend-schedulerbackend-workeroverridescachestoragetraceqlapitempotempo-clitempo-querytempo-vultureoperationsdocsdeps

写 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 在每次合入maindocs子目录改动时触发。helm-charts文件夹从 next 分支发布,Tempo 文档从latest分支发布。

调试

使用调试器有助于定位 Tempo 代码中的问题。仓库提供了 docker-compose 调试示例——该目录包含docker-compose.yamltempo.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 13:47:38

RTOS优先级反转原理与GD32F103实战排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 13:46:14

心跳失控?把 OpenClaw 的模型通道改到 TaoToken 再盯 Token 黑洞

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 13:46:12

学生编程入门选 TRAE 做课设,模型通道改到 TaoToken 通道行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 13:46:08

API接口调用实战:从400报错排查到JSON Schema、鉴权与重试幂等

上周有个做后端的朋友甩过来一段代码&#xff0c;说照着官方示例改的&#xff0c;"API接口调用"死活跑不通&#xff0c;返回一串 400&#xff0c;报错里还带着一段长得像天书的正则。我让他把完整的请求体贴给我看&#xff0c;五分钟就定位到了问题——不是密钥错了&…

作者头像 李华
网站建设 2026/9/17 13:45:03

杰理AW33N系列蓝牙芯片选型实战指南:烧录、OTA与温漂避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华