自行创建的 Prometheus exporter

首先

今天是品川降临日历2019年的第22天。

普罗米修斯是众所周知的一种监控软件,它可以访问受监视对象并收集数据,以进行监视和通知。受监视对象可以是 exporter,它从数据中获取响应并发送给普罗米修斯,还可以是批处理、应用程序等通过 Push 发送的数据,这些数据将被存储并准备好 PushGateway,然后响应给普罗米修斯。
exporter 包括官方和非官方版本,并且有多个可供使用。同时,也有为了自行创建 exporter 的客户端库可供使用。
虽然目前没有制作自己的 exporter 的机会,但为了未来的某一天,我想利用现有的客户端库尝试自己动手制作一个。

获取度量

暂时创建一个用于获取Linux内存使用率的导出器。
对于Linux,可以通过查看/proc/meminfo文件来了解内存使用率,因此我们将设计它以读取此文件。

我试着自己做。

hirano00o/sample-exporter:Prometheus的样本导出器
在创建过程中,参考了客户端库的示例和node_exporter。

在node_exporter中,我们过去通过参数指定要获取的信息并进行过滤,但这次不会使用与获取信息有关的参数,所以变成了这样。

func main() {
    flag.Parse()

    c, err := collector.NewSampleCollector()
    if err != nil {
        log.Fatal(err)
    }
    // NewしたCollectorを登録する。ここに登録したもののメトリクスが取得できる
    prometheus.MustRegister(c)

    http.Handle("/metrics", promhttp.Handler())

    log.Println("Listening on ", *addr)
    if err := http.ListenAndServe(*addr, nil); err != nil {
        log.Fatal(err)
    }
}

collect.go 是一个基础框架,可以让您在需要获取除了内存以外的信息时轻松添加。

var (
...
    factories      = make(map[string]func() (Collector, error))
    collectorState = make(map[string]int)
)

// memory.goでinit()で呼ぶ
func registCollector(collector string, f func() (Collector, error)) {
    factories[collector] = f
    collectorState[collector] = 0
}

type SampleCollector struct {
    Collectors map[string]Collector  // keyがstring型, valueはCollectorと言う名のinterface型
}

// memory.goでも利用するし、メモリ以外にもCPU等取得情報を追加したいときに備えて、interfaceで定義しておく
type Collector interface {
    Update(ch chan<- prometheus.Metric) error
}

func NewSampleCollector() (*SampleCollector, error) {
    collectors := make(map[string]Collector)
    for k, _ := range collectorState {
        f, err := factories[k]()
        if err != nil {
            return nil, err
        }
        collectors[k] = f
    }
    // 今回はメモリだけだが、CPU等の他情報の取得も簡単に追加できる
    return &SampleCollector{Collectors: collectors}, nil
}

// Describe と Collect は、Collector interface に実装されている
// https://godoc.org/github.com/prometheus/client_golang/prometheus#Collector
func (sc SampleCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- scrapeDurationDesc
    ch <- scrapeSuccessDesc
}

// goroutine で複数の(今回はメモリだけだが)情報を並列に取得(execute)
func (sc SampleCollector) Collect(ch chan<- prometheus.Metric) {
    wg := sync.WaitGroup{}
    wg.Add(len(sc.Collectors))
    for name, c := range sc.Collectors {
        go func(name string, c Collector) {
            execute(name, c, ch)
            wg.Done()
        }(name, c)
    }
    wg.Wait()
}

func execute(name string, c Collector, ch chan<- prometheus.Metric) {
    begin := time.Now()
    err := c.Update(ch)
    duration := time.Since(begin)
    var success float64

    if err != nil {
        log.Printf("ERROR: %s collector failed after %fs: %s", name, duration.Seconds(), err.Error())
        success = 0
    }
    success = 1
    // 集計したい情報は chan に Metric を渡す
    ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name)
    ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, name)
}

在node_exporter中,获取内存信息是通过解析/proc/meminfo文件来完成的,而这次我们使用了以下的库:shirou/gopsutil: 用于Go语言的psutil。

const (
    subsystem = "memory"
)

type memoryCollector struct{}

// MustRegister するために、init() で collect.go の factories に追加する
func init() {
    registCollector(subsystem, NewMemoryCollector)
}

func NewMemoryCollector() (Collector, error) {
    return &memoryCollector{}, nil
}

func (c *memoryCollector) Update(ch chan<- prometheus.Metric) error {
    var metricType prometheus.ValueType

    // メモリ情報の取得
    v, err := mem.VirtualMemory()
    if err != nil {
        return fmt.Errorf("could not get memory info: %s", err)
    }

...
    for i := 0; i < t.NumField(); i++ {
...
        if strings.Contains(t.Field(i).Name, "Total") == true {
            // Total (例えばMemTotal)が入っていたら、Counter
            // メトリクスの種類は https://prometheus.io/docs/concepts/metric_types/
            metricType = prometheus.CounterValue
        } else {
            metricType = prometheus.GaugeValue
        }
...
        // 集計したい情報なので chan に Metric を渡す
        ch <- prometheus.MustNewConstMetric(
            prometheus.NewDesc(
                // メトリクス名を BuildFQName() で作成し、指定
                prometheus.BuildFQName(namespace, subsystem, t.Field(i).Name),
                fmt.Sprintf("Memory information filed %s", t.Field(i).Name),
                nil, nil,
            ),
            // メトリクスタイプと値を指定、値はfloat64
            metricType, f64,
        )
    }
    return nil
}

结局

由于长度较长,我只输出了部分内容,但看起来成功获取了。

$ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent"
# HELP sample_memory_UsedPercent Memory information filed UsedPercent
# TYPE sample_memory_UsedPercent counter
sample_memory_UsedPercent 1.9555757729632135
$ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent"
# HELP sample_memory_UsedPercent Memory information filed UsedPercent
# TYPE sample_memory_UsedPercent counter
sample_memory_UsedPercent 1.9468305687203793

最后一句

结果尽管变成了类似于node_exporter的劣质版本,但意外地很容易就能自己制作出一款exporter。

举个例子,在公司里可以制作一款通用且易于使用的exporter,并进行分发,暂时安装就可以。这样在grafana进行长期稳定性测试时很方便的。

Note: The meaning of “node_exporter” and “exporter” may require further clarification for a more accurate paraphrase.
结果把劣化的node_exporter版本做出来了,但制作exporter事实上很容易。例子,公司通用的简单使用的exporter制作发送,就是暂时OK的形式。这样稳定测试时在grafana上看起来很方便。

广告
将在 10 秒后关闭
bannerAds