使用go自定义prometheus的exporter

介绍

prometheus中如果要监控服务器和应用的各种指标,需要用各种各样的exporter服务,例如node_exportesmysql_exportespgsql_exportes等。这些都是官方或者第三方已经提供好的。但是如果自己想要监控一些其它exportes没有的指标,则就需要自己去构建一个属于自己的exportes,好在官方提供相关的库,目前支持以下语言:

官方支持语言:

metric的类型

在开始之前需要了解下metric的类型划分

  • Counter(计数器):只增不减的计数器,用于记录事件发生的次数,例如请求数量、错误数量等。
  • Gauge(仪表盘):可增可减的指标,用于记录当前的状态,例如 CPU 使用率、内存使用量等。
  • Histogram(直方图):用于记录数据的分布情况,例如请求响应时间的分布情况。
  • Summary(摘要):与 Histogram 类似,但是它会在客户端计算出一些摘要信息,例如平均值、标准差等。

类型详解

Guage

Gauge的特点:

1. 可以任意上升或下降,没有固定的范围限制。
2. 可以被设置为任何值,不像Counter只能递增。
3. 可以被用来表示瞬时值或累计值。
4. 可以被用来表示单个实体的状态,例如单个服务器的CPU使用率。
5. 可以被用来表示多个实体的总体状态,例如整个集群的CPU使用率。

Gauge的使用:

1. Gauge的值可以通过set()方法进行设置。
2. Gauge的值可以通过inc()和dec()方法进行增加或减少。
3. Gauge的值可以通过add()方法进行增加或减少指定的值。
4. Gauge的值可以通过set_to_current_time()方法设置为当前时间戳。
5. Gauge的值可以通过observe()方法进行设置,这个方法可以用来记录样本值和时间戳。

Counter

Counter的特点:

1. Counter只能增加,不能减少或重置。
2. Counter的值是一个非负整数。
3. Counter的值可以随时间增加,但不会减少。
4. Counter的值在重启Prometheus时会重置为0。
5. Counter的值可以被多个Goroutine同时增加,不需要加锁。
6. Counter的值可以被推送到Pushgateway中,用于监控非Prometheus监控的数据。

Counter的使用方法:

1. 在程序中定义一个Counter对象,并初始化为0。
2. 当需要记录计数时,调用Counter的Inc()方法增加计数器的值。
3. 将Counter对象暴露给Prometheus,使其能够收集数据。
4. 在Prometheus中定义一个相应的指标,并将Counter对象与该指标关联。

示例代码:

import (
 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promauto"
)

// 定义一个Counter对象
var requestCounter = promauto.NewCounter(prometheus.CounterOpts{
 Name: "http_requests_total",
 Help: "The total number of HTTP requests",
})

// 记录请求计数
func handleRequest() {
 requestCounter.Inc()
 // 处理请求
}

在上面的代码中,我们定义了一个名为http_requests_totalCounter对象,用于记录HTTP请求的总数。每当处理一个请求时,我们调用requestCounter.Inc()方法增加计数器的值。最后,我们将Counter对象暴露给Prometheus,并在Prometheus中定义了一个名为http_requests_total的指标,将Counter对象与该指标关联。这样,Prometheus就能够收集和展示http_requests_total指标的数据了

Histogram

Histogram是一种Prometheus指标类型,用于度量数据的分布情况。它将数据分成一系列桶(bucket),每个桶代表一段范围内的数据。每个桶都有一个计数器(counter),用于记录该范围内的数据数量。在Prometheus中,Histogram指标类型的名称以“_bucket”结尾。

Histogram指标类型通常用于度量请求延迟、响应大小等连续型数据。例如,我们可以使用Histogram指标类型来度量Web应用程序的请求延迟。我们可以将请求延迟分成几个桶,例如0.1秒、0.5秒、1秒、5秒、10秒、30秒等。每个桶都记录了在该范围内的请求延迟的数量。

Histogram指标类型还有两个重要的计数器:sum和count。sum用于记录所有数据的总和,count用于记录数据的数量。通过这两个计数器,我们可以计算出平均值和其他统计信息。

在Prometheus中,我们可以使用histogram_quantile函数来计算某个百分位数的值。例如,我们可以使用histogram_quantile(0.9, my_histogram)来计算my_histogram指标类型中90%的请求延迟的值。

总之,Histogram指标类型是一种非常有用的指标类型,可以帮助我们了解数据的分布情况,从而更好地监控和优化应用程序的性能。

Summary

Summary是Prometheus中的一种指标类型,用于记录一组样本的总和、计数和分位数。它适用于记录耗时、请求大小等具有较大变化范围的指标。

Summary指标类型包含以下几个指标:

1. sum:样本值的总和。
2. count:样本值的计数。
3. quantile:分位数。

其中,sum和count是必须的,而quantile是可选的。
在使用Summary指标类型时,需要注意以下几点:

