Prometheus Script Exporter开发

612 阅读3分钟

问题所在

在日常工作中有大量的脚本 需要我们提供自定义的监控,但我们又不可能把每一个脚本做成一个 Exporter。 我们的想法是把每个脚本当作一个模块,存放在同一个目录下。我们开发一个模块(脚本)的执行器,能够对不同的模块进行监控。

image.png 我们通过在浏览器上 输入匹配相应 module 的 URL 使执行器获得对应模块进行执行并将监测结果返回给浏览器。

执行器(Executor)

搭建目录 image.png

执行器的搭建 (executor.go)

  • 执行器结构体
    • 模块存放相对路径
    • 日志
import (
    "github.com/infraboard/mcube/logger"
    "github.com/infraboard/mcube/logger/zap"
        )
func NewExcutor(moduleDir string) *Excutor {
	return &Excutor{
		ModuleDir: moduleDir,
		log:       zap.L(),
	}
}

type Excutor struct {
	ModuleDir string
	log       logger.Logger
}
  • Find函数(模块名)
    • 根据 Exector内的 包路径 变量获取 该目录的绝对路径
    • 将 该模块的 绝对路径拼接后返回
func (e *Excutor) Find(module string) (string, error) {
	// 获取到存放脚本目录的 绝对路径
	absPath, err := filepath.Abs(e.ModuleDir)
	if err != nil {
		return "", fmt.Errorf("find module %s abs path error %s", module, err)
	}

	// 防止用户传入的执行脚本 超出指定目录
	if strings.Contains(module, "..") {
		return "", fmt.Errorf("module forbiden .. in module")
	}

	// 拼凑脚本绝对路径
	return filepath.Join(absPath, module), nil
}
  • 执行函数(模块名模块参数Writer )
    • 通过 模块名 获得模块的绝对路径
    • 执行模块并以流的形式返回输出
func (e *Excutor) Exec(moduleName, moduleParams string, dst io.Writer) error {
	// 寻找模块 绝对路径
	fp, err := e.Find(moduleName)
	if err != nil {
		return err
	}

	if fp == "" {
		return fmt.Errorf("module: %s not found", moduleName)
	}
        
        // 日志打印
	e.log.Infof("found module path: %s", fp)

	// 执行模块并实时返回执行结果
	var cmd *exec.Cmd
	ext := filepath.Ext(moduleName)
	switch ext {
	case ".sh":
		cmd = exec.Command("bash", fp, moduleParams)
	case ".py":
		cmd = exec.Command("python", fp, moduleParams)
	default:
		// 二进制可执行文件 直接执行
		cmd = exec.Command(fp, moduleParams)
	}

	// 获取命令输出,以流的方式
	cmd.Stderr = dst
	cmd.Stdout = dst

	// 执行命令, 后台执行 go cmd.Run
	if err := cmd.Start(); err != nil {
		return err
	}

	return cmd.Wait()
}

测试执行器

  • 申明一个全局 执行器 变量, 并将其初始化
var (
	e *script.Excutor
)

func init() {
	zap.DevelopmentSetup()
	e = script.NewExcutor("modules")
}
  • 我们在 modules 内 写一个python程序(print(1))进行测试
func TestExec(t *testing.T) {
	bf := bytes.NewBuffer([]byte{})
	err := e.Exec("test.py", "", bf)
	if err != nil {
		t.Fatal(err)
	}
	t.Log("result", bf.String())
}
  • 执行结果 image.png

模块编写

  • 复习上节内容,搭建一个简单的模块(脚本)使用

搭建目录 image.png

data.txt

image.png

收集器(Colletcor.go)

  • data中的 count,tps,diff皆为动态标签
type RocketMQCollector struct {
	count   *prometheus.Desc
	tps     *prometheus.Desc
	diff    *prometheus.Desc
	dataDir string
}

func NewCollector(dataDir string) *RocketMQCollector {
	return &RocketMQCollector{
		count: prometheus.NewDesc(
			"rocketmq_count", // 指标名称
			"rocketmq_count", // 指标描述
			[]string{"group", "version", "type", "model"}, // 动态标签
			prometheus.Labels{"module": "rocketmq"}, // 静态标签
		),
		tps: prometheus.NewDesc(
			"rocketmq_tps",
			"rocketmq_tps",
			[]string{"group", "version", "type", "model"},
			prometheus.Labels{"module": "rocketmq"},
		),
		diff: prometheus.NewDesc(
			"rocketmq_diff",
			"rocketmq_diff",
			[]string{"group", "version", "type", "model"},
			prometheus.Labels{"module": "rocketmq"},
		),
		dataDir: dataDir,
	}
}
  • 收集器需要实现的方法
