
Grafana Tempo 中的 OpenTelemetry Collector 中间件扩展 APIHTTP 与 gRPC 拦截机制的源码级解析【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo本技术指南以 Grafana Tempo 仓库中 vendor 的 OpenTelemetry Collectorextensionmiddleware包文档为核心系统讲解 Collector 中间件扩展Middleware Extension如何注入 HTTP 与 gRPC 连接的客户端/服务端链路并深入关联configmiddleware配置包、底层 Go 接口实现以及 Tempo 分布式追踪后端在接收端distributor receiver中实际应用中间件包装的调用链。读完本文你将掌握中间件扩展的接口设计、配置方式、类型断言机制与错误语义并能在 Tempo 的配置与源码中定位、理解甚至自定义这类中间件。中间件扩展 API 概览extensionmiddleware包位于 vendor/go.opentelemetry.io/collector/extension/extensionmiddleware它定义了一组用于在 OpenTelemetry Collector 的导出器exporters与接收器receivers中注入中间件行为的接口。其姊妹包configmiddleware负责在组件配置中引用这些中间件扩展二者配套使用一个负责“定义能力”一个负责“按名引用”。该包在metadata.yaml中声明为type: extensionmiddleware、class: pkg源码项目归属open-telemetry/opentelemetry-collector即它是一个供 Collector 各组件复用的公共包级实现。中间件扩展可以配置在 gRPC 与 HTTP 连接上且同时覆盖客户端client与服务端server两侧。“中间件middleware”一词在这里定义得非常宽泛泛指一切在请求进出 RPC 系统时对请求进行拦截、施加动作或观察的行为——限流、认证、指标埋点、日志、压缩等都属于这一范畴。中间件的能力与细节因协议而异在某些场景下这些接口甚至允许配置超出“中间件”本意的其他行为。因此官方文档特别强调使用者必须对配置的扩展保持信任因为这些扩展有能力破坏 RPC 的安全性及其它配置。换言之中间件扩展是“在传输层握手后拥有全部权力”的代码切勿加载不受信任的扩展。中间件在代码层面生效的两个关键前提文档明确指出中间件通常是在这样一个代码层级被配置的该层级同时具备两个特征调用组件的身份未知因为confighttp与configgrpc接口同样不会携带调用组件的身份信息所以中间件在包装时拿不到“上游是谁”的上下文。信号类型未知同一条连接会服务多个信号traces、metrics、logs中间件包装逻辑必须与具体信号类型解耦。这两点决定了中间件接口被设计成“协议级、连接级”的抽象而不是“信号级”的抽象——它们包装的是http.RoundTripper、http.Handler、grpc.DialOption、grpc.ServerOption这类传输层原语。四大核心接口HTTP 与 gRPC 双栈文档给出的接口矩阵非常清晰每个接口只有一个用于为某协议配置客户端或服务端中间件的函数如果扩展无法被配置则返回错误。新协议或新的中间件配置方式可以通过新增接口来引入并且对每个接口在configmiddleware包中都存在一个对应的“按名称定位中间件扩展”的方法。HTTP 栈HTTP 接口方法每次请求都会调用一次以构造客户端或服务端中间件对象接口返回内容作用对象HTTPClient返回一个创建新http.RoundTripper的函数HTTP 客户端HTTPServer返回一个创建新http.Handler的函数HTTP 服务端“每次请求调用一次”意味着 HTTP 中间件是请求级的每个请求都会经由包装后的RoundTripper客户端侧或Handler服务端侧流经中间件可以对单个请求做注入、改写、统计或短路。gRPC 栈gRPC 接口方法在 setup建立连接/启动服务时调用一次以配置客户端或服务端中间件对象接口返回内容作用对象GRPCClient返回[]grpc.DialOptiongRPC 客户端拨号GRPCServer返回[]grpc.ServerOptiongRPC 服务端启动gRPC 中间件是连接级的通过grpc.DialOption如grpc.WithChainUnaryInterceptor、grpc.WithChainStreamInterceptor或grpc.ServerOption在一次拨号/启动时把拦截器链一次性装配好后续所有 RPC 调用都经过同一套拦截器。源码级剖析接口定义与函数式适配器四个接口在仓库中的实际定义位于 client.go 与 server.go与文档描述完全对应。客户端侧接口client.go// HTTPClient is an interface for HTTP client middleware extensions. type HTTPClient interface { // GetHTTPRoundTripper initializes a client HTTP RoundTripper // wrapper, this typically called when the Collector // starts. If there is an error this returns a nil // function. If the error is nil, the WrapHTTPRoundTripperFunc // will never be nil. GetHTTPRoundTripper(context.Context) (WrapHTTPRoundTripperFunc, error) } // GRPCClient is an interface for gRPC client middleware extensions. type GRPCClient interface { // GetGRPCClientOptions returns the gRPC dial options to use for client connections. GetGRPCClientOptions(context.Context) ([]grpc.DialOption, error) }其中WrapHTTPRoundTripperFunc被定义为函数别名// WrapHTTPRoundTripperFunc is called to initialize a new instance of // HTTP client middleware. type WrapHTTPRoundTripperFunc func(context.Context, http.RoundTripper) (http.RoundTripper, error)即中间件接收一个原始RoundTripper返回一个包装后的RoundTripper形成链式包装。注意注释中的约定如果返回错误则为 nil 函数若错误为 nil则返回的包装函数永远不会是 nil——这保证了调用方拿到非 nil 值即可安全调用。同时该文件提供了函数式适配器GetHTTPRoundTripperFunc与GetGRPCClientOptionsFunc让普通函数可以直接实现接口var _ HTTPClient (*GetHTTPRoundTripperFunc)(nil)编译期断言。值得注意的细节是空适配器nil 函数的兜底行为GetHTTPRoundTripper在f nil时返回一个透传函数return rt, nilGetGRPCClientOptions在f nil时返回nil, nil——即“无中间件”的空操作实现这为可选中间件场景提供了优雅降级。服务端侧接口server.go// HTTPServer defines the interface for HTTP server middleware extensions. type HTTPServer interface { // GetHTTPHandler wraps the provided base http.Handler. GetHTTPHandler(_ context.Context) (WrapHTTPHandlerFunc, error) } // GRPCServer defines the interface for gRPC server middleware extensions. type GRPCServer interface { // GetGRPCServerOptions returns options for a gRPC server. GetGRPCServerOptions(context.Context) ([]grpc.ServerOption, error) }WrapHTTPHandlerFunc同样以函数别名定义// WrapHTTPHandlerFunc is called to initialize a new instance of // HTTP server middleware at runtime. type WrapHTTPHandlerFunc func(context.Context, http.Handler) (http.Handler, error)函数式适配器GetHTTPHandlerFunc与GetGRPCServerOptionsFunc同样具备编译期断言与 nil 兜底GetHTTPHandler在 nil 时返回func(_ context.Context, h http.Handler) (http.Handler, error) { return h, nil }即原样返回基础 Handler。configmiddleware按名称引用中间件扩展中间件接口负责“做什么”而 configmiddleware 包负责“引哪个”。它定义了Config类型允许组件将中间件扩展配置为有序列表该支持内置在通过confighttp与configgrpc配置的 push 型接收器中例如 OTLP 接收器receivers: otlp: protocols: http: middlewares: - id: limitermiddlewareConfig 结构体从源码 configmiddleware.go 可见// Middleware defines the extension ID for a middleware component. type Config struct { // ID specifies the name of the extension to use. ID component.ID mapstructure:id,omitempty // prevent unkeyed literal initialization _ struct{} }它只有一个字段IDcomponent.ID类型mapstructure 标签id,omitempty含义就是“要使用的扩展名”。末尾的匿名空结构体字段用于防止未键控的复合字面量初始化。对应地config.schema.yaml 描述了其 JSON Schemaid为字符串类型、自定义类型为go.opentelemetry.io/collector/component.ID。四个定位方法Config提供四个核心方法通过map[component.ID]component.Component通常是组件Start()时传入的component.Host扩展映射定位中间件方法返回类型用途GetHTTPClientRoundTripperextensionmiddleware.WrapHTTPRoundTripperFunc用中间件扩展包装 HTTP 客户端GetHTTPServerHandlerextensionmiddleware.WrapHTTPHandlerFunc用中间件扩展包装 HTTP 服务端GetGRPCClientOptions[]grpc.DialOption为 gRPC 客户端配置中间件GetGRPCServerOptions[]grpc.ServerOption为 gRPC 服务端配置中间件这些方法通常在组件Start()时被调用传入component.Host的扩展映射如果按名找不到扩展会返回错误。错误语义与类型断言源码中定义了五类错误揭示了完整的失败路径var ( errMiddlewareNotFound errors.New(middleware not found) errNotHTTPServer errors.New(requested extension is not an HTTP server middleware) errNotGRPCServer errors.New(requested extension is not a gRPC server middleware) errNotHTTPClient errors.New(requested extension is not an HTTP client middleware) errNotGRPCClient errors.New(requested extension is not a gRPC client middleware) )以GetHTTPServerHandler为例其查找逻辑为先在扩展映射中按m.ID查找找不到则返回failed to resolve middleware xxx: middleware not found找到后做类型断言ext.(extensionmiddleware.HTTPServer)断言失败则返回errNotHTTPServer。这意味着扩展 ID 存在但类型不对与扩展 ID 不存在是两种可区分的错误便于排障。在 Tempo 中的实际应用distributor 接收端中间件链回到 Grafana Tempo 本体。Tempo 的 distributor 模块封装了 OTLP、Jaeger、Zipkin、Kafka 等接收器见 modules/distributor/receiver/shim.go 中otlpreceiver.NewFactory()、jaegerreceiver.NewFactory()等工厂注册并定义了与 Collector 中间件思想同构的中间件抽象。Tempo 的接收端中间件接口定义在 modules/distributor/receiver/middleware.gotype Middleware interface { Wrap(consumer.Traces) consumer.Traces } type MiddlewareFunc func(consumer.Traces) consumer.Traces // Wrap implements Interface func (tc MiddlewareFunc) Wrap(next consumer.Traces) consumer.Traces { return tc(next) }它与extensionmiddleware的“函数式接口 链式包装”设计如出一辙MiddlewareFunc让普通函数可直接充当中间件。shim.go 中factoryBase.CreateTraces(ctx, params, cfg, middleware.Wrap(shim))正是把中间件包装后的consumer.Traces传入接收器工厂构成“接收器 → 中间件链 → shim → 后续处理”的流水线。Tempo 内置了两个具体中间件实现可作为自定义中间件的参考范本FakeTenantMiddleware通过user.InjectOrgID(ctx, util.FakeTenantID)为所有请求注入假的租户 ID常见于单租户/测试场景MultiTenancyMiddleware先从 gRPC 请求提取租户失败则回退到 HTTP 请求的client.FromContext(ctx)与 metadata 头user.OrgIDHeaderName中提取并校验租户头数量多于一个报错最后user.InjectOrgID注入租户。从源码结构看Tempo 把“租户身份注入/提取”这类横切关注点做成了与 Collector 中间件扩展同一层级的可插拔包装器这也印证了官方文档“中间件泛指对进出 RPC 请求的拦截、施加动作与观察”的宽泛定义。小结与自定义指引中间件扩展 API 的完整用法可归纳为一条清晰链路实现接口按协议与方向选择HTTPClient/HTTPServer/GRPCClient/GRPCServer返回包装函数或 gRPC 选项若返回错误则视为配置失败。注册扩展在 Collector 的extensions段声明扩展实例确保其类型实现了目标接口。按名引用在confighttp/configgrpc的middlewares列表中写- id: extension-name通过Config的四个Get*方法在Start()时解析并装配。链式叠加多个中间件形成有序列表包装函数一层层包裹原始RoundTripper/HandlergRPC 侧则合并进 DialOption / ServerOption。如需在 Tempo 中新增中间件可直接仿照 modules/distributor/receiver/middleware.go 中的MiddlewareFunc模式实现Wrap(consumer.Traces) consumer.Traces并在 shim 的创建链路中传入若要走 Collector 原生扩展体系则按extensionmiddleware接口实现后通过configmiddleware按名引用。两条路径都遵循“接口定义能力、配置按名引用、函数链式包装、nil 兜底降级”的统一设计哲学。【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考