Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

插件编写入门指南 #12

Open
UlricQin opened this issue Nov 30, 2023 · 0 comments
Open

插件编写入门指南 #12

UlricQin opened this issue Nov 30, 2023 · 0 comments
Labels
documentation Improvements or additions to documentation

Comments

@UlricQin
Copy link
Contributor

UlricQin commented Nov 30, 2023

前言

cprobe 要监控各类组件,所以需要统一的插件设计,经过社区小伙伴的一起努力,第一个版本的插件框架已经成型。后面大家就可以补充更多插件进来了。这个文档 issue 会讲解如何编写一个插件,供大家参考。

配置

每个插件都会有一些配置文件,来配置采集行为,分门别类的放置在 conf.d 目录下,每个子目录就是一个插件配置目录,我们一起来看一下 mysql 和 redis 插件的配置目录:

ulric@ulric-flashcat conf.d % tree
.
├── backup.yaml
├── mysql
│   ├── doc
│   │   ├── README.md
│   │   ├── alert
│   │   │   └── prom_alert_01.yaml
│   │   └── dash
│   │       └── grafana_mysql_01.json
│   ├── inst.yaml
│   ├── main.yaml
│   ├── rule_coll.toml
│   ├── rule_cust.toml
│   └── rule_head.toml
├── redis
│   ├── backup.toml
│   ├── cluster.toml
│   ├── doc
│   │   ├── README.md
│   │   ├── alert
│   │   │   └── prom_alert_01.yaml
│   │   └── dash
│   │       ├── grafana_redis_01.json
│   │       └── grafana_redis_02.json
│   ├── main.yaml
│   └── rule.toml
└── writer.yaml

8 directories, 18 files

每个插件目录下面都有一个 main.yaml 作为入口配置文件,配置了如何做监控目标的服务发现、不同的监控目标要生效的采集规则、采集频率、附加标签、relabel 规则等。这一部分逻辑框架会自动处理,各个插件的编写人员无需关注。

插件编写人员需要设计采集规则文件,即 rule*.toml,采集规则主要是配置认证信息以及要采集的内容,比如 mysql 插件就要在 rule 文件中设计一些配置项让用户填写 user、password 之类的,mysql 插件还可以采集 mysql 的 global status、global variables、slave status、schema size 等等很多内容,具体要启用哪些采集能力,也需要通过配置文件来设置。每个采集 job 可以关联多个 rule 文件,框架会自动把这几个 rule 文件的内容拼接在一起,decode 成内存里的一个数据结构,这个数据结构是需要各个插件自定义的,如此一来,插件就可以自定义自己的配置 struct,也可以基于这些配置项自定义自己的采集逻辑。

插件代码结构

插件的代码要放在 plugins 目录下,一个插件一个目录,一般在目录下会有一个入口 go 文件,比如 mysql 插件代码目录下有个 mysql.go,redis 插件代码目录下有个 redis.go。

插件的代码中要定义一个空的 struct,这个 struct 要包含两个方法,一个是 ParseConfig,另一个是 Scrape,以 redis 插件举例:

type Redis struct {}
func (*Redis) ParseConfig(bs []byte) (any, error) {}
func (*Redis) Scrape(ctx context.Context, target string, c any, ss *types.Samples) error {}

再看一下 mysql 插件的:

type MySQL struct {}
func (*MySQL) ParseConfig(bs []byte) (any, error) {}
func (*MySQL) Scrape(ctx context.Context, target string, c any, ss *types.Samples) error {}

框架层面会调用各个插件的 ParseConfig 方法来 decode 配置文件,调用 Scrape 方法来抓取监控数据。ParseConfig 的参数就是 job 里几个 rule toml 文件拼成的内容,Scrape 方法的各个参数的作用稍作解释:

  • ctx:这是 Context,用于接收退出信号的
  • target:要摘取的实例的目标地址,比如某个 mysql 实例可能是:10.1.2.3:3306
  • c:是配置文件解析之后的数据结构
  • ss:用于存放抓取到的监控数据

初期做的两个插件 mysql 和 redis,都是改造自 exporter,exporter 的监控数据采集逻辑通常是固定的,一般就是实例化 Exporter 对象,然后调用其 Collect 方法,然后收集 Collect 方法采集到的数据,最后做一下数据格式转换塞给 ss 即可。比如以 Redis 举例:

func (*Redis) Scrape(ctx context.Context, target string, c any, ss *types.Samples) error {
	// 这个方法中如果要对配置 c 变量做修改,一定要 clone 一份之后再修改,因为并发的多个 target 共享了一个 c 变量
	cfg := c.(*Config)

	// 清洗 target 格式
	if !strings.Contains(target, "://") {
		target = "redis://" + target
	}

	u, err := url.Parse(target)
	if err != nil {
		return errors.WithMessagef(err, "failed to parse target %s", target)
	}

	u.User = nil
	target = u.String()

	// 准备 RedisExporter 所需要的配置 Options
	conf := cfg.Global
	opts := exporter.Options{
		User:                      conf.User,
		Password:                  conf.Password,
		...
		ExportClientsInclPort:     conf.ExportClientsIncludePort,
	}

	// 实例化 RedisExporter
	exp, err := exporter.NewRedisExporter(target, opts)
	if err != nil {
		return errors.Wrap(err, "failed to create redis exporter")
	}

	// 准备一个 channel,用于存放监控数据,后面传给 Collect 方法,Collect 方法内部收集到的数据会吐给这个 channel
	ch := make(chan prometheus.Metric)

	// 创建一个 goroutine 执行 Collect 逻辑
	go func() {
		exp.Collect(ch)
		close(ch)
	}()

	// 从 channel 中读取 RedisExporter 采集到的数据,塞给 ss 数据结构,AddPromMetric 方法会自动进行数据结构格式转换
	for m := range ch {
		if err := ss.AddPromMetric(m); err != nil {
			logger.Warnf("failed to transform prometheus metric: %s", err)
		}
	}

	return nil
}

上面的代码为了简化说明核心逻辑,删掉了一些代码,原始代码大家可以去看 redis.go 的内容。

插件注册

插件中还需要有个 init 方法,用于做插件注册,比如 redis 插件:

func init() {
	plugins.RegisterPlugin(types.PluginRedis, &Redis{})
}

最后,在 registry.go 中 import 一下,这个 import 会自动触发这个 init 方法的执行,如此,就完活了。

补充

每个插件,希望在提供采集能力的同时,也提供一些知识经验沉淀,比如沉淀一些告警规则、仪表盘,这些内容可以放到 conf.d/<plugin>/doc 目录,大家一起完善,持续做到开箱即用。

@UlricQin UlricQin added the documentation Improvements or additions to documentation label Nov 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant