Skip to content

Commit

Permalink
module/apmprometheus: prometheus wrapper
Browse files Browse the repository at this point in the history
Package apmprometheus provides a method for
wrapping a prometheus.Gatherer to adapt it
to the elasticapm.MetricsGatherer interface.
Untyped, Gauge and Counter metric types are
supported; Histogram and and Summary metrics
are silently ignored for now.
  • Loading branch information
axw committed May 18, 2018
1 parent 066fe1c commit 65b1521
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
67 changes: 67 additions & 0 deletions module/apmprometheus/gatherer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package apmprometheus

import (
"context"

"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"

"github.com/elastic/apm-agent-go"
)

// Wrap returns an elasticapm.MetricsGatherer wrapping g.
func Wrap(g prometheus.Gatherer) elasticapm.MetricsGatherer {
return gatherer{g}
}

type gatherer struct {
p prometheus.Gatherer
}

// GatherMetrics gathers metrics from the prometheus.Gatherer p.g,
// and adds them to out.
func (g gatherer) GatherMetrics(ctx context.Context, out *elasticapm.Metrics) error {
metricFamilies, err := g.p.Gather()
if err != nil {
return errors.WithStack(err)
}
for _, mf := range metricFamilies {
name := mf.GetName()
switch mf.GetType() {
case dto.MetricType_COUNTER:
for _, m := range mf.GetMetric() {
v := m.GetCounter().GetValue()
out.AddCounter(name, makeLabels(m.GetLabel()), v)
}
case dto.MetricType_GAUGE:
metrics := mf.GetMetric()
if name == "go_info" && len(metrics) == 1 && metrics[0].GetGauge().GetValue() == 1 {
// Ignore the "go_info" metric from the
// built-in GoCollector, as we provide
// the same information in the payload.
continue
}
for _, m := range metrics {
v := m.GetGauge().GetValue()
out.AddGauge(name, makeLabels(m.GetLabel()), v)
}
case dto.MetricType_UNTYPED:
for _, m := range mf.GetMetric() {
v := m.GetUntyped().GetValue()
out.AddGauge(name, makeLabels(m.GetLabel()), v)
}
default:
// TODO(axw) MetricType_SUMMARY, MetricType_HISTOGRAM
}
}
return nil
}

func makeLabels(lps []*dto.LabelPair) []elasticapm.MetricLabel {
labels := make([]elasticapm.MetricLabel, len(lps))
for i, lp := range lps {
labels[i] = elasticapm.MetricLabel{Name: lp.GetName(), Value: lp.GetValue()}
}
return labels
}
99 changes: 99 additions & 0 deletions module/apmprometheus/gatherer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package apmprometheus_test

import (
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/apm-agent-go"
"github.com/elastic/apm-agent-go/model"
"github.com/elastic/apm-agent-go/module/apmprometheus"
"github.com/elastic/apm-agent-go/transport/transporttest"
)

func TestGoCollector(t *testing.T) {
g := apmprometheus.Wrap(prometheus.DefaultGatherer)
metrics := gatherMetrics(g)
require.Len(t, metrics, 1)
assert.Nil(t, metrics[0].Labels)

assert.Contains(t, metrics[0].Samples, "go_memstats_alloc_bytes")
assert.Contains(t, metrics[0].Samples, "go_memstats_alloc_bytes_total")
assert.NotNil(t, metrics[0].Samples["go_memstats_alloc_bytes"].Value)
assert.NotNil(t, metrics[0].Samples["go_memstats_alloc_bytes_total"].Count)
}

func TestLabels(t *testing.T) {
r := prometheus.NewRegistry()
httpReqsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total", Help: "."},
[]string{"code", "method"},
)
httpReqsInflight := prometheus.NewGaugeVec(
prometheus.GaugeOpts{Name: "http_requests_inflight", Help: "."},
[]string{"code", "method"},
)
r.MustRegister(httpReqsTotal)
r.MustRegister(httpReqsInflight)

httpReqsTotal.WithLabelValues("404", "GET").Inc()
httpReqsTotal.WithLabelValues("200", "PUT").Inc()
httpReqsTotal.WithLabelValues("200", "GET").Add(123)
httpReqsInflight.WithLabelValues("200", "GET").Set(10)

g := apmprometheus.Wrap(r)
metrics := gatherMetrics(g)

assert.Equal(t, []*model.Metrics{{
Labels: model.StringMap{
{Key: "code", Value: "200"},
{Key: "method", Value: "GET"},
},
Samples: map[string]model.Metric{
"http_requests_total": {
Count: newFloat64(123),
},
"http_requests_inflight": {
Value: newFloat64(10),
},
},
}, {
Labels: model.StringMap{
{Key: "code", Value: "200"},
{Key: "method", Value: "PUT"},
},
Samples: map[string]model.Metric{
"http_requests_total": {
Count: newFloat64(1),
},
},
}, {
Labels: model.StringMap{
{Key: "code", Value: "404"},
{Key: "method", Value: "GET"},
},
Samples: map[string]model.Metric{
"http_requests_total": {
Count: newFloat64(1),
},
},
}}, metrics)
}

func gatherMetrics(g elasticapm.MetricsGatherer) []*model.Metrics {
tracer, transport := transporttest.NewRecorderTracer()
defer tracer.Close()
tracer.AddMetricsGatherer(g)
tracer.SendMetrics(nil)
metrics := transport.Payloads()[0].Metrics()
for _, s := range metrics {
s.Timestamp = model.Time{}
}
return metrics
}

func newFloat64(v float64) *float64 {
return &v
}

0 comments on commit 65b1521

Please sign in to comment.