node_exporter嵌入自定义指标

背景

监控告警项目中,采用prometheus和alertmanager实现监控和告警。node_exporter采集cpu等常用指标,blackbox_exporter采集网络连通性等指标。然而,在实际测试中,构造不同的网络中断、服务中断的案例比较繁琐。故在node_exporter基础上添加自定义的collector收集器,从文件中读取信息,采集自定义的监控指标;同样,可以自定义采集cpu等常规指标的方式。这样易于构造数据来测试告警。

实现方式

嵌入node_exporter自定义的指标非常容易,只需要定义好3个函数(注册函数,构建收集器函数和更新指标函数)即可。

package collector

import (
	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/prometheus/client_golang/prometheus"
	"gopkg.in/yaml.v2"
	"io/ioutil"
)

const (
	probeSubsystem = "probe"
)

type probeCollector struct {
	probeSuccess *prometheus.Desc
	logger       log.Logger
}

// 自定义的探针指标,用于模拟类似于blackbox_exporter中的icmp和tcp探针
type ProbeConfig struct {
	Icmp []struct {
		Targets []string `yaml:"targets"`
		Success bool     `yaml:"success"`
	} `yaml:"icmp,omitempty"`
	Tcp []struct {
		Targets []string `yaml:"targets"`
		Success bool     `yaml:"success,omitempty"`
	} `yaml:"tcp,omitempty"`
}

func init() {
	// 注册函数,用于注册ProbeCollector到exporter中,实现指标的采集
	registerCollector("probe", defaultEnabled, NewProbeCollector)
}

// 构建收集器函数,返回自定义的收集器(该收集器需要实现Update接口才能完成自定义的指标的采集)
func NewProbeCollector(logger log.Logger) (Collector, error) {
	tmpProbe := prometheus.NewDesc(
		prometheus.BuildFQName(namespace, probeSubsystem, "success"),
		"Whether probe(icmp, tcp etc.) success or not.",
		[]string{"module", "target"}, nil,
	)

	return &probeCollector{
		probeSuccess: tmpProbe,
		logger:       logger,
	}, nil
}

// 收集器的Update接口,实现自定义指标的更新,由prometheus根据其定义的时间间隔来采集数据
func (p *probeCollector) Update(ch chan<- prometheus.Metric) error {
	// 采用读取配置文件的方式,通过修改配置文件改变采集的指标属性,FakeDataFile为文件名,在collector.go中init函数初始化
	data, err := ioutil.ReadFile(FakeDataFile)
	if err != nil {
		level.Error(p.logger).Log("error", err.Error())
		return err
	}

	ret := ProbeConfig{}
	err = yaml.Unmarshal(data, &ret)
	if err != nil {
		level.Error(p.logger).Log("error", err.Error())
		return err
	}

	var ifSuccess float64
	if ret.Icmp != nil {
		for _, v := range ret.Icmp {
			if v.Success {
				ifSuccess = 1
			} else {
				ifSuccess = 0
			}
			for _, target := range v.Targets {
				ch <- prometheus.MustNewConstMetric(p.probeSuccess, prometheus.GaugeValue, ifSuccess, "icmp", target)
			}
		}
	}

	if ret.Tcp != nil {
		for _, v := range ret.Tcp {
			if v.Success {
				ifSuccess = 1
			} else {
				ifSuccess = 0
			}
			for _, target := range v.Targets {
				ch <- prometheus.MustNewConstMetric(p.probeSuccess, prometheus.GaugeValue, ifSuccess, "tcp", target)
			}
		}
	}
	return nil
}

重要的3个函数分别为init函数中的registerCollector函数,收集器函数NewProbeCollector和收集器的更新接口Update

下面是和probe探针指标相关的配置项

# This config file should be under local path or "/opt/" path
# ......

# TCP/IP
icmp:
  - targets:
      - 127.0.0.1
      - 10.0.0.0
    success: true
  - targets:
      - 10.0.0.1
    success: false
tcp:
  - targets:
      - 127.0.0.1:8080
    success: true
  - targets:
      - 127.0.0.1:80
    success: false

# ......

访问相关网页即可看到如下指标信息

