如何用go自定义prometheus的exporter

Posted 哼嘿哈嘿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何用go自定义prometheus的exporter相关的知识,希望对你有一定的参考价值。

介绍

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

官方支持语言:

metric的类型

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

  1. Counter(计数器):只增不减的计数器,用于记录事件发生的次数,例如请求数量、错误数量等。
  2. Gauge(仪表盘):可增可减的指标,用于记录当前的状态,例如 CPU 使用率、内存使用量等。
  3. Histogram(直方图):用于记录数据的分布情况,例如请求响应时间的分布情况。
  4. 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"),
	

使用 Go 开发 Prometheus Exporter

Exporter 是 Prometheus 监控的核心,如果你遇到一些应用不存在相应的 Exporter,那么我们可以自己去编写 Exporter。下面我们简单介绍如何使用 Golang 来快速编写一个 Exporter。

1. 安装 GO 和依赖包

按照 https://golang.org/doc/install 上的步骤进行安装配置 GO 环境,创建一个名为 my_first_exporter 的文件夹。

$ go mod init my_first_exporter 
$ go get github.com/prometheus/client_golang 
$ go get github.com/joho/godotenv
--> creates go.mod file
--> Installs dependency into the go.mod file

2. 创建入口点和导入依赖包

package main

import (
 "github.com/joho/godotenv"
 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promhttp"
)

3. 创建 main() 函数

func main()

4. 添加 prometheus metrics 端点,并在某个服务端口上监听

func main() {
   http.Handle("/metrics", promhttp.Handler())
   log.Fatal(http.ListenAndServe(":9101"nil))
}

5. 使用 curl 请求外部服务接口

比如我们这里监控的应用程序是 MirthConnect,所以我需要进行两个 API 接口调用:

  • 获取 channel 统计数据
  • 获取 channel id 和名称映射
curl -k --location --request GET 'https://apihost/api/channels/statistics' \
--user admin:admin

curl -k --location --request GET 'https://apihost/api/channels/idsAndNames' \
--user admin:admin

6. 将 curl 调用转换为 go http 调用,并解析结果

如果你是 Go 新手,这应该是最困难的一步。对于我这里的例子,端点返回的是 XML 格式的数据,所以我必须用 "encoding/xml" 包来反序列化 XML。转换成功后意味着我的 GO 程序可以执行和 curl 命令一样的 API 调用。

7. 声明 Prometheus metrics

在 Prometheus 中,每个 metric 指标都由以下几个部分组成:metric name/metric label values/metric help text/metric type/measurement ,例如:

Example:
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code=”200"} 1.829159e+06
promhttp_metric_handler_requests_total{code=”500"
} 0
promhttp_metric_handler_requests_total{code=”503"} 0

对于应用 scrapers,我们将定义 Prometheus metrics 描述信息,其中包括 metric 名称、metric label 标签以及 metric 帮助信息。

messagesReceived = prometheus.NewDesc(
 prometheus.BuildFQName(namespace, """messages_received_total"),
 "How many messages have been received (per channel).",
 []string{"channel"}, nil,
)

8. 定义一个结构体实现 Prometheus 的 Collector 接口

Prometheus 的 client 库提供了实现自定义 Exportor 的接口,Collector 接口定义了两个方法 Describe 和 Collect,实现这两个方法就可以暴露自定义的数据:

  • Describe(chan<- *Desc)
  • Collect(chan<- Metric)

如下所示:

type Exporter struct {
 mirthEndpoint, mirthUsername, mirthPassword string
}

func NewExporter(mirthEndpoint string, mirthUsername string, mirthPassword string) *Exporter {
 return &Exporter{
  mirthEndpoint: mirthEndpoint,
  mirthUsername: mirthUsername,
  mirthPassword: mirthPassword,
 }
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
}

9. 在 Describe 函数中,把第7步的 metric 描述信息发送给它

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
 ch <- up
 ch <- messagesReceived
 ch <- messagesFiltered
 ch <- messagesQueued
 ch <- messagesSent
 ch <- messagesErrored
}

10. 将接口调用逻辑从第6步移到 Collect 函数中

直接将采集的数据发送到 prometheus.Metric 通道中。

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
 channelIdNameMap, err := e.LoadChannelIdNameMap()
 if err != nil {
  ch <- prometheus.MustNewConstMetric(
   up, prometheus.GaugeValue, 0,
  )
  log.Println(err)
  return
 }
 ch <- prometheus.MustNewConstMetric(
  up, prometheus.GaugeValue, 1,
 )

 e.HitMirthRestApisAndUpdateMetrics(channelIdNameMap, ch)
}

当执行 api 调用时,确保使用prometheus.MustNewConstMetric(prometheus.Desc, metric type, measurement)发送测量值,如果你需要传入额外的标签,可以像下面这样在参数列表的后面加入:

channelError, _ := strconv.ParseFloat(channelStatsList.Channels[i].Error, 64)
ch <- prometheus.MustNewConstMetric(
 messagesErrored, prometheus.GaugeValue, channelError, channelName,
)

11. 在 main 函数中声明 exporter

exporter := NewExporter(mirthEndpoint, mirthUsername, mirthPassword)
prometheus.MustRegister(exporter)

到这里其实这个 Exporter 就可以使用了,每次访问 metrics 路由的时候,它会执行 api 调用,并以 Prometheus Text 文本格式返回数据。下面的步骤主要是方便部署了。

12. 将硬编码的 api 路径放到 flag 中

var (
listenAddress = flag.String("web.listen-address"":9141",
 "Address to listen on for telemetry")
metricsPath = flag.String("web.telemetry-path""/metrics",
 "Path under which to expose metrics")
)
func main() {
   flag.Parse()
   ...
   http.Handle(*metricsPath, promhttp.Handler())
   log.Fatal(http.ListenAndServe(*listenAddress, nil))
}

13. 将凭证放入环境变量

如果应用端点改变了或者登录凭证改变了怎么办?我们可以从环境变量中来加载这些数据,在这个例子中,我们使用 godotenv 这个包来帮助将变量值存储在本地的一个目录中:

import (
  "os"
)
func main() {
 err := godotenv.Load()
 if err != nil {
  log.Println("Error loading .env file, assume env variables are set.")
 }
 mirthEndpoint := os.Getenv("MIRTH_ENDPOINT")
 mirthUsername := os.Getenv("MIRTH_USERNAME")
 mirthPassword := os.Getenv("MIRTH_PASSWORD")
}

整个 Exporter 完整的代码如下所示:

package main

import (
 "crypto/tls"
 "encoding/xml"
 "flag"
 "io/ioutil"
 "log"
 "net/http"
 "os"
 "strconv"

 "github.com/joho/godotenv"
 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promhttp"
)

/*
<map>
  <entry>
    <string>101af57f-f26c-40d3-86a3-309e74b93512</string>
    <string>Send-Email-Notification</string>
  </entry>
</map>
*/

type ChannelIdNameMap struct {
 XMLName xml.Name       `xml:"map"`
 Entries []ChannelEntry `xml:"entry"`
}
type ChannelEntry struct {
 XMLName xml.Name `xml:"entry"`
 Values  []string `xml:"string"`
}

/*
<list>
  <channelStatistics>
    <serverId>c5e6a736-0e88-46a7-bf32-5b4908c4d859</serverId>
    <channelId>101af57f-f26c-40d3-86a3-309e74b93512</channelId>
    <received>0</received>
    <sent>0</sent>
    <error>0</error>
    <filtered>0</filtered>
    <queued>0</queued>
  </channelStatistics>
</list>
*/

type ChannelStatsList struct {
 XMLName  xml.Name       `xml:"list"`
 Channels []ChannelStats `xml:"channelStatistics"`
}
type ChannelStats struct {
 XMLName   xml.Name `xml:"channelStatistics"`
 ServerId  string   `xml:"serverId"`
 ChannelId string   `xml:"channelId"`
 Received  string   `xml:"received"`
 Sent      string   `xml:"sent"`
 Error     string   `xml:"error"`
 Filtered  string   `xml:"filtered"`
 Queued    string   `xml:"queued"`
}

const namespace = "mirth"
const channelIdNameApi = "/api/channels/idsAndNames"
const channelStatsApi = "/api/channels/statistics"

var (
 tr = &http.Transport{
  TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 }
 client = &http.Client{Transport: tr}

 listenAddress = flag.String("web.listen-address"":9141",
  "Address to listen on for telemetry")
 metricsPath = flag.String("web.telemetry-path""/metrics",
  "Path under which to expose metrics")

 // Metrics
 up = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """up"),
  "Was the last Mirth query successful.",
  nilnil,
 )
 messagesReceived = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """messages_received_total"),
  "How many messages have been received (per channel).",
  []string{"channel"}, nil,
 )
 messagesFiltered = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """messages_filtered_total"),
  "How many messages have been filtered (per channel).",
  []string{"channel"}, nil,
 )
 messagesQueued = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """messages_queued"),
  "How many messages are currently queued (per channel).",
  []string{"channel"}, nil,
 )
 messagesSent = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """messages_sent_total"),
  "How many messages have been sent (per channel).",
  []string{"channel"}, nil,
 )
 messagesErrored = prometheus.NewDesc(
  prometheus.BuildFQName(namespace, """messages_errored_total"),
  "How many messages have errored (per channel).",
  []string{"channel"}, nil,
 )
)

type Exporter struct {
 mirthEndpoint, mirthUsername, mirthPassword string
}

func NewExporter(mirthEndpoint string, mirthUsername string, mirthPassword string) *Exporter {
 return &Exporter{
  mirthEndpoint: mirthEndpoint,
  mirthUsername: mirthUsername,
  mirthPassword: mirthPassword,
 }
}

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
 ch <- up
 ch <- messagesReceived
 ch <- messagesFiltered
 ch <- messagesQueued
 ch <- messagesSent
 ch <- messagesErrored
}

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
 channelIdNameMap, err := e.LoadChannelIdNameMap()
 if err != nil {
  ch <- prometheus.MustNewConstMetric(
   up, prometheus.GaugeValue, 0,
  )
  log.Println(err)
  return
 }
 ch <- prometheus.MustNewConstMetric(
  up, prometheus.GaugeValue, 1,
 )

 e.HitMirthRestApisAndUpdateMetrics(channelIdNameMap, ch)
}

func (e *Exporter) LoadChannelIdNameMap() (map[string]string, error) {
 // Create the map of channel id to names
 channelIdNameMap := make(map[string]string)

 req, err := http.NewRequest("GET", e.mirthEndpoint+channelIdNameApi, nil)
 if err != nil {
  return nil, err
 }

 // This one line implements the authentication required for the task.
 req.SetBasicAuth(e.mirthUsername, e.mirthPassword)
 // Make request and show output.
 resp, err := client.Do(req)
 if err != nil {
  return nil, err
 }

 body, err := ioutil.ReadAll(resp.Body)
 resp.Body.Close()
 if err != nil {
  return nil, err
 }
 // fmt.Println(string(body))

 // we initialize our array
 var channelIdNameMapXML ChannelIdNameMap
 // we unmarshal our byteArray which contains our
 // xmlFiles content into 'users' which we defined above
 err = xml.Unmarshal(body, &channelIdNameMapXML)
 if err != nil {
  return nil, err
 }

 for i := 0; i < len(channelIdNameMapXML.Entries); i++ {
  channelIdNameMap[channelIdNameMapXML.Entries[i].Values[0]] = channelIdNameMapXML.Entries[i].Values[1]
 }

 return channelIdNameMap, nil
}

func (e *Exporter) HitMirthRestApisAndUpdateMetrics(channelIdNameMap map[string]string, ch chan<- prometheus.Metric) {
 // Load channel stats
 req, err := http.NewRequest("GET", e.mirthEndpoint+channelStatsApi, nil)
 if err != nil {
  log.Fatal(err)
 }

 // This one line implements the authentication required for the task.
 req.SetBasicAuth(e.mirthUsername, e.mirthPassword)
 // Make request and show output.
 resp, err := client.Do(req)
 if err != nil {
  log.Fatal(err)
 }

 body, err := ioutil.ReadAll(resp.Body)
 resp.Body.Close()
 if err != nil {
  log.Fatal(err)
 }
 // fmt.Println(string(body))

 // we initialize our array
 var channelStatsList ChannelStatsList
 // we unmarshal our byteArray which contains our
 // xmlFiles content into 'users' which we defined above
 err = xml.Unmarshal(body, &channelStatsList)
 if err != nil {
  log.Fatal(err)
 }

 for i := 0; i < len(channelStatsList.Channels); i++ {
  channelName := channelIdNameMap[channelStatsList.Channels[i].ChannelId]

  channelReceived, _ := strconv.ParseFloat(channelStatsList.Channels[i].Received, 64)
  ch <- prometheus.MustNewConstMetric(
   messagesReceived, prometheus.GaugeValue, channelReceived, channelName,
  )

  channelSent, _ := strconv.ParseFloat(channelStatsList.Channels[i].Sent, 64)
  ch <- prometheus.MustNewConstMetric(
   messagesSent, prometheus.GaugeValue, channelSent, channelName,
  )

  channelError, _ := strconv.ParseFloat(channelStatsList.Channels[i].Error, 64)
  ch <- prometheus.MustNewConstMetric(
   messagesErrored, prometheus.GaugeValue, channelError, channelName,
  )

  channelFiltered, _ := strconv.ParseFloat(channelStatsList.Channels[i].Filtered, 64)
  ch <- prometheus.MustNewConstMetric(
   messagesFiltered, prometheus.GaugeValue, channelFiltered, channelName,
  )

  channelQueued, _ := strconv.ParseFloat(channelStatsList.Channels[i].Queued, 64)
  ch <- prometheus.MustNewConstMetric(
   messagesQueued, prometheus.GaugeValue, channelQueued, channelName,
  )
 }

 log.Println("Endpoint scraped")
}

func main() {
 err := godotenv.Load()
 if err != nil {
  log.Println("Error loading .env file, assume env variables are set.")
 }

 flag.Parse()

 mirthEndpoint := os.Getenv("MIRTH_ENDPOINT")
 mirthUsername := os.Getenv("MIRTH_USERNAME")
 mirthPassword := os.Getenv("MIRTH_PASSWORD")

 exporter := NewExporter(mirthEndpoint, mirthUsername, mirthPassword)
 prometheus.MustRegister(exporter)

 http.Handle(*metricsPath, promhttp.Handler())
 http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(`<html>
             <head><title>Mirth Channel Exporter</title></head>
             <body>
             <h1>Mirth Channel Exporter</h1>
             <p><a href='`
 + *metricsPath + `'>Metrics</a></p>
             </body>
             </html>`
))
 })
 log.Fatal(http.ListenAndServe(*listenAddress, nil))
}

14. 编写一个 Makefile 文件,方便在不同平台上快速构建

Makefile 可以让你在开发过程中省去很多多余的操作,比如我们要构建多个平台的构建程序,可以创建如下所示的 Makefile 文件。

linux:
   GOOS=linux GOARCH=amd64 go build
mac:
   GOOS=darwin GOARCH=amd64 go build

只要调用 make macmake linux 命令就可以看到不同的可执行文件出现。

15. 编写一个 service 文件,将这个 go 应用作为守护进程运行

我们可以为这个 Exporter 编写一个 service 文件或者 Dockerfile 文件来管理该应用,比如这里我们直接在 Centos 7 上用 systemd 来管理该应用。这可以编写一个如下所示的 service 文件:

[Unit]
Description=mirth channel exporter
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
WorkingDirectory=/mirth/mirthconnect
EnvironmentFile=/etc/sysconfig/mirth_channel_exporter
ExecStart=/mirth/mirthconnect/mirth_channel_exporter

[Install]
WantedBy=multi-user.target

到这里就完成了使用 Golang 编写一个简单的 Prometheus Exporter。

原文链接:https://medium.com/teamzerolabs/15-steps-to-write-an-application-prometheus-exporter-in-go-9746b4520e26

  点击屏末  | 即刻学习

以上是关于如何用go自定义prometheus的exporter的主要内容,如果未能解决你的问题,请参考以下文章

prometheus node-exporter增加新的自定义监控项

Linux-监控三剑客之prometheus

#yyds干货盘点#Prometheus 之 Exporter 详解

性能监控之 blackbox_exporter+Prometheus+Grafana 实现网络探测

Prometheus 没有抓取 stats d exporter

prometheus使用三(自定义监控指标实现)