1. 每个Summary指标类型都会记录所有样本的总和和计数,因此它们的值会随时间变化而变化。
2. 每个Summary指标类型都可以记录多个分位数,例如50%、90%、95%、99%等。
3. 每个Summary指标类型都可以设置一个时间窗口,用于计算分位数。
4. 每个Summary指标类型都可以设置一个最大样本数,用于限制内存使用。
5. 每个Summary指标类型都可以设置一个标签集,用于区分不同的实例。
总之,Summary指标类型是一种非常有用的指标类型,可以帮助我们更好地了解系统的性能和健康状况

示例

以下示例实现了通过传入的端口号监听对应的进程,并输出进程的一些信息,如pid、cmdline、exe、ppid、内存使用等信息(通过读/proc/pid/目录下的文件来实现),后面如果有其他需要可自行修改。因为写的比较仓促,这里也不详细介绍代码中的含义,有兴趣的可以留言,或者直接拿走代码试试。

目录结构是

|-main.go
|-go.mod
|-go.sum
|-collector
 |-- exec.go
 |-- port.go

main.go

package main

import (
	"fmt"
	"net/http"
	"time"

	"exporter/collector"

	"github.com/alecthomas/kingpin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

// 定义命令行参数
var (
	ticker = kingpin.Flag("ticker", "Interval for obtaining indicators.").Short('t').Default("5").Int()
	mode = kingpin.Flag("mode", "Using netstat or lsof for specified port pid information.").Short('m').Default("netstat").String()
	port = kingpin.Flag("port", "This service is to listen the port.").Short('p').Default("9527").String()
	ports = kingpin.Arg("ports", "The process of listening on ports.").Required().Strings()
)

func main() {
	kingpin.Version("1.1")
	kingpin.Parse()
	// 注册自身采集器
	prometheus.MustRegister(collector.NewPortCollector(*ports, *mode))
	// fmt.Printf("Would ping: %s with timeout %s \n", *mode, *ports)
	go func() {
	for {
	collector.NewPortCollector(*ports, *mode).Updata()
	time.Sleep(time.Duration(*ticker) * time.Second)
	}
	}()
	http.Handle("/metrics", promhttp.Handler())
	fmt.Println("Ready to listen on port:", *port)
	if err := http.ListenAndServe("0.0.0.0:"+*port, nil); err != nil {
	fmt.Printf("Error occur when start server %v", err)
	}
}

exec.go

package collector

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
)

var (
	order int
	awkMap = make(map[int]string)
	result = make(map[string]string)
	// 定义要在status文件里筛选的关键字
	targetList = []string{"Name", "State", "PPid", "Uid", "Gid", "VmHWM", "VmRSS"}
	targetResult = make(map[string]map[string]string)
)

func stringGrep(s string, d string) (bool, error) {
	for k, v := range d {
	if v != rune(s[k]) {
	return false, fmt.Errorf("string does not match")
	}
	}
	order = 1
	resolv, err := stringAWK(s[len(d):])
	if len(resolv) == 0 {
	return false, err
	}
	order = 0
	return true, nil
}

func stringAWK(s string) (map[int]string, error) {
	i := 0
	for k, v := range s {
	if v != rune(9) && v != rune(32) && v != rune(10) {
	i = 1
	awkMap[order] += string(v)
	} else {
	if i > 0 {
	order++
	i = 0
	}
	stringAWK(s[k+1:])
	return awkMap, nil
	}
	}
	return awkMap, fmt.Errorf("awk error")
}

func GetProcessInfo(p []string, m string) map[string]map[string]string {
	for _, port := range p {
	// 通过端口号获取进程pid信息
	// 通过组合命令行的方式执行linux命令,筛选出pid
	cmd := "sudo " + m + " -tnlp" + "|grep :" + port + "|awk '{print $NF}'|awk -F'/' '{print $1}'"
	getPid := exec.Command("bash", "-c", cmd)
	out, err := getPid.Output()
	if err != nil {
	fmt.Println("exec command failed", err)
	return nil
	}
	dir := strings.ReplaceAll(string(out), "\n", "")
	if len(dir) == 0 {
	fmt.Println("'dir' string is empty")
	return nil
	// panic("'dir' string is empty")
	}
	// fmt.Println("test_dir", dir)
	result["pid"] = dir
	// 获取命令行绝地路径
	cmdRoot := "sudo ls -l /proc/" + dir + "/exe |awk '{print $NF}'"
	getCmdRoot := exec.Command("bash", "-c", cmdRoot)
	out, err = getCmdRoot.Output()
	if err != nil {
	fmt.Println("exec getCmdRoot command failed", err)
	}
	// fmt.Println("test_cmdroot", strings.ReplaceAll(string(out), "\n", ""))
	result["cmdroot"] = strings.ReplaceAll(string(out), "\n", "")
	// 获取/proc/pid/cmdline文件内信息
	cmdline, err := os.Open("/proc/" + dir + "/cmdline")
	if err != nil {
	fmt.Println("open cmdline file error :", err)
	panic(err)
	}
	cmdlineReader, err := bufio.NewReader(cmdline).ReadString('\n')
	if err != nil && err != io.EOF {
	fmt.Println(err)
	}
	result["cmdline"] = strings.ReplaceAll(cmdlineReader, "\x00", " ")
	// 获取/proc/pid/status文件内信息
	status, err := os.Open("/proc/" + dir + "/status")
	if err != nil {
	fmt.Println("open status file error :", err)
	}

	// 执行函数返回前关闭打开的文件
	defer cmdline.Close()
	defer status.Close()

	statusReader := bufio.NewReader(status)
	if err != nil {
	fmt.Println(err)
	}

	for {
	line, err := statusReader.ReadString('\n') //注意是字符
	if err == io.EOF {
	if len(line) != 0 {
	fmt.Println(line)
	}
	break
	}
	if err != nil {
	fmt.Println("read file failed, err:", err)
	// return
	}
	for _, v := range targetList {
	istrue, _ := stringGrep(line, v)
	if istrue {
	result[v] = awkMap[2]
	// fmt.Printf("%v结果是:%v\n", v, awkMap[2])
	awkMap = make(map[int]string)
	}
	}
	}
	// fmt.Println("数据的和:", result)
	// fmt.Println("test_result", result)
	targetResult[port] = result
	// 给result map重新赋值,要不然使用的是同一个map指针,targetResult结果是一样的
	result = make(map[string]string)
	}
	// fmt.Println("test_total", targetResult)
	return targetResult
}