# HELP node_probe_success Whether probe(icmp, tcp etc.) success or not.
# TYPE node_probe_success gauge
node_probe_success{module="icmp",target="10.0.0.0"} 1
node_probe_success{module="icmp",target="10.0.0.1"} 0
node_probe_success{module="icmp",target="127.0.0.1"} 1
node_probe_success{module="tcp",target="127.0.0.1:80"} 0
node_probe_success{module="tcp",target="127.0.0.1:8080"} 1

遇到的问题

init函数的调用

node_exporter.go文件中的参数指定配置文件路径,但在collector/collector.go中的全局变量没有接收到该参数的值。这是因为包的init函数在主函数main执行前先执行了。改用手动调用“初始化”函数的方式来设置包collector中的全局变量。
node_exporter.go中的相关代码:

func main() {
	var (
	// ......
		fakeDataConfigFile = kingpin.Flag(
			"fakedata.config-file",
			"Config file which includes fake data to collect.",
		).Default("/opt/config_fake_data.yaml").String()
	)
	// ......
	if *disableDefaultCollectors {
		collector.DisableDefaultCollectors()
	}
	// 将node_exporter主函数参数接收到的值赋给包collector中的全局变量FakeDataFile,并手动调用collector包的“初始化”函数InitCollector()
	collector.FakeDataFile = *fakeDataConfigFile
	collector.InitCollector()
	// ......
}

collector.go中的“初始化”函数

func InitCollector() {
	loadConfig()
	setDefaultConfig()
	updateConfig()
	watchConfig()
}

在collector包的“初始化”函数InitCollector中进行加载配置(loadConfig)、设置配置项的默认值(setDefaultConfig)、更新配置项(updateConfig)和监听配置文件(watchConfig)的工作,主要使用viper库:

  • "github.com/fsnotify/fsnotify"
  • "github.com/spf13/viper"

关于init函数的执行顺序:

  • 优先执行被依赖的包中的init函数
  • 在程序执行中,一个包的init函数只执行一次
  • 相同包,不同源文件中的init函数以源文件名从小到大排列后依次执行
  • 同一源文件中的多个init函数,按定义的顺序执行

结合本次自定义的监控指标,在初始化函数中注册指标,但包中的全局变量FakeDataFile是在所有初始化工作都完成后才被赋值的。不过,由于是在更新指标的值时才读取配置文件,所以对指标的更新无影响。

go的编译控制

在collector其他源文件中如:cpu_linux.go文件中包名上方会包含编译控制的标签

// ......
// +build !nocpu

package collector
//......

在实现自定义的指标时,除非有特别需要,否则无需包含编译控制的标签。
另外,如果只有// +build字段,而没有任何标签,执行go build时同样会忽略该源文件。
除通过标签进行编译控制外,还可以通过文件后缀实现编译控制,如cpu_linux.go等。

更多关于go的编译控制,参考:

服务启动失败之prometheus.NewDesc()

以服务方式启动node_exporter失败,运行journalctl -u node_exporter.service查看错误日志,发现panic: “” is not a valid metric name。在项目中搜索该错误信息,定位在func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc,该函数明确指出fqName must not be empty.。所以,在自定义的指标中搜索空字符串,找到问题来源:

func NewSchedulerCollector(logger log.Logger) (Collector, error) {
	const namespace = "scheduler"

	schedulerStatus := prometheus.NewDesc(
		// you can let namespace and subsystem be empty, but name should not be empty.
		prometheus.BuildFQName(namespace, "status", ""),
		"the status of scheduler, 1 means normal",
		nil,
		nil,
	)
	// ......

其中,func BuildFQName(namespace, subsystem, name string) string接收3个字符串,返回以_拼接后的字符串,当name为空时会直接返回空字符串;也就是说,传入的参数至少要保证参数name不为空。调整代码段为prometheus.BuildFQName(namespace, "", "status"),,重新编译后,运行成功。

node_exporter.service

[Unit]
Description=node exporter service
After=network.target
Wants=network.target

[Service]
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100

Restart=always
RestartSec=20
TimeoutSec=300
User=root
Group=root
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

可执行文件node_exporter在目录/usr/local/bin/下,服务文件node_exporter.service在目录/etc/systemd/system/下。其他启动参数可通过node_exporter -h查看。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值