问题所在
在日常工作中有大量的脚本 需要我们提供自定义的监控,但我们又不可能把每一个脚本做成一个 Exporter。
我们的想法是把每个脚本当作一个模块,存放在同一个目录下。我们开发一个模块(脚本)的执行器,能够对不同的模块进行监控。
我们通过在浏览器上 输入匹配相应
module 的 URL 使执行器获得对应模块进行执行并将监测结果返回给浏览器。
执行器(Executor)
搭建目录
执行器的搭建 (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())
}
- 执行结果
模块编写
- 复习上节内容,搭建一个简单的模块(脚本)使用
搭建目录
data.txt
收集器(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())
}
执行结果
编写模块(脚本)
在编写脚本之前我们先看一下项目的总体架构
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)
}
我们执行一下,并在浏览器中查看