Prometheus 自定义服务发现:基于 file_sd 适配器实现官方发行版未内置的 SD 机制
【免费下载链接】prometheusThe Prometheus monitoring system and time series database.项目地址: https://gitcode.com/GitHub_Trending/pr/prometheus
本篇指南基于 Prometheus 仓库中的 custom-sd 示例 展开,讲解如何用 file_sd 适配器把任意“非官方”服务发现(Service Discovery,SD)实现接入 Prometheus:你只需实现一个Discoverer接口,由适配器将其产出的目标组(TargetGroup)落盘为 file_sd 兼容的 JSON 文件,再由prometheus.yml中的file_sd_configs消费。读完本文,你可以照着仓库示例接入 Consul 等任意注册中心,并理解适配器从目标同步、变更判定到原子写文件的完整实现。
一、为什么需要 file_sd 适配器
Prometheus 官方发行版内置了大量 SD 机制(EC2、Kubernetes、Consul、DNS 等,源码位于 discovery/ 目录),但企业内网常存在私有注册中心或自研配置系统,这些机制无法直接写进发行版。custom SD 适配器的思路是“解耦”:
- 自定义 SD 逻辑运行在一个独立进程中,实现 Prometheus 的
discovery.Discoverer接口; - 适配器(
adapter包)负责驱动该 Discoverer,把目标组序列化为 JSON 写入一个文件; - 该文件通过
prometheus.yml的file_sd机制交给 Prometheus 抓取,无需使用静态配置(static config)即可动态传递目标。
整体数据流如下:
自定义 Discoverer(如 Consul 查询逻辑) │ []*targetgroup.Group(chan) ▼ discovery.Manager(discovery/manager.go) │ 变更后的目标组 ▼ Adapter:生成 JSON 并原子写入 custom_sd.json │ 文件变更(fsnotify 磁盘监听) ▼ file_sd(discovery/file/file.go)→ Prometheus 抓取目标从源码结构看,这条链路复用了 Prometheus 官方的服务发现核心组件——适配器内部直接创建discovery.Manager来驱动 Discoverer,因此自定义 SD 的行为(目标增删、标签变化)与内置 SD 机制在语义上完全一致,只是输出端从“内存中的 scrape manager”换成了“磁盘文件 + file_sd 监听”。
二、示例目录结构
示例位于 documentation/examples/custom-sd/,包含三部分:
| 路径 | 内容 |
|---|---|
| adapter/adapter.go | file_sd 适配器核心实现,实现自定义 SD 时无需修改此文件 |
| adapter/adapter_test.go | 适配器的单元测试(目标组生成、文件写出) |
| adapter-usage/main.go | 一个可直接运行的示例:为 Consul 实现的Discoverer+ 适配器调用入口 |
按原 README 的说明,adapter-usage目录包含一个基础 Consul 服务发现的Discoverer实现:它向 Consul 查询所有已知服务(跳过 Consul 自身),把服务的全部元数据以标签(label)形式随目标一起打包进TargetGroup;adapter目录则是你需要导入并把自定义Discoverer传入的适配器代码。
三、file_sd:适配器与 Prometheus 的对接契约
适配器产出的文件必须能被file_sd解析。根据官方配置文档 docs/configuration/configuration.md 中<file_sd_config>一节,file_sd 的定义是:
File-based service discovery provides a more generic way to configure static targets and serves as an interface to plug in custom service discovery mechanisms.
关键行为与参数(原文档内容,完整继承):
- 读取一组包含零个或多个
<static_config>的文件,文件格式支持JSON 或 YAML; - 文件变更通过磁盘监听(fsnotify)检测并立即生效;父目录也被隐式监听,以高效处理原子重命名和新增的 glob 匹配文件(若父目录文件过多,监听开销会增大);
- 仅“结果良好(well-formed)的目标组变更”会被应用;
- 作为兜底,文件会按
refresh_interval周期性重读; - 每个目标在 relabel 阶段带有元标签
__meta_filepath,值为其来源文件路径。
# prometheus.yml 片段 scrape_configs: - job_name: custom_sd_consul file_sd_configs: - files: - /var/lib/prometheus/custom_sd.json refresh_interval: 5m # 周期性重读文件的兜底间隔,默认 5m文件内容格式(JSON / YAML 二选一):
[ { "targets": [ "<host>", ... ], "labels": { "<labelname>": "<labelvalue>", ... } } ]- targets: [ - '<host>' ] labels: [ <labelname>: <labelvalue> ... ]文件名约束来自 discovery/file/file.go 中的校验逻辑:路径必须以.json、.yml、.yaml(大小写不敏感)结尾,最后一段路径可含一个*通配符(如my/path/tg_*.json),正则定义为^[^*]*(\*[^/]*)?\.(json|yml|yaml|JSON|YML|YAML)$;files至少需要一个条目。默认刷新间隔在 DefaultSDConfig 中定义为5 * time.Minute。这也解释了为什么适配器的输出文件默认命名为custom_sd.json——后缀天然符合 file_sd 的 glob 校验。
四、Discoverer 接口:自定义 SD 的唯一硬性要求
从 discovery/discovery.go 可以看到接口定义(实现自定义 SD 必须遵守的契约):
// Discoverer provides information about target groups. It maintains a set // of sources from which TargetGroups can originate. Whenever a discovery provider // detects a potential change, it sends the TargetGroup through its channel. // // Discoverer does not know if an actual change happened. // It does guarantee that it sends the new TargetGroup whenever a change happens. // // Discoverers should initially send a full set of all discoverable TargetGroups. type Discoverer interface { // Run hands a channel to the discovery provider (Consul, DNS, etc.) through which // it can send updated target groups. It must return when the context is canceled. // It should not close the update channel on returning. Run(ctx context.Context, up chan<- []*targetgroup.Group) }接口契约可以归纳为四点:
- 初始全量:首次应发送全部可发现目标组的完整集合;
- 只发“可能变了”的组:Discoverer 不判断是否真的变化,只保证变化发生时发送新
TargetGroup; - 不得关闭传入的 channel:返回时不应关闭
up; - 响应 context:
ctx取消时Run必须返回。
适配器正是通过 discovery/manager.go 的StartCustomProvider把这个 Discoverer 挂进discovery.Manager。该方法的源码注释值得注意:“used for sdtool. Only use this if you know what you're doing”(仅供 sdtool 类工具使用,非标准路径),说明这是面向工具侧的扩展入口,而非prometheus.yml的常规配置路径。其内部逻辑是创建Provider、调用startProvider,后者在两个 goroutine 中分别执行p.d.Run(ctx, updates)和m.updater(ctx, p, updates),完成“Discoverer 产出 → Manager 聚合 → 下游订阅”的闭环。
五、适配器核心实现解析(adapter/adapter.go)
5.1 数据结构
适配器内部用 customSD 结构 描述单个目标组,即 file_sd JSON 的最小形态:
type customSD struct { Targets []string `json:"targets"` Labels map[string]string `json:"labels"` }Adapter 结构 持有驱动所需的全部依赖:
// Adapter runs an unknown service discovery implementation and converts its target groups // to JSON and writes to a file for file_sd. type Adapter struct { ctx context.Context disc discovery.Discoverer // 你的自定义 SD 实现 groups map[string]*customSD // 当前已知的目标组快照 manager *discovery.Manager // 驱动 Discoverer 的官方 Manager output string // 输出文件路径 name string // 该 SD 机制的名称(Provider 名) logger *slog.Logger }5.2 NewAdapter 参数说明
构造入口见 NewAdapter:
func NewAdapter(ctx context.Context, file, name string, d discovery.Discoverer, logger *slog.Logger, sdMetrics *discovery.SDMetrics, registerer prometheus.Registerer) *Adapter| 参数 | 含义 |
|---|---|
ctx | 生命周期 context;取消时 Manager 会停止 provider |
file | 输出文件路径,即prometheus.yml中file_sd_configs.files指向的文件 |
name | 自定义 SD 机制名,用作 Manager 中 Provider 的名字 |
d | 你的discovery.Discoverer实现实例 |
logger | slog.Logger |
sdMetrics | *discovery.SDMetrics(机制指标 + refresh 指标),示例中通过discovery.RegisterSDMetrics/NewRefreshMetrics注册 |
registerer | prometheus.Registerer,此处示例使用独立的prometheus.NewRegistry() |
NewAdapter内部通过discovery.NewManager(ctx, logger, registerer, sdMetrics)创建 Manager——即适配器直接复用 Prometheus 主进程同款的服务发现管理器,无需自行实现目标聚合、去重与清理逻辑。
5.3 运行主流程
Run() 只有三行:
func (a *Adapter) Run() { //nolint:errcheck go a.manager.Run() a.manager.StartCustomProvider(a.ctx, a.name, a.disc) go a.runCustomSD(a.ctx) }a.manager.Run()在独立 goroutine 中启动 Manager 主循环;StartCustomProvider把你的 Discoverer 注册为 Provider 并启动(对应第四节discovery/manager.go中的实现);runCustomSD订阅 Manager 的同步通道。
runCustomSD 持续从a.manager.SyncCh()读取目标组全集,并在ctx取消或通道关闭时退出:
func (a *Adapter) runCustomSD(ctx context.Context) { updates := a.manager.SyncCh() for { select { case <-ctx.Done(): case allTargetGroups, ok := <-updates: // Handle the case that a target provider exits and closes the channel // before the context is done. if !ok { return } a.refreshTargetGroups(allTargetGroups) } } }5.4 变更判定与原子写文件
refreshTargetGroups 先由 generateTargetGroups 把map[string][]*targetgroup.Group(key 为 SD 类型名)压缩为map[string]*customSD,再用reflect.DeepEqual与旧快照比对:只有发生变化才落盘,避免无意义的文件写入触发 file_sd 反复重载。
目标组到文件的转换规则:
- 每个
TargetGroup的Targets展平为字符串数组并排序(sort.Strings,保证输出稳定,也便于 DeepEqual 判定); - 组级
Labels原样复制; - 映射 key 为
fmt.Sprintf("%s:%s:%s", k, group.Source, groupFingerprint.String()),其中 fingerprint 是“所有目标地址指纹 XOR + 组标签指纹”——引入指纹是为了防止sd_type与group.Source都不唯一时 key 冲突。
写文件采用临时文件 + 原子重命名,见 writeOutput:
// Writes JSON formatted targets to output file. func (a *Adapter) writeOutput() error { arr := mapToArray(a.groups) b, _ := json.MarshalIndent(arr, "", " ") dir, _ := filepath.Split(a.output) tmpfile, err := os.CreateTemp(dir, "sd-adapter") // ... // Close the file immediately for platforms (eg. Windows) that cannot move // a file while a process is holding a file handle. tmpfile.Close() err = os.Rename(tmpfile.Name(), a.output) // ... }两个细节与 file_sd 的实现特性直接对应:
- 临时文件与目标文件同目录(
filepath.Split取目录),保证os.Rename是同一文件系统内的原子操作——而 file_sd 恰好隐式监听父目录来“efficiently handle atomic renaming”(见 discovery/file/file.go 的 fsnotify 引入与 configuration.md 的说明); - 重命名前先
tmpfile.Close(),源码注释说明这是为了兼容 Windows 等平台“持有句柄时无法移动文件”的限制。
六、完整示例走读:Consul 版 Discoverer(adapter-usage/main.go)
adapter-usage/main.go 是一个可编译运行的完整程序(package main),演示了 README 中“Usage”一节要求的全部动作:替换示例 SD 配置、实现Discoverer、把实例传给NewAdapter。源码中对应的Note:/NOTE:注释即原文档指代的改造点。
6.1 命令行参数
程序基于 kingpin 提供两个标志(main.go#L41-L43):
| 标志 | 默认值 | 说明 |
|---|---|---|
--output.file | custom_sd.json | file_sd 兼容的输出文件路径 |
--listen.address | localhost:8500 | Consul HTTP API 监听地址 |
6.2 自定义 SD 配置(替换点一)
按注释 “Note: create a config struct for your custom SD type here” 定义自己的配置结构(main.go#L80-L85):
// Note: create a config struct for your custom SD type here. type sdConfig struct { Address string TagSeparator string RefreshInterval int }注意这与官方内置 Consul SD 不同:官方机制的配置走prometheus.yml的consul_sd_configs并由discovery.RegisterConfig注册,而这里因为运行在独立进程中,配置由程序自身的命令行/硬编码给出,在main()中组装(main.go#L259-L264):
// NOTE: create an instance of your new SD implementation here. cfg := sdConfig{ TagSeparator: ",", Address: *listenAddress, RefreshInterval: 30, // 每 30 秒轮询一次 Consul }6.3 实现 Discoverer 接口(替换点二)
按注释 “Note: This is the struct with your implementation of the Discoverer interface (see Run function)” 定义结构(main.go#L87-L95),核心是必须实现的Run函数。示例的 Run 方法 逻辑:
- 按
refreshInterval定时轮询GET http://<address>/v1/catalog/services获取全部服务名; - 跳过
consul服务本身(对应 README“except Consul itself”的描述); - 对每个服务调用
GET /v1/catalog/service/<name>,由 parseServiceNodes 解析为*targetgroup.Group; - 每轮结束后发送
ch <- tgs,然后等待下一 tick 或ctx.Done()。
几个值得学习的实现细节:
服务地址选择:若服务注册了ServiceAddress(可能来自远端节点注册),则用它拼接端口,否则回退到节点Address:
var addr string if node.ServiceAddress != "" { addr = net.JoinHostPort(node.ServiceAddress, strconv.Itoa(node.ServicePort)) } else { addr = net.JoinHostPort(node.Address, strconv.Itoa(node.ServicePort)) }元标签集:示例把 Consul 的全部服务数据以__meta_consul_*标签形式带上(main.go#L46-L57 定义):
| 元标签 | 内容 |
|---|---|
__meta_consul_address | 节点地址 |
__meta_consul_node | 节点名 |
__meta_consul_tags | 服务标签(前后包裹分隔符,使 relabel 正则无需考虑位置) |
__meta_consul_service_address | 可选的服务地址 |
__meta_consul_service_port | 服务端口 |
__meta_consul_service_id | 服务 ID |
节点元数据(NodeMeta)还会经strutil.SanitizeLabelName清洗后逐个附加为__meta_<key>标签。这些__meta_*标签只存在于 relabel 阶段,你可以在relabel_configs中把它们映射为正式标签或据此过滤目标。
消失目标的处理:discovery结构维护oldSourceList,每轮记录本次出现的服务;若某服务从 Consul 目录中消失,则补发一个只有Source、无目标的空TargetGroup(main.go#L215-L222),确保下游能及时清理旧目标。
错误容忍策略:注释明确说明,对“单个服务查询失败”视为本轮致命(break跳出本轮),宁可保留部分陈旧目标,也不因一次超时就提交不完整的目标列表;若服务真的消失,下一轮外层循环会处理(main.go#L190-L194)。
6.4 组装并启动适配器(替换点三)
main()的最后几步(main.go#L276-L291)展示了把 Discoverer 交给适配器的标准写法:
reg := prometheus.NewRegistry() refreshMetrics := prom_discovery.NewRefreshMetrics(reg) mechanismMetrics, err := prom_discovery.RegisterSDMetrics(reg, refreshMetrics) if err != nil { logger.Error("failed to register service discovery metrics", "err", err) os.Exit(1) } sdMetrics := &prom_discovery.SDMetrics{ MechanismMetrics: mechanismMetrics, RefreshManager: refreshMetrics, } sdAdapter := adapter.NewAdapter(ctx, *outputFile, "exampleSD", disc, logger, sdMetrics, reg) sdAdapter.Run() <-ctx.Done()说明:
RegisterSDMetrics会注册scrape_sd_discovered_targets、scrape_sd_running_duration_seconds、scrape_sd_discovery_refresh_success、scrape_sd_refresh_duration_seconds等机制指标(对应 discovery/metrics.go 中的定义),注册到独立 registry,不会污染 Prometheus 主进程;- 第三个参数
"exampleSD"即name,在 Manager 中作为 Provider 名出现; - 由于
ctx是context.Background(),<-ctx.Done()永不返回,进程将持续运行——这正是适配器作为常驻 sidecar 进程的形态。
七、测试依据
适配器的正确性由 adapter_test.go 覆盖,可作为实现自定义 SD 时的验收参照:
- TestGenerateTargetGroups 用四组表驱动用例验证目标组转换:空组(key 为
customSD:Consul:0000000000000000,即零指纹)、多目标组(如customSD:Azure:282a007a18fadbbb)、空/非空混合、以及乱序 IP 的排序稳定性(输入192.168.1.55, 192.168.1.44,输出排序后的192.168.1.44, 192.168.1.55)——最后一个用例专门验证“地址乱序不导致无谓的重复写文件”; - TestWriteOutput 验证
writeOutput能真实地把 JSON 落盘。
八、落地清单与注意事项
改造三步(即原 README 的 Usage 要求):在
adapter-usage/main.go中替换示例sdConfig、实现自己的discovery.Discoverer(重点是Run函数)、把其实例传给adapter.NewAdapter;所有改造点均有Note:/NOTE:注释标记,adapter/adapter.go无需改动(其头部注释明确写明 “you do not need to edit this file when implementing a custom sd”)。输出文件名必须匹配 file_sd 的 glob:以
.json/.yml/.yaml结尾,否则 file_sd 配置校验 会拒绝该配置。适配器是独立进程:它与 Prometheus 主进程解耦,需要自行保证常驻(systemd / 容器等);它崩溃后文件停止更新,Prometheus 会继续使用文件中的最后目标集直到超时清理,这一点从源码结构看与官方各 SD 机制的单点行为一致。
轮询间隔由你的 Discoverer 决定:Consul 示例是 30 秒定时轮询(
RefreshInterval: 30),若你的机制支持 watch/事件推送(如 Consul 的 block queries),可在Run中用事件驱动代替 ticker,降低目标变更延迟;file_sd 侧的refresh_interval(默认 5m)只是磁盘监听的兜底。变更判定基于 DeepEqual:
generateTargetGroups已对目标排序以保证输出稳定;自定义Run中若每轮都发送内容相同的目标组,适配器不会重复写文件,可放心发送全量。运行方式:示例属于主模块(导入路径为
github.com/prometheus/prometheus/documentation/examples/custom-sd/adapter),可在仓库根目录直接构建运行,例如:go run ./documentation/examples/custom-sd/adapter-usage \ --output.file=/var/lib/prometheus/custom_sd.json \ --listen.address=localhost:8500前提是目标注册中心(示例为 Consul,默认
localhost:8500)可用。
九、小结
custom-sd 示例展示了 Prometheus 服务发现体系的标准扩展姿势:自定义侧只需要一个满足 Discoverer 契约 的Run实现,官方侧由 Adapter 负责驱动 discovery.Manager、做变更判定与原子写文件,Prometheus 侧则零改动地用file_sd_configs消费。相比把第三方 SD 直接编译进发行版,这种“进程级适配 + 文件接口”的方案边界清晰、可独立升级与测试(见 adapter_test.go),是把私有注册中心、自研配置系统接入 Prometheus 抓取目标的最通用路径。
【免费下载链接】prometheusThe Prometheus monitoring system and time series database.项目地址: https://gitcode.com/GitHub_Trending/pr/prometheus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考