背景
监控告警项目中,采用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等。
服务启动失败之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
查看。