add graphite bridge that support delta counters
This commit is contained in:
committed by
Carl Bergquist
parent
c1ef89d3bf
commit
d6b8c6a2d2
+22
-3
@@ -1,6 +1,11 @@
|
||||
package metrics
|
||||
|
||||
import "sync/atomic"
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Counters hold an int64 value that can be incremented and decremented.
|
||||
type Counter interface {
|
||||
@@ -8,21 +13,33 @@ type Counter interface {
|
||||
|
||||
Clear()
|
||||
Count() int64
|
||||
Dec(int64)
|
||||
Inc(int64)
|
||||
}
|
||||
|
||||
func promifyName(name string) string {
|
||||
return strings.Replace(name, ".", "_", -1)
|
||||
}
|
||||
|
||||
// NewCounter constructs a new StandardCounter.
|
||||
func NewCounter(meta *MetricMeta) Counter {
|
||||
promCounter := prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: promifyName(meta.Name()) + "_total",
|
||||
Help: meta.Name(),
|
||||
ConstLabels: prometheus.Labels(meta.GetTagsCopy()),
|
||||
})
|
||||
|
||||
prometheus.MustRegister(promCounter)
|
||||
|
||||
return &StandardCounter{
|
||||
MetricMeta: meta,
|
||||
count: 0,
|
||||
Counter: promCounter,
|
||||
}
|
||||
}
|
||||
|
||||
func RegCounter(name string, tagStrings ...string) Counter {
|
||||
cr := NewCounter(NewMetricMeta(name, tagStrings))
|
||||
MetricStats.Register(cr)
|
||||
//MetricStats.Register(cr)
|
||||
return cr
|
||||
}
|
||||
|
||||
@@ -31,6 +48,7 @@ func RegCounter(name string, tagStrings ...string) Counter {
|
||||
type StandardCounter struct {
|
||||
count int64 //Due to a bug in golang the 64bit variable need to come first to be 64bit aligned. https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
*MetricMeta
|
||||
prometheus.Counter
|
||||
}
|
||||
|
||||
// Clear sets the counter to zero.
|
||||
@@ -51,6 +69,7 @@ func (c *StandardCounter) Dec(i int64) {
|
||||
// Inc increments the counter by the given amount.
|
||||
func (c *StandardCounter) Inc(i int64) {
|
||||
atomic.AddInt64(&c.count, i)
|
||||
c.Counter.Add(float64(i))
|
||||
}
|
||||
|
||||
func (c *StandardCounter) Snapshot() Metric {
|
||||
|
||||
+18
-5
@@ -4,7 +4,11 @@
|
||||
|
||||
package metrics
|
||||
|
||||
import "sync/atomic"
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Gauges hold an int64 value that can be set arbitrarily.
|
||||
type Gauge interface {
|
||||
@@ -15,18 +19,25 @@ type Gauge interface {
|
||||
}
|
||||
|
||||
func NewGauge(meta *MetricMeta) Gauge {
|
||||
if UseNilMetrics {
|
||||
return NilGauge{}
|
||||
}
|
||||
promGauge := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: promifyName(meta.Name()) + "_total",
|
||||
Help: meta.Name(),
|
||||
ConstLabels: prometheus.Labels(meta.GetTagsCopy()),
|
||||
})
|
||||
|
||||
prometheus.MustRegister(promGauge)
|
||||
|
||||
return &StandardGauge{
|
||||
MetricMeta: meta,
|
||||
value: 0,
|
||||
Gauge: promGauge,
|
||||
}
|
||||
}
|
||||
|
||||
func RegGauge(name string, tagStrings ...string) Gauge {
|
||||
tr := NewGauge(NewMetricMeta(name, tagStrings))
|
||||
MetricStats.Register(tr)
|
||||
|
||||
//MetricStats.Register(tr)
|
||||
return tr
|
||||
}
|
||||
|
||||
@@ -65,6 +76,7 @@ func (NilGauge) Value() int64 { return 0 }
|
||||
type StandardGauge struct {
|
||||
value int64
|
||||
*MetricMeta
|
||||
prometheus.Gauge
|
||||
}
|
||||
|
||||
// Snapshot returns a read-only copy of the gauge.
|
||||
@@ -75,6 +87,7 @@ func (g *StandardGauge) Snapshot() Metric {
|
||||
// Update updates the gauge's value.
|
||||
func (g *StandardGauge) Update(v int64) {
|
||||
atomic.StoreInt64(&g.value, v)
|
||||
g.Gauge.Set(float64(v))
|
||||
}
|
||||
|
||||
// Value returns the gauge's current value.
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright 2016 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package graphite provides a bridge to push Prometheus metrics to a Graphite
|
||||
// server.
|
||||
package graphitepublisher
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/expfmt"
|
||||
"github.com/prometheus/common/model"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultInterval = 15 * time.Second
|
||||
millisecondsPerSecond = 1000
|
||||
)
|
||||
|
||||
// HandlerErrorHandling defines how a Handler serving metrics will handle
|
||||
// errors.
|
||||
type HandlerErrorHandling int
|
||||
|
||||
// These constants cause handlers serving metrics to behave as described if
|
||||
// errors are encountered.
|
||||
const (
|
||||
// Ignore errors and try to push as many metrics to Graphite as possible.
|
||||
ContinueOnError HandlerErrorHandling = iota
|
||||
|
||||
// Abort the push to Graphite upon the first error encountered.
|
||||
AbortOnError
|
||||
)
|
||||
|
||||
// Config defines the Graphite bridge config.
|
||||
type Config struct {
|
||||
// The url to push data to. Required.
|
||||
URL string
|
||||
|
||||
// The prefix for the pushed Graphite metrics. Defaults to empty string.
|
||||
Prefix string
|
||||
|
||||
// The interval to use for pushing data to Graphite. Defaults to 15 seconds.
|
||||
Interval time.Duration
|
||||
|
||||
// The timeout for pushing metrics to Graphite. Defaults to 15 seconds.
|
||||
Timeout time.Duration
|
||||
|
||||
// The Gatherer to use for metrics. Defaults to prometheus.DefaultGatherer.
|
||||
Gatherer prometheus.Gatherer
|
||||
|
||||
// The logger that messages are written to. Defaults to no logging.
|
||||
Logger Logger
|
||||
|
||||
// ErrorHandling defines how errors are handled. Note that errors are
|
||||
// logged regardless of the configured ErrorHandling provided Logger
|
||||
// is not nil.
|
||||
ErrorHandling HandlerErrorHandling
|
||||
|
||||
// Graphite does not support ever increasing counter the same way
|
||||
// prometheus does. Rollups and ingestion might cannot handle ever
|
||||
// increasing counters. This option allows enabled the caller to
|
||||
// calculate the delta by saving the last sent counter in memory
|
||||
// and subtraction it from the collected value before sending.
|
||||
CountersAsDelta bool
|
||||
}
|
||||
|
||||
// Bridge pushes metrics to the configured Graphite server.
|
||||
type Bridge struct {
|
||||
url string
|
||||
prefix string
|
||||
countersAsDetlas bool
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
|
||||
errorHandling HandlerErrorHandling
|
||||
logger Logger
|
||||
|
||||
g prometheus.Gatherer
|
||||
|
||||
lastValue map[model.Fingerprint]float64
|
||||
}
|
||||
|
||||
// Logger is the minimal interface Bridge needs for logging. Note that
|
||||
// log.Logger from the standard library implements this interface, and it is
|
||||
// easy to implement by custom loggers, if they don't do so already anyway.
|
||||
type Logger interface {
|
||||
Println(v ...interface{})
|
||||
}
|
||||
|
||||
// NewBridge returns a pointer to a new Bridge struct.
|
||||
func NewBridge(c *Config) (*Bridge, error) {
|
||||
b := &Bridge{}
|
||||
|
||||
if c.URL == "" {
|
||||
return nil, errors.New("missing URL")
|
||||
}
|
||||
b.url = c.URL
|
||||
|
||||
if c.Gatherer == nil {
|
||||
b.g = prometheus.DefaultGatherer
|
||||
} else {
|
||||
b.g = c.Gatherer
|
||||
}
|
||||
|
||||
if c.Logger != nil {
|
||||
b.logger = c.Logger
|
||||
}
|
||||
|
||||
if c.Prefix != "" {
|
||||
b.prefix = c.Prefix
|
||||
}
|
||||
|
||||
var z time.Duration
|
||||
if c.Interval == z {
|
||||
b.interval = defaultInterval
|
||||
} else {
|
||||
b.interval = c.Interval
|
||||
}
|
||||
|
||||
if c.Timeout == z {
|
||||
b.timeout = defaultInterval
|
||||
} else {
|
||||
b.timeout = c.Timeout
|
||||
}
|
||||
|
||||
b.errorHandling = c.ErrorHandling
|
||||
b.lastValue = map[model.Fingerprint]float64{}
|
||||
b.countersAsDetlas = c.CountersAsDelta
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Run starts the event loop that pushes Prometheus metrics to Graphite at the
|
||||
// configured interval.
|
||||
func (b *Bridge) Run(ctx context.Context) {
|
||||
ticker := time.NewTicker(b.interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := b.Push(); err != nil && b.logger != nil {
|
||||
b.logger.Println("error pushing to Graphite:", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push pushes Prometheus metrics to the configured Graphite server.
|
||||
func (b *Bridge) Push() error {
|
||||
mfs, err := b.g.Gather()
|
||||
if err != nil || len(mfs) == 0 {
|
||||
switch b.errorHandling {
|
||||
case AbortOnError:
|
||||
return err
|
||||
case ContinueOnError:
|
||||
if b.logger != nil {
|
||||
b.logger.Println("continue on error:", err)
|
||||
}
|
||||
default:
|
||||
panic("unrecognized error handling value")
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", b.url, b.timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return b.writeMetrics(conn, mfs, b.prefix, model.Now())
|
||||
}
|
||||
|
||||
func (b *Bridge) writeMetrics(w io.Writer, mfs []*dto.MetricFamily, prefix string, now model.Time) error {
|
||||
for _, mf := range mfs {
|
||||
vec, err := expfmt.ExtractSamples(&expfmt.DecodeOptions{
|
||||
Timestamp: now,
|
||||
}, mf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := bufio.NewWriter(w)
|
||||
for _, s := range vec {
|
||||
if err := writeSanitized(buf, prefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := buf.WriteByte('.'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeMetric(buf, s.Metric, mf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value := b.replaceCounterWithDelta(mf, s.Metric, s.Value)
|
||||
if _, err := fmt.Fprintf(buf, " %g %d\n", value, int64(s.Timestamp)/millisecondsPerSecond); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := buf.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error {
|
||||
metricName, hasName := m[model.MetricNameLabel]
|
||||
numLabels := len(m) - 1
|
||||
if !hasName {
|
||||
numLabels = len(m)
|
||||
}
|
||||
|
||||
labelStrings := make([]string, 0, numLabels)
|
||||
for label, value := range m {
|
||||
if label != model.MetricNameLabel {
|
||||
labelStrings = append(labelStrings, fmt.Sprintf("%s %s", string(label), string(value)))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
switch numLabels {
|
||||
case 0:
|
||||
if hasName {
|
||||
return writeSanitized(buf, string(metricName))
|
||||
}
|
||||
default:
|
||||
sort.Strings(labelStrings)
|
||||
if err = writeSanitized(buf, string(metricName)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range labelStrings {
|
||||
if err = buf.WriteByte('.'); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = writeSanitized(buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mf.GetType() == dto.MetricType_COUNTER {
|
||||
// Adding the `.count` suffix makes it possible to configure
|
||||
// `sum` as rollup strategy for counters
|
||||
if _, err = fmt.Fprint(buf, ".count"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSanitized(buf *bufio.Writer, s string) error {
|
||||
prevUnderscore := false
|
||||
|
||||
for _, c := range s {
|
||||
c = replaceInvalidRune(c)
|
||||
if c == '_' {
|
||||
if prevUnderscore {
|
||||
continue
|
||||
}
|
||||
prevUnderscore = true
|
||||
} else {
|
||||
prevUnderscore = false
|
||||
}
|
||||
if _, err := buf.WriteRune(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceInvalidRune(c rune) rune {
|
||||
if c == ' ' {
|
||||
return '.'
|
||||
}
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || (c >= '0' && c <= '9')) {
|
||||
return '_'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (b *Bridge) replaceCounterWithDelta(mf *dto.MetricFamily, metric model.Metric, value model.SampleValue) float64 {
|
||||
if !b.countersAsDetlas || mf.GetType() != dto.MetricType_COUNTER {
|
||||
return float64(value)
|
||||
}
|
||||
|
||||
//TODO(bergquist): turn _count in summery into delta
|
||||
//TODO(bergquist): turn _count in histogram into delta
|
||||
|
||||
key := metric.Fingerprint()
|
||||
_, exists := b.lastValue[key]
|
||||
if !exists {
|
||||
b.lastValue[key] = 0
|
||||
}
|
||||
|
||||
delta := float64(value) - b.lastValue[key]
|
||||
b.lastValue[key] = float64(value)
|
||||
|
||||
return delta
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package graphitepublisher
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
func TestCountersAsDelta(t *testing.T) {
|
||||
b, _ := NewBridge(&Config{
|
||||
URL: "localhost:12345",
|
||||
CountersAsDelta: true,
|
||||
})
|
||||
ty := dto.MetricType(0)
|
||||
mf := &dto.MetricFamily{
|
||||
Type: &ty,
|
||||
Metric: []*dto.Metric{},
|
||||
}
|
||||
m := model.Metric{}
|
||||
|
||||
var want float64
|
||||
var got float64
|
||||
want = float64(1)
|
||||
got = b.replaceCounterWithDelta(mf, m, model.SampleValue(1))
|
||||
if got != want {
|
||||
t.Fatalf("want %v got %v", want, got)
|
||||
}
|
||||
|
||||
got = b.replaceCounterWithDelta(mf, m, model.SampleValue(2))
|
||||
if got != want {
|
||||
t.Fatalf("want %v got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountersAsDeltaDisabled(t *testing.T) {
|
||||
b, _ := NewBridge(&Config{
|
||||
URL: "localhost:12345",
|
||||
CountersAsDelta: false,
|
||||
})
|
||||
ty := dto.MetricType(0)
|
||||
mf := &dto.MetricFamily{
|
||||
Type: &ty,
|
||||
Metric: []*dto.Metric{},
|
||||
}
|
||||
m := model.Metric{}
|
||||
|
||||
var want float64
|
||||
var got float64
|
||||
want = float64(1)
|
||||
got = b.replaceCounterWithDelta(mf, m, model.SampleValue(1))
|
||||
if got != want {
|
||||
t.Fatalf("want %v got %v", want, got)
|
||||
}
|
||||
|
||||
want = float64(2)
|
||||
got = b.replaceCounterWithDelta(mf, m, model.SampleValue(2))
|
||||
if got != want {
|
||||
t.Fatalf("want %v got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
in, out string
|
||||
}{
|
||||
{in: "hello", out: "hello"},
|
||||
{in: "hE/l1o", out: "hE_l1o"},
|
||||
{in: "he,*ll(.o", out: "he_ll_o"},
|
||||
{in: "hello_there%^&", out: "hello_there_"},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := bufio.NewWriter(&buf)
|
||||
|
||||
for i, tc := range testCases {
|
||||
if err := writeSanitized(w, tc.in); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatalf("flush failed: %v", err)
|
||||
}
|
||||
|
||||
if want, got := tc.out, buf.String(); want != got {
|
||||
t.Fatalf("test case index %d: got sanitized string %s, want %s", i, got, want)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSummary(t *testing.T) {
|
||||
sumVec := prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "name",
|
||||
Help: "docstring",
|
||||
ConstLabels: prometheus.Labels{"constname": "constvalue"},
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||
},
|
||||
[]string{"labelname"},
|
||||
)
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(sumVec)
|
||||
|
||||
b, err := NewBridge(&Config{
|
||||
URL: "localhost:8080",
|
||||
Gatherer: reg,
|
||||
CountersAsDelta: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create bridge. err: %v", err)
|
||||
}
|
||||
|
||||
sumVec.WithLabelValues("val1").Observe(float64(10))
|
||||
sumVec.WithLabelValues("val1").Observe(float64(20))
|
||||
sumVec.WithLabelValues("val1").Observe(float64(30))
|
||||
sumVec.WithLabelValues("val2").Observe(float64(20))
|
||||
sumVec.WithLabelValues("val2").Observe(float64(30))
|
||||
sumVec.WithLabelValues("val2").Observe(float64(40))
|
||||
|
||||
mfs, err := reg.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
now := model.Time(1477043083)
|
||||
var buf bytes.Buffer
|
||||
err = b.writeMetrics(&buf, mfs, "prefix", now)
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
want := `prefix.name.constname.constvalue.labelname.val1.quantile.0_5 20 1477043
|
||||
prefix.name.constname.constvalue.labelname.val1.quantile.0_9 30 1477043
|
||||
prefix.name.constname.constvalue.labelname.val1.quantile.0_99 30 1477043
|
||||
prefix.name_sum.constname.constvalue.labelname.val1 60 1477043
|
||||
prefix.name_count.constname.constvalue.labelname.val1 3 1477043
|
||||
prefix.name.constname.constvalue.labelname.val2.quantile.0_5 30 1477043
|
||||
prefix.name.constname.constvalue.labelname.val2.quantile.0_9 40 1477043
|
||||
prefix.name.constname.constvalue.labelname.val2.quantile.0_99 40 1477043
|
||||
prefix.name_sum.constname.constvalue.labelname.val2 90 1477043
|
||||
prefix.name_count.constname.constvalue.labelname.val2 3 1477043
|
||||
`
|
||||
|
||||
if got := buf.String(); want != got {
|
||||
t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteHistogram(t *testing.T) {
|
||||
histVec := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "name",
|
||||
Help: "docstring",
|
||||
ConstLabels: prometheus.Labels{"constname": "constvalue"},
|
||||
Buckets: []float64{0.01, 0.02, 0.05, 0.1},
|
||||
},
|
||||
[]string{"labelname"},
|
||||
)
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(histVec)
|
||||
|
||||
b, err := NewBridge(&Config{
|
||||
URL: "localhost:8080",
|
||||
Gatherer: reg,
|
||||
CountersAsDelta: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error creating bridge: %v", err)
|
||||
}
|
||||
|
||||
histVec.WithLabelValues("val1").Observe(float64(10))
|
||||
histVec.WithLabelValues("val1").Observe(float64(20))
|
||||
histVec.WithLabelValues("val1").Observe(float64(30))
|
||||
histVec.WithLabelValues("val2").Observe(float64(20))
|
||||
histVec.WithLabelValues("val2").Observe(float64(30))
|
||||
histVec.WithLabelValues("val2").Observe(float64(40))
|
||||
|
||||
mfs, err := reg.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
now := model.Time(1477043083)
|
||||
var buf bytes.Buffer
|
||||
err = b.writeMetrics(&buf, mfs, "prefix", now)
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
want := `prefix.name_bucket.constname.constvalue.labelname.val1.le.0_01 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_02 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_05 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_1 0 1477043
|
||||
prefix.name_sum.constname.constvalue.labelname.val1 60 1477043
|
||||
prefix.name_count.constname.constvalue.labelname.val1 3 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val1.le._Inf 3 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_01 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_02 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_05 0 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_1 0 1477043
|
||||
prefix.name_sum.constname.constvalue.labelname.val2 90 1477043
|
||||
prefix.name_count.constname.constvalue.labelname.val2 3 1477043
|
||||
prefix.name_bucket.constname.constvalue.labelname.val2.le._Inf 3 1477043
|
||||
`
|
||||
if got := buf.String(); want != got {
|
||||
t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCounter(t *testing.T) {
|
||||
cntVec := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "name",
|
||||
Help: "docstring",
|
||||
ConstLabels: prometheus.Labels{"constname": "constvalue"},
|
||||
},
|
||||
[]string{"labelname"},
|
||||
)
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(cntVec)
|
||||
|
||||
cntVec.WithLabelValues("val1").Inc()
|
||||
cntVec.WithLabelValues("val2").Inc()
|
||||
|
||||
b, err := NewBridge(&Config{
|
||||
URL: "localhost:8080",
|
||||
Gatherer: reg,
|
||||
CountersAsDelta: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error creating bridge: %v", err)
|
||||
}
|
||||
|
||||
// first collect
|
||||
mfs, err := reg.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = b.writeMetrics(&buf, mfs, "prefix", model.Time(1477043083))
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
want := `prefix.name.constname.constvalue.labelname.val1.count 1 1477043
|
||||
prefix.name.constname.constvalue.labelname.val2.count 1 1477043
|
||||
`
|
||||
if got := buf.String(); want != got {
|
||||
t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
|
||||
}
|
||||
|
||||
//next collect
|
||||
cntVec.WithLabelValues("val1").Inc()
|
||||
cntVec.WithLabelValues("val2").Inc()
|
||||
|
||||
mfs, err = reg.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
buf = bytes.Buffer{}
|
||||
err = b.writeMetrics(&buf, mfs, "prefix", model.Time(1477053083))
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
want2 := `prefix.name.constname.constvalue.labelname.val1.count 1 1477053
|
||||
prefix.name.constname.constvalue.labelname.val2.count 1 1477053
|
||||
`
|
||||
if got := buf.String(); want2 != got {
|
||||
t.Fatalf("wanted \n%s\n, got \n%s\n", want2, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
reg := prometheus.NewRegistry()
|
||||
cntVec := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "name",
|
||||
Help: "docstring",
|
||||
ConstLabels: prometheus.Labels{"constname": "constvalue"},
|
||||
},
|
||||
[]string{"labelname"},
|
||||
)
|
||||
cntVec.WithLabelValues("val1").Inc()
|
||||
cntVec.WithLabelValues("val2").Inc()
|
||||
reg.MustRegister(cntVec)
|
||||
|
||||
host := "localhost"
|
||||
port := ":56789"
|
||||
b, err := NewBridge(&Config{
|
||||
URL: host + port,
|
||||
Gatherer: reg,
|
||||
Prefix: "prefix",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error creating bridge: %v", err)
|
||||
}
|
||||
|
||||
nmg, err := newMockGraphite(port)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating mock graphite: %v", err)
|
||||
}
|
||||
defer nmg.Close()
|
||||
|
||||
err = b.Push()
|
||||
if err != nil {
|
||||
t.Fatalf("error pushing: %v", err)
|
||||
}
|
||||
|
||||
wants := []string{
|
||||
"prefix.name.constname.constvalue.labelname.val1.count 1",
|
||||
"prefix.name.constname.constvalue.labelname.val2.count 1",
|
||||
}
|
||||
|
||||
select {
|
||||
case got := <-nmg.readc:
|
||||
for _, want := range wants {
|
||||
matched, err := regexp.MatchString(want, got)
|
||||
if err != nil {
|
||||
t.Fatalf("error pushing: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Fatalf("missing metric:\nno match for %s received by server:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
return
|
||||
case err := <-nmg.errc:
|
||||
t.Fatalf("error reading push: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
t.Fatalf("no result from graphite server")
|
||||
}
|
||||
}
|
||||
|
||||
func newMockGraphite(port string) (*mockGraphite, error) {
|
||||
readc := make(chan string)
|
||||
errc := make(chan error)
|
||||
ln, err := net.Listen("tcp", port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
io.Copy(&b, conn)
|
||||
readc <- b.String()
|
||||
}()
|
||||
|
||||
return &mockGraphite{
|
||||
readc: readc,
|
||||
errc: errc,
|
||||
Listener: ln,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockGraphite struct {
|
||||
readc chan string
|
||||
errc chan error
|
||||
|
||||
net.Listener
|
||||
}
|
||||
@@ -5,7 +5,7 @@ var UseNilMetrics bool
|
||||
|
||||
func init() {
|
||||
// init with nil metrics
|
||||
initMetricVars(&MetricSettings{})
|
||||
//initMetricVars(&MetricSettings{})
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
+31
-2
@@ -2,6 +2,7 @@ package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -10,18 +11,46 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitepublisher"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var metricsLogger log.Logger = log.New("metrics")
|
||||
var metricPublishCounter int64 = 0
|
||||
|
||||
type logWrapper struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (lw *logWrapper) Println(v ...interface{}) {
|
||||
lw.logger.Info("graphite metric bridge", v...)
|
||||
}
|
||||
|
||||
func Init() {
|
||||
settings := readSettings()
|
||||
initMetricVars(settings)
|
||||
go instrumentationLoop(settings)
|
||||
//go instrumentationLoop(settings)
|
||||
|
||||
cfg := &graphitepub.Config{
|
||||
URL: "localhost:2003",
|
||||
Gatherer: prometheus.DefaultGatherer,
|
||||
Prefix: "prefix",
|
||||
Interval: 10 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
Logger: &logWrapper{logger: metricsLogger},
|
||||
ErrorHandling: graphitepub.ContinueOnError,
|
||||
CountersAsDelta: true,
|
||||
}
|
||||
|
||||
bridge, err := graphitepub.NewBridge(cfg)
|
||||
if err != nil {
|
||||
metricsLogger.Error("failed to create graphite bridge", "error", err)
|
||||
} else {
|
||||
go bridge.Run(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentationLoop(settings *MetricSettings) chan struct{} {
|
||||
@@ -50,13 +79,13 @@ func sendMetrics(settings *MetricSettings) {
|
||||
updateTotalStats()
|
||||
|
||||
metrics := MetricStats.GetSnapshots()
|
||||
|
||||
for _, publisher := range settings.Publishers {
|
||||
publisher.Publish(metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTotalStats() {
|
||||
|
||||
// every interval also publish totals
|
||||
metricPublishCounter++
|
||||
if metricPublishCounter%10 == 0 {
|
||||
|
||||
+16
-15
@@ -7,6 +7,8 @@ package metrics
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Timers capture the duration and rate of events.
|
||||
@@ -31,28 +33,22 @@ type Timer interface {
|
||||
Variance() float64
|
||||
}
|
||||
|
||||
// NewCustomTimer constructs a new StandardTimer from a Histogram and a Meter.
|
||||
func NewCustomTimer(meta *MetricMeta, h Histogram, m Meter) Timer {
|
||||
if UseNilMetrics {
|
||||
return NilTimer{}
|
||||
}
|
||||
return &StandardTimer{
|
||||
MetricMeta: meta,
|
||||
histogram: h,
|
||||
meter: m,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTimer constructs a new StandardTimer using an exponentially-decaying
|
||||
// sample with the same reservoir size and alpha as UNIX load averages.
|
||||
func NewTimer(meta *MetricMeta) Timer {
|
||||
if UseNilMetrics {
|
||||
return NilTimer{}
|
||||
}
|
||||
promSummary := prometheus.NewSummary(prometheus.SummaryOpts{
|
||||
Name: promifyName(meta.Name()),
|
||||
Help: meta.Name(),
|
||||
ConstLabels: prometheus.Labels(meta.GetTagsCopy()),
|
||||
})
|
||||
|
||||
prometheus.MustRegister(promSummary)
|
||||
|
||||
return &StandardTimer{
|
||||
MetricMeta: meta,
|
||||
histogram: NewHistogram(meta, NewExpDecaySample(1028, 0.015)),
|
||||
meter: NewMeter(meta),
|
||||
Summary: promSummary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +125,7 @@ type StandardTimer struct {
|
||||
histogram Histogram
|
||||
meter Meter
|
||||
mutex sync.Mutex
|
||||
prometheus.Summary
|
||||
}
|
||||
|
||||
// Count returns the number of events recorded.
|
||||
@@ -216,6 +213,8 @@ func (t *StandardTimer) Update(d time.Duration) {
|
||||
defer t.mutex.Unlock()
|
||||
t.histogram.Update(int64(d))
|
||||
t.meter.Mark(1)
|
||||
|
||||
t.Summary.Observe(float64(d))
|
||||
}
|
||||
|
||||
// Record the duration of an event that started at a time and ends now.
|
||||
@@ -225,6 +224,8 @@ func (t *StandardTimer) UpdateSince(ts time.Time) {
|
||||
sinceMs := time.Since(ts) / time.Millisecond
|
||||
t.histogram.Update(int64(sinceMs))
|
||||
t.meter.Mark(1)
|
||||
|
||||
t.Summary.Observe(float64(sinceMs))
|
||||
}
|
||||
|
||||
// Variance returns the variance of the values in the sample.
|
||||
|
||||
Reference in New Issue
Block a user