port.go

package collector

import (
	"sync"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/shirou/gopsutil/host"
)

var (
	isexist float64 = 1
	namespace = "own_process"
	endetail = "datails"
	endmems = "mems"
)

// 定义收集指标结构体
// 分为进程信息和内存信息
type PortCollector struct {
	ProcessDetail portMetrics
	ProcessMems portMetrics
	mutex sync.Mutex // 使用于多个协程访问共享资源的场景
	// value prometheus.Gauge
}

type portMetrics []struct {
	desc *prometheus.Desc
	value map[string]string
}

func (p *PortCollector) Describe(ch chan<- *prometheus.Desc) {
	for _, metric := range p.ProcessDetail {
	ch <- metric.desc
	}

	for _, metric := range p.ProcessMems {
	ch <- metric.desc
	}
	// ch <- p.ProcessMems
}

func (p *PortCollector) Collect(ch chan<- prometheus.Metric) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	// ch <- prometheus.MustNewConstMetric(p.ProcessMems, prometheus.GaugeValue, 0)
	for _, metric := range p.ProcessDetail {
	ch <- prometheus.MustNewConstMetric(metric.desc, prometheus.GaugeValue, isexist, metric.value["cmdroot"], metric.value["cmdline"], metric.value["Name"], metric.value["State"], metric.value["PPid"], metric.value["Uid"], metric.value["Gid"])
	}
	for _, metric := range p.ProcessMems {
	ch <- prometheus.MustNewConstMetric(metric.desc, prometheus.GaugeValue, isexist, metric.value["Name"], metric.value["pid"], metric.value["VmHWM"], metric.value["VmRSS"])
	}
}

func (p *PortCollector) Updata() {
	// Do nothing here as the value is generated in the Collect() function
}

func newMetrics(p []string, s map[string]map[string]string, u string) *portMetrics {
	host, _ := host.Info()
	hostname := host.Hostname
	var detailList, memsList portMetrics
	for _, v := range p {
	// fmt.Println(k, v)
	detailList = append(detailList, struct {
	desc *prometheus.Desc
	value map[string]string
	}{
	desc: prometheus.NewDesc(
	prometheus.BuildFQName(namespace, v, endetail),
	"Process-related information of port "+v,
	[]string{"cmdroot", "cmdline", "process_name", "status", "ppid", "ownuser", "owngroup"}, // 设置动态labels,collect函数里传来的就是这个变量的值
	prometheus.Labels{"host_name": hostname}), // 设置静态labels
	value: s[v],
	})

	memsList = append(memsList, struct {
	desc *prometheus.Desc
	value map[string]string
	}{
	desc: prometheus.NewDesc(
	prometheus.BuildFQName(namespace, v, endmems),
	"Process memory usage information of port "+v,
	[]string{"process_name", "pid", "vmhwm", "vmrss"}, // 设置动态labels,collect函数里传来的就是这个变量的值
	prometheus.Labels{"host_name": hostname}), // 设置静态labels
	value: s[v],
	})
	}
	if u == "detail" {
	return &detailList
	} else {
	return &memsList
	}
}

// NewPortCollector 创建port收集器,返回指标信息
func NewPortCollector(p []string, m string) *PortCollector {
	final := GetProcessInfo(p, m)
	// fmt.Printf("test_fanal:%#v", len(final))
	if len(final) == 0 {
	isexist = 0
	} else {
	isexist = 1
	}
	return &PortCollector{
	ProcessDetail: *newMetrics(p, final, "detail"),
	ProcessMems: *newMetrics(p, final, "mems"),
	}
}
作者:哼嘿哈嘿原文地址:https://blog.csdn.net/weixin_44208042/article/details/129790750

%s 个评论

要回复文章请先登录注册