自定义 exporter 开发
2023-09-27
开发一个自定义 exporter
首先我们有个需求是要监控 Bond 网卡数量,趁这个机会开发一个自定义 exporter
先编写 main.go 注册 Prometheus 的 Metrics
// main.go
package main
import (
"fmt"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/klog"
"bond-exporter/pkg"
)
func main() {
// 使用 prometheus 的 NewGaugeVec 初始化自定义指标
bondNumberMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "node_bond_number_total",
Help: "Bond interface number total",
}, []string{"interface", "hostname"})
bondSpeedMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "node_bond_speed_gauge",
Help: "Bond interface speed gauge",
}, []string{"interface", "hostname"})
// 把全部指标注册进 Prometheus
prometheus.MustRegister(bondNumberMetric, bondSpeedMetric)
bond, err := pkg.NewBond()
if err != nil {
panic(err)
}
hostname := bond.HostName()
// 协程死循环执行采集任务,拿到值后写入指标
go func() {
for {
for k, v := range bond.BondNumber() {
bondNumberMetric.WithLabelValues(k, hostname).Set(float64(v))
}
for k, v := range bond.BondSpeed() {
bondSpeedMetric.WithLabelValues(k, hostname).Set(float64(v))
}
time.Sleep(time.Second * 3)
}
}()
// 注册 api 函数
http.Handle("/metrics", promhttp.Handler())
klog.Infof("prepare to start the service, the port is %d/metrics \n", bond.Port)
// 启动
klog.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", bond.Port), nil))
}
再编写业务代码
// /pkg/bond.go
package pkg
import (
"bufio"
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"k8s.io/klog"
)
type Bond struct {
Port int
BondInterface string
BondInterfaces []string
}
func (b *Bond) HostName() string {
hostname, err := os.Hostname()
if err != nil {
return ""
}
parts := strings.Split(hostname, ".")
if len(parts) >= 2 {
return strings.Join(parts[:len(parts)-1], ".")
} else {
return hostname
}
}
func (b *Bond) BondNumber() map[string]int {
bondMap := make(map[string]int)
for _, bondName := range b.BondInterfaces {
bondMap[bondName] = 0
file, err := os.ReadFile("/proc/net/bonding/" + bondName)
if err != nil {
klog.Errorf("os.ReadFile /proc/net/bonding/%s failed, because: %s \n", bondName, err.Error())
continue
}
rawProcFile := strings.TrimSpace(string(file))
splitIndex := strings.Index(rawProcFile, "Slave Interface:")
if splitIndex == -1 {
splitIndex = len(rawProcFile)
}
slavePart := rawProcFile[splitIndex:]
scanner := bufio.NewScanner(strings.NewReader(slavePart))
var count = 0
for scanner.Scan() {
line := scanner.Text()
stats := strings.Split(line, ":")
if len(stats) < 2 {
continue
}
name := strings.TrimSpace(stats[0])
value := strings.TrimSpace(stats[1])
if strings.Contains(name, "MII Status") && value == "up" {
count++
}
}
bondMap[bondName] = count
}
return bondMap
}
func (b *Bond) BondSpeed() map[string]int {
bondMap := make(map[string]int)
for _, bondName := range b.BondInterfaces {
bondMap[bondName] = 0
cmd := exec.Command("bash", "-c", fmt.Sprintf("ethtool %s", bondName))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
klog.Errorf("cmd.Start bash -c ethtool %s failed, because: %s \n", bondName, err.Error())
continue
}
_ = cmd.Wait()
if stderr.String() != "" {
klog.Errorf("stderr.String failed, because there is content: %s \n", stderr.String())
continue
}
if stdout.String() != "" {
rawProcFile := strings.TrimSpace(stdout.String())
scanner := bufio.NewScanner(strings.NewReader(rawProcFile))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "Speed:") {
re := regexp.MustCompile(`(\d+)Mb/s`)
matches := re.FindStringSubmatch(line)
if len(matches) >= 2 {
speed, err := strconv.Atoi(matches[1])
if err != nil {
klog.Errorf("strconv.Atoi failed, because: %s \n", err)
continue
}
bondMap[bondName] = speed
}
}
}
}
}
return bondMap
}
func (b *Bond) listInterfaces() error {
if b.BondInterface != "" {
b.BondInterfaces = strings.Split(b.BondInterface, ",")
} else {
paths, err := filepath.Glob("/proc/net/bonding/*")
if err != nil {
return err
}
for _, p := range paths {
b.BondInterfaces = append(b.BondInterfaces, filepath.Base(p))
}
}
return nil
}
func (b *Bond) newBondFlags() {
flag.StringVar(&b.BondInterface, "interface", b.BondInterface, "please input bond0,bond1,bond2 format")
flag.IntVar(&b.Port, "port", b.Port, "please input bond0,bond1,bond2 format")
flag.Parse()
}
func NewBond() (*Bond, error) {
bond := &Bond{
Port: 10000,
}
bond.newBondFlags()
if err := bond.listInterfaces(); err != nil {
return nil, err
}
return bond, nil
}