func (c *RocketMQCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.diff
	ch <- c.count
	ch <- c.tps
}

func (c *RocketMQCollector) Collect(ch chan<- prometheus.Metric) {
	// 读取文件
	f, err := os.Open(c.dataDir)
	if err != nil{
		panic(err)
	}
	defer f.Close()

	r := bufio.NewReader(f)
        // 按行进行解析
	for {
		line, _, err := r.ReadLine()
		if err == io.EOF{
			break
		}

		if err != nil{
			panic(err)
		}

		m := ParseLine(string(line)) // 

		// 不是第一行 (每列名称)
		if m.Group != "#Group"{
			ch <- prometheus.MustNewConstMetric(c.count, prometheus.GaugeValue, m.Float64Count(), m.Group, m.Version, m.Type, m.Model)
			ch <- prometheus.MustNewConstMetric(c.tps, prometheus.GaugeValue, m.Float64Tps(), m.Group, m.Version, m.Type, m.Model)
			ch <- prometheus.MustNewConstMetric(c.diff, prometheus.GaugeValue, m.Float64DiffTotal(), m.Group, m.Version, m.Type, m.Model)
		}
	}
}

模型(model.go)

我们从目录中得到每列的名称并将其封装在一个结构体里

type RocketMQMetric struct {
	Group     string
	Count     string
	Version   string
	Type      string
	Model     string
	TPS       string
	DiffTotal string
}
  • 解析 data 的一行内容
    • 先逐字符读取到 chars
    • 遇到 空格时将 chars的内容写进 words 并清空 chars
func ParseLine(line string) *RocketMQMetric {
	words := []string{}
	chars := []rune{}
	for _, c := range line {
		if c != ' ' {
			chars = append(chars, c)
		} else {
			if len(chars) > 0 {
				words = append(words, string(chars))
				chars = []rune{}
			}
		}
	}
	// 一行结束
	if len(chars) > 0{
		words = append(words, string(chars))
	}
	return &RocketMQMetric{
		Group:     words[0],
		Count:     words[1],
		Version:   words[2],
		Type:      words[3],
		Model:     words[4],
		TPS:       words[5],
		DiffTotal: words[6],
	}
}
  • floatxxx函数:将结构体的 动态标签从 string类型转到 float64 类型

测试采集器

func TestCollector(t *testing.T) {
	registry := prometheus.NewRegistry()

	c := collector.NewCollector("sample/data.txt")

	registry.MustRegister(c)

	mf, err := registry.Gather()
	if err != nil{
		t.Fatal(err)
	}

	b := bytes.NewBuffer([]byte{})
	enc := expfmt.NewEncoder(b, expfmt.FmtText)
	// 遍历每一个metric 把他编码成FmtText, 也就是Prometheus文本格式
	// 编码过后会把结果输出到buffer里面
	for i := range mf {
		enc.Encode(mf[i])
	}

	// buffer里面就是编码后的数据
	t.Log(b.String())
}

执行结果 image.png

编写模块(脚本)

在编写脚本之前我们先看一下项目的总体架构 image.png
modules内放着我们的模块(即将编写的),通过main函数 暴露 HTTP Handler 内创建执行器执行该模块,并在浏览器上显示

  • 我们把刚编写好的 收集器 写入模块
func main(){
	registry := prometheus.NewRegistry()
	
	dataDir := os.Getenv("ROCKETMQ_DATA_DIR")
	if dataDir == ""{
		dataDir = "collector/sample/data.txt"
	}

	c := collector.NewCollector(dataDir)
	registry.MustRegister(c)

	mf, err := registry.Gather()
	if err != nil{
		fmt.Println(err)
		os.Exit(2)
	}

	enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
	for i := range mf{
		enc.Encode(mf[i])
	}
}

设置环境变量: export ROCKETMQ_DATA_DIR="modules/sample/data.txt"

打包成二进制文件: go build -o rocketmq main.go

项目入口

func ScriptHandler(w http.ResponseWriter, r *http.Request){
	sc := script.NewExcutor("modules")
	qs := r.URL.Query()
	err := sc.Exec(qs.Get("module"), qs.Get("params"), w)
	if err != nil{
		fmt.Fprint(w, fmt.Sprint("exec error:", err))
	}
}

func main(){
	zap.DevelopmentSetup()
	http.HandleFunc("/metrics", ScriptHandler)
	http.ListenAndServe(":8050", nil)

}

我们执行一下,并在浏览器中查看

image.png