forked from mirrors/statsd_exporter
Implement statsd-to-prometheus metric label mappings.
This commit is contained in:
parent
b09f038e44
commit
80d504ae46
3 changed files with 242 additions and 8 deletions
40
main.go
40
main.go
|
@ -23,6 +23,7 @@ import (
|
||||||
var (
|
var (
|
||||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address on which to expose generated Prometheus metrics.")
|
listeningAddress = flag.String("listeningAddress", ":8080", "The address on which to expose generated Prometheus metrics.")
|
||||||
statsdListeningAddress = flag.String("statsdListeningAddress", ":8126", "The UDP address on which to receive statsd metric lines.")
|
statsdListeningAddress = flag.String("statsdListeningAddress", ":8126", "The UDP address on which to receive statsd metric lines.")
|
||||||
|
mappingConfig = flag.String("mappingConfig", "mapping.conf", "Metric mapping configuration file name.")
|
||||||
summaryFlushInterval = flag.Duration("summaryFlushInterval", 15*time.Minute, "How frequently to reset all summary metrics.")
|
summaryFlushInterval = flag.Duration("summaryFlushInterval", 15*time.Minute, "How frequently to reset all summary metrics.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,6 +128,7 @@ type Bridge struct {
|
||||||
Counters *CounterContainer
|
Counters *CounterContainer
|
||||||
Gauges *GaugeContainer
|
Gauges *GaugeContainer
|
||||||
Summaries *SummaryContainer
|
Summaries *SummaryContainer
|
||||||
|
mapper *metricMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeMetricName(metricName string) string {
|
func escapeMetricName(metricName string) string {
|
||||||
|
@ -141,29 +143,43 @@ func (b *Bridge) Listen(e <-chan Events) {
|
||||||
for {
|
for {
|
||||||
events := <-e
|
events := <-e
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
metricName := escapeMetricName(event.MetricName())
|
metricName := ""
|
||||||
|
prometheusLabels := map[string]string{}
|
||||||
|
|
||||||
|
labels, present := b.mapper.getMapping(event.MetricName())
|
||||||
|
if present {
|
||||||
|
metricName = labels["name"]
|
||||||
|
for label, value := range labels {
|
||||||
|
if label != "name" {
|
||||||
|
prometheusLabels[label] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metricName = escapeMetricName(event.MetricName())
|
||||||
|
}
|
||||||
|
|
||||||
switch event.(type) {
|
switch event.(type) {
|
||||||
case *CounterEvent:
|
case *CounterEvent:
|
||||||
counter := b.Counters.Get(metricName + "_counter")
|
counter := b.Counters.Get(metricName + "_counter")
|
||||||
counter.IncrementBy(prometheus.NilLabels, event.Value())
|
counter.IncrementBy(prometheusLabels, event.Value())
|
||||||
|
|
||||||
eventStats.Increment(map[string]string{"type": "counter"})
|
eventStats.Increment(map[string]string{"type": "counter"})
|
||||||
|
|
||||||
case *GaugeEvent:
|
case *GaugeEvent:
|
||||||
gauge := b.Gauges.Get(metricName + "_gauge")
|
gauge := b.Gauges.Get(metricName + "_gauge")
|
||||||
gauge.Set(prometheus.NilLabels, event.Value())
|
gauge.Set(prometheusLabels, event.Value())
|
||||||
|
|
||||||
eventStats.Increment(map[string]string{"type": "gauge"})
|
eventStats.Increment(map[string]string{"type": "gauge"})
|
||||||
|
|
||||||
case *TimerEvent:
|
case *TimerEvent:
|
||||||
summary := b.Summaries.Get(metricName + "_timer")
|
summary := b.Summaries.Get(metricName + "_timer")
|
||||||
summary.Add(prometheus.NilLabels, event.Value())
|
summary.Add(prometheusLabels, event.Value())
|
||||||
|
|
||||||
sum := b.Counters.Get(metricName + "_timer_total")
|
sum := b.Counters.Get(metricName + "_timer_total")
|
||||||
sum.IncrementBy(prometheus.NilLabels, event.Value())
|
sum.IncrementBy(prometheusLabels, event.Value())
|
||||||
|
|
||||||
count := b.Counters.Get(metricName + "_timer_count")
|
count := b.Counters.Get(metricName + "_timer_count")
|
||||||
count.Increment(prometheus.NilLabels)
|
count.Increment(prometheusLabels)
|
||||||
|
|
||||||
eventStats.Increment(map[string]string{"type": "timer"})
|
eventStats.Increment(map[string]string{"type": "timer"})
|
||||||
|
|
||||||
|
@ -175,11 +191,12 @@ func (b *Bridge) Listen(e <-chan Events) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBridge() *Bridge {
|
func NewBridge(mapper *metricMapper) *Bridge {
|
||||||
return &Bridge{
|
return &Bridge{
|
||||||
Counters: NewCounterContainer(),
|
Counters: NewCounterContainer(),
|
||||||
Gauges: NewGaugeContainer(),
|
Gauges: NewGaugeContainer(),
|
||||||
Summaries: NewSummaryContainer(),
|
Summaries: NewSummaryContainer(),
|
||||||
|
mapper: mapper,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +361,14 @@ func main() {
|
||||||
l := &StatsDListener{conn: conn}
|
l := &StatsDListener{conn: conn}
|
||||||
go l.Listen(events)
|
go l.Listen(events)
|
||||||
|
|
||||||
bridge := NewBridge()
|
mapper := metricMapper{}
|
||||||
|
if mappingConfig != nil {
|
||||||
|
err := mapper.initFromFile(*mappingConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading config:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bridge := NewBridge(&mapper)
|
||||||
go func() {
|
go func() {
|
||||||
for _ = range time.Tick(*summaryFlushInterval) {
|
for _ = range time.Tick(*summaryFlushInterval) {
|
||||||
bridge.Summaries.Flush()
|
bridge.Summaries.Flush()
|
||||||
|
|
111
mapper.go
Normal file
111
mapper.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// Copyright (c) 2013, Prometheus Team
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
identifierRE = `[a-zA-Z_][a-zA-Z0-9_]+`
|
||||||
|
metricLineRE = regexp.MustCompile(`^(\*\.|` + identifierRE + `\.)+(\*|` + identifierRE + `)$`)
|
||||||
|
labelLineRE = regexp.MustCompile(`^(` + identifierRE + `)\s*=\s*"(.*)"$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type metricMapping struct {
|
||||||
|
regex *regexp.Regexp
|
||||||
|
labels map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricMapper struct {
|
||||||
|
mappings []metricMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
type configLoadStates int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SEARCHING configLoadStates = iota
|
||||||
|
METRIC_DEFINITION
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *metricMapper) initFromString(fileContents string) error {
|
||||||
|
lines := strings.Split(fileContents, "\n")
|
||||||
|
state := SEARCHING
|
||||||
|
|
||||||
|
mapping := metricMapping{labels: map[string]string{}}
|
||||||
|
for i, line := range lines {
|
||||||
|
line := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case SEARCHING:
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !metricLineRE.MatchString(line) {
|
||||||
|
return fmt.Errorf("Line %d: expected metric match line, got: %s", i, line)
|
||||||
|
}
|
||||||
|
metricRe := strings.Replace(line, ".", "\\.", -1)
|
||||||
|
metricRe = strings.Replace(metricRe, "*", "([^.]+)", -1)
|
||||||
|
mapping.regex = regexp.MustCompile("^" + metricRe + "$")
|
||||||
|
state = METRIC_DEFINITION
|
||||||
|
|
||||||
|
case METRIC_DEFINITION:
|
||||||
|
if line == "" {
|
||||||
|
if len(mapping.labels) == 0 {
|
||||||
|
return fmt.Errorf("Line %d: metric mapping didn't set any labels", i)
|
||||||
|
}
|
||||||
|
if _, ok := mapping.labels["name"]; !ok {
|
||||||
|
return fmt.Errorf("Line %d: metric mapping didn't set a metric name", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mappings = append(l.mappings, mapping)
|
||||||
|
|
||||||
|
state = SEARCHING
|
||||||
|
mapping = metricMapping{labels: map[string]string{}}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := labelLineRE.FindStringSubmatch(line)
|
||||||
|
if len(matches) != 3 {
|
||||||
|
return fmt.Errorf("Line %d: expected label mapping line, got: %s", i, line)
|
||||||
|
}
|
||||||
|
mapping.labels[matches[1]] = matches[2]
|
||||||
|
default:
|
||||||
|
panic("illegal state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *metricMapper) initFromFile(fileName string) error {
|
||||||
|
mappingStr, err := ioutil.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return l.initFromString(string(mappingStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *metricMapper) getMapping(statsdMetric string) (labels map[string]string, present bool) {
|
||||||
|
for _, mapping := range l.mappings {
|
||||||
|
matches := mapping.regex.FindStringSubmatchIndex(statsdMetric)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := map[string]string{}
|
||||||
|
for label, valueExpr := range mapping.labels {
|
||||||
|
value := mapping.regex.ExpandString([]byte{}, valueExpr, statsdMetric, matches)
|
||||||
|
labels[label] = string(value)
|
||||||
|
}
|
||||||
|
return labels, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
99
mapper_test.go
Normal file
99
mapper_test.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2013, Prometheus Team
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetricMapper(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
config string
|
||||||
|
configBad bool
|
||||||
|
mappings map[string]map[string]string
|
||||||
|
}{
|
||||||
|
// Empty config.
|
||||||
|
{},
|
||||||
|
// Config with several mapping definitions.
|
||||||
|
{
|
||||||
|
config: `
|
||||||
|
test.dispatcher.*.*.*
|
||||||
|
name="dispatch_events"
|
||||||
|
processor="$1"
|
||||||
|
action="$2"
|
||||||
|
result="$3"
|
||||||
|
job="test_dispatcher"
|
||||||
|
|
||||||
|
*.*
|
||||||
|
name="catchall"
|
||||||
|
first="$1"
|
||||||
|
second="$2"
|
||||||
|
third="$3"
|
||||||
|
job="$1-$2-$3"
|
||||||
|
`,
|
||||||
|
mappings: map[string]map[string]string{
|
||||||
|
"test.dispatcher.FooProcessor.send.succeeded": map[string]string{
|
||||||
|
"name": "dispatch_events",
|
||||||
|
"processor": "FooProcessor",
|
||||||
|
"action": "send",
|
||||||
|
"result": "succeeded",
|
||||||
|
"job": "test_dispatcher",
|
||||||
|
},
|
||||||
|
"foo.bar": map[string]string{
|
||||||
|
"name": "catchall",
|
||||||
|
"first": "foo",
|
||||||
|
"second": "bar",
|
||||||
|
"third": "",
|
||||||
|
"job": "foo-bar-",
|
||||||
|
},
|
||||||
|
"foo.bar.baz": map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Config with bad metric line.
|
||||||
|
{
|
||||||
|
config: `
|
||||||
|
bad-metric-line.*.*
|
||||||
|
name="foo"
|
||||||
|
`,
|
||||||
|
configBad: true,
|
||||||
|
},
|
||||||
|
// Config with bad label line.
|
||||||
|
{
|
||||||
|
config: `
|
||||||
|
test.*.*
|
||||||
|
name=foo
|
||||||
|
`,
|
||||||
|
configBad: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, scenario := range scenarios {
|
||||||
|
mapper := metricMapper{}
|
||||||
|
err := mapper.initFromString(scenario.config)
|
||||||
|
if err != nil && !scenario.configBad {
|
||||||
|
t.Fatalf("%d. Config load error: %s", i, err)
|
||||||
|
}
|
||||||
|
if err == nil && scenario.configBad {
|
||||||
|
t.Fatalf("%d. Expected bad config, but loaded ok", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric, mapping := range scenario.mappings {
|
||||||
|
labels, present := mapper.getMapping(metric)
|
||||||
|
if len(labels) == 0 && present {
|
||||||
|
t.Fatalf("%d.%q: Expected metric to not be present", i, metric)
|
||||||
|
}
|
||||||
|
if len(labels) != len(mapping) {
|
||||||
|
t.Fatalf("%d.%q: Expected %d labels, got %d", i, metric, len(mapping), len(labels))
|
||||||
|
}
|
||||||
|
for label, value := range labels {
|
||||||
|
if mapping[label] != value {
|
||||||
|
t.Fatalf("%d.%q: Expected labels %v, got %v", i, metric, mapping, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue