前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一个Prometheus exporter

实现一个Prometheus exporter

作者头像
你大哥
发布2022-11-23 14:36:20
1.2K0
发布2022-11-23 14:36:20
举报
文章被收录于专栏:容器云实践

Prometheus 官方和社区提供了非常多的exporter,涵盖数据库、中间件、OS、存储、硬件设备等,具体可查看exporters[1]、exporterhub.io[2],通过这些 exporter 基本可以覆盖80%的监控需求,依然有小部分需要通过自定义脚本或者定制、修改社区exporter实现。本文我们将学习如何通过go编写一个简单的expoter用于暴露OS的负载。

要实现的三个load指标如下:

exporter的核心是http服务,对外暴露exporter本身运行时指标和监控信息。我们可以直接通过 net/http暴力实现,更好的方式是使用Prometheus 官方提供的client library 来简化一部分工作。

client library官方支持语言:

  • Go
  • Java or Scala
  • Python
  • Ruby
  • Rust

也有社区支持的其他语言库如C、C++、PHP等

获取数据源


在使用client library暴露数据之前,我们得先找到数据源,以linux为例要获取系统负载我们可以读取/proc目录下的loadavg文件。涉及到各类操作系统指标的获取可以参考官方的node-exporter[3],这里我们给他写成load包,等会直接调用GetLoad()就能拿到数据了。

代码语言:javascript
复制
package collect

import (
        "fmt"
        "io/ioutil"
        "strconv"
        "strings"
)

// The path of the proc filesystem.
var procPath = "/proc/loadavg"

// Read loadavg from /proc.
func GetLoad() (loads []float64, err error) {
        data, err := ioutil.ReadFile(procPath)
        if err != nil {
                return nil, err
        }
        loads, err = parseLoad(string(data))
        if err != nil {
                return nil, err
        }
        return loads, nil
}

// Parse /proc loadavg and return 1m, 5m and 15m.
func parseLoad(data string) (loads []float64, err error) {
        loads = make([]float64, 3)
        parts := strings.Fields(data)
        if len(parts) < 3 {
                return nil, fmt.Errorf("unexpected content in %s", procPath)
        }
        for i, load := range parts[0:3] {
                loads[i], err = strconv.ParseFloat(load, 64)
                if err != nil {
                        return nil, fmt.Errorf("could not parse load '%s': %w", load, err)
                }
        }
        return loads, nil
}

通过client_golang暴露指标


开通我们提到exporter要暴露的指标包含两部分,一是本身的运行时信息,另一个监控的metrics。而运行时信息 client_golang已经帮我们实现了,我们要做的是通过 client_golang包将监控数据转换为metrics后再暴露出来。

一个最基础使用 client_golang包示例如下:

代码语言:javascript
复制
package main

import (
        "net/http"

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

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

promhttp.Handler()封装了本身的 go 运行时 metrics,并按照metircs后接value的格式在前端输出。

当我们访问2112端口的metrics路径时得到如下数据:

代码语言:javascript
复制
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 7
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.15.14"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
...

如何暴露自定义metrics呢?

先看如下的示例:

代码语言:javascript
复制
package main

import (
  "net/http"
  "time"
  "log"

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

func recordMetrics() {
  go func() {
    for {
      opsProcessed.Inc()
      time.Sleep(2 * time.Second)
    }
  }()
}

var (
  opsProcessed = prometheus.NewCounter(prometheus.CounterOpts{
    Namespace: "myapp",
    Name:      "processed_ops_total",
    Help:      "The total number of processed events",
  })
)

func main() {
  prometheus.MustRegister(opsProcessed)
  recordMetrics()

  http.Handle("/metrics", promhttp.Handler())
  log.Print("export /metrics on port :8085")
  http.ListenAndServe(":8085", nil)
}

示例来自于官方仓库,做了稍加修改。可以看到使用NewCounter方法可以很快地帮我们创建一个Prometheus Counter数据类型实例。

Counter接口的定义包含了Counter本身的特性-只能增加即Inc和Add,同时还包含Meterics、Collector接口

Collector还包含2个方法,待会我们写自己的Collector时需要实现这两个方法。

代码语言:javascript
复制
type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

CounterOpts 来源于metrics.go 的Ops结构体定义了构成metrics的基本结构。

接着将opsProcessed这个Counter进行注册,所谓注册也就是让 Handler跟踪这个Counter中的metircs和collector

运行后,访问/metircs可以看到自定义指标myappprocessedops_total通过定时的Inc()调用来更新value

代码语言:javascript
复制
# HELP myapp_processed_ops_total The total number of processed events
# TYPE myapp_processed_ops_total counter
myapp_processed_ops_total 15

下面我们通过自定义collector实现一个简易的exporter

目录结构如下:

代码语言:javascript
复制
# tree .
.
├── collect
│   ├── collector.go
│   └── loadavg.go
├── go.mod
├── go.sum
└── main.go

loadavg.go即上面的获取数据源。

collector.go如下:

代码语言:javascript
复制
package collect

import (
  "log"

  "github.com/prometheus/client_golang/prometheus"
)

var namespace = "node"


type loadavgCollector struct {
  metrics []typedDesc
}

type typedDesc struct {
  desc      *prometheus.Desc
  valueType prometheus.ValueType
}


func NewloadavgCollector() *loadavgCollector {
  return &loadavgCollector{
    metrics: []typedDesc{
      {prometheus.NewDesc(namespace+"_load1", "1m load average.", nil, nil), prometheus.GaugeValue},
      {prometheus.NewDesc(namespace+"_load5", "5m load average.", , nil), prometheus.GaugeValue},
      {prometheus.NewDesc(namespace+"_load15", "15m load average.", nil, nil), prometheus.GaugeValue},
    },
  }
}

//Each and every collector must implement the Describe function.
//It essentially writes all descriptors to the prometheus desc channel.
func (collector *loadavgCollector) Describe(ch chan<- *prometheus.Desc) {

  //Update this section with the each metric you create for a given collector
  ch <- collector.metrics[1].desc
}

//Collect implements required collect function for all promehteus collectors
func (collector *loadavgCollector) Collect(ch chan<- prometheus.Metric) {

  //Implement logic here to determine proper metric value to return to prometheus
  //for each descriptor or call other functions that do so.
  loads, err := GetLoad()
  if err != nil {
    log.Print("get loadavg error: ", err)
  }

  //Write latest value for each metric in the prometheus metric channel.
  //Note that you can pass CounterValue, GaugeValue, or UntypedValue types here.

  for i, load := range loads {
    ch <- prometheus.MustNewConstMetric(collector.metrics[i].desc, prometheus.GaugeValue, load)
  }

}

collector中每一个要暴露的metrics都需要包含一个metrics描述即desc,都需要符合prometheus.Desc结构,我们可以直接使用NewDesc来创建。这里我们创建了三个metircsname分别为nodeload1、nodeload5、node15以及相应的描述,也可以加上对应的label。

接着实现collector的两个方法Describe、Collect分别写入对应的发送channel,其中prometheus.Metric的通道传入的值还包括三个load的value

最后在主函数中注册collector

代码语言:javascript
复制
prometheus.MustRegister(collect.NewloadavgCollector())

在Prometheus每个请求周期到达时都会使用GetLoad()获取数据,转换为metircs,发送给Metrics通道,http Handler处理和返回。


实现一个指标丰富、可靠性高的exporter感觉还是有一些困难的,需要对Go的一些特性以及Prometheus client包有较深入的了解。本文是对exporter编写的简单尝试,如实现逻辑、方式或理解不准确可参考开源exporter和官方文档。

文章涉及代码可查看:exporter[4]

通过博客阅读:iqsing.github.io[5]

参考

[1] exporters: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exporters.md

[2] exporterhub.io: https://exporterhub.io/

[3]

node-exporter: https://github.com/prometheus/node_exporter

[4]

exporter: https://github.com/iqsing/code/tree/main/exporter

[5]

iqsing.github.io: https://iqsing.github.io/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 容器云实践 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 获取数据源
  • 通过client_golang暴露指标
相关产品与服务
Prometheus 监控服务
Prometheus 监控服务(TencentCloud Managed Service for Prometheus,TMP)是基于开源 Prometheus 构建的高可用、全托管的服务,与腾讯云容器服务(TKE)高度集成,兼容开源生态丰富多样的应用组件,结合腾讯云可观测平台-告警管理和 Prometheus Alertmanager 能力,为您提供免搭建的高效运维能力,减少开发及运维成本。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档