貔貅云原生

貔貅云原生

自定义 exporter 开发

189
0
0
2023-09-27
自定义 exporter 开发

开发一个自定义 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
}