Merge pull request #40 from Jimdo/dogstats-extensions-squashed

Dogstats extensions squashed
This commit is contained in:
Tobias Schmidt 2016-05-03 12:42:38 -04:00
commit ba9c37eb20
3 changed files with 184 additions and 39 deletions

View file

@ -26,6 +26,16 @@ We recommend this only as an intermediate solution and recommend switching to
[native Prometheus instrumentation](http://prometheus.io/docs/instrumenting/clientlibs/)
in the long term.
### DogStatsD extensions
The exporter will convert DogStatsD-style tags to prometheus labels. See
[Tags](http://docs.datadoghq.com/guides/dogstatsd/#tags) in the DogStatsD
documentation for the concept description and
[Datagram Format](http://docs.datadoghq.com/guides/dogstatsd/#datagram-format)
for specifics. It boils down to appending
`|#tag:value,another_tag:another_value` to the normal StatsD format. Tags
without values (`#some_tag`) are not supported.
## Building and Running
$ go build

View file

@ -14,7 +14,7 @@
package main
import (
"fmt"
"reflect"
"testing"
)
@ -33,6 +33,7 @@ func TestHandlePacket(t *testing.T) {
&CounterEvent{
metricName: "foo",
value: 2,
labels: map[string]string{},
},
},
}, {
@ -42,6 +43,7 @@ func TestHandlePacket(t *testing.T) {
&GaugeEvent{
metricName: "foo",
value: 3,
labels: map[string]string{},
},
},
}, {
@ -51,6 +53,87 @@ func TestHandlePacket(t *testing.T) {
&TimerEvent{
metricName: "foo",
value: 200,
labels: map[string]string{},
},
},
}, {
name: "datadog tag extension",
in: "foo:100|c|#tag1:bar,tag2:baz",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"tag1": "bar", "tag2": "baz"},
},
},
}, {
name: "datadog tag extension with # in all keys (as sent by datadog php client)",
in: "foo:100|c|#tag1:bar,#tag2:baz",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"tag1": "bar", "tag2": "baz"},
},
},
}, {
name: "datadog tag extension with tag keys unsupported by prometheus",
in: "foo:100|c|#09digits:0,tag.with.dots:1",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"_09digits": "0", "tag_with_dots": "1"},
},
},
}, {
name: "datadog tag extension with valueless tags: ignored",
in: "foo:100|c|#tag_without_a_value",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{},
},
},
}, {
name: "datadog tag extension with valueless tags (edge case)",
in: "foo:100|c|#tag_without_a_value,tag:value",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"tag": "value"},
},
},
}, {
name: "datadog tag extension with empty tags (edge case)",
in: "foo:100|c|#tag:value,,",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"tag": "value"},
},
},
}, {
name: "datadog tag extension with sampling",
in: "foo:100|c|@0.1|#tag1:bar,#tag2:baz",
out: Events{
&CounterEvent{
metricName: "foo",
value: 1000,
labels: map[string]string{"tag1": "bar", "tag2": "baz"},
},
},
}, {
name: "datadog tag extension with multiple colons",
in: "foo:100|c|@0.1|#tag1:foo:bar",
out: Events{
&CounterEvent{
metricName: "foo",
value: 1000,
labels: map[string]string{"tag1": "foo:bar"},
},
},
}, {
@ -60,26 +143,32 @@ func TestHandlePacket(t *testing.T) {
&TimerEvent{
metricName: "foo",
value: 200,
labels: map[string]string{},
},
&TimerEvent{
metricName: "foo",
value: 300,
labels: map[string]string{},
},
&CounterEvent{
metricName: "foo",
value: 50,
labels: map[string]string{},
},
&GaugeEvent{
metricName: "foo",
value: 6,
labels: map[string]string{},
},
&CounterEvent{
metricName: "bar",
value: 1,
labels: map[string]string{},
},
&TimerEvent{
metricName: "bar",
value: 5,
labels: map[string]string{},
},
},
}, {
@ -94,6 +183,13 @@ func TestHandlePacket(t *testing.T) {
}, {
name: "illegal sampling factor",
in: "foo:1|c|@bar",
out: Events{
&CounterEvent{
metricName: "foo",
value: 1,
labels: map[string]string{},
},
},
}, {
name: "zero sampling factor",
in: "foo:2|c|@0",
@ -101,6 +197,7 @@ func TestHandlePacket(t *testing.T) {
&CounterEvent{
metricName: "foo",
value: 2,
labels: map[string]string{},
},
},
}, {
@ -121,12 +218,12 @@ func TestHandlePacket(t *testing.T) {
}
if len(actual) != len(scenario.out) {
t.Fatalf("%d. Expected %d events, got %d", i, len(scenario.out), len(actual))
t.Fatalf("%d. Expected %d events, got %d in scenario '%s'", i, len(scenario.out), len(actual), scenario.name)
}
for j, expected := range scenario.out {
if fmt.Sprintf("%v", actual[j]) != fmt.Sprintf("%v", expected) {
t.Fatalf("%d.%d. Expected %v, got %v", i, j, actual[j], expected)
if !reflect.DeepEqual(&expected, &actual[j]) {
t.Fatalf("%d.%d. Expected %#v, got %#v in scenario '%s'", i, j, expected, actual[j], scenario.name)
}
}
}

View file

@ -143,31 +143,38 @@ func (c *SummaryContainer) Get(metricName string, labels prometheus.Labels) prom
type Event interface {
MetricName() string
Value() float64
Labels() map[string]string
}
type CounterEvent struct {
metricName string
value float64
labels map[string]string
}
func (c *CounterEvent) MetricName() string { return c.metricName }
func (c *CounterEvent) Value() float64 { return c.value }
func (c *CounterEvent) MetricName() string { return c.metricName }
func (c *CounterEvent) Value() float64 { return c.value }
func (c *CounterEvent) Labels() map[string]string { return c.labels }
type GaugeEvent struct {
metricName string
value float64
labels map[string]string
}
func (g *GaugeEvent) MetricName() string { return g.metricName }
func (g *GaugeEvent) Value() float64 { return g.value }
func (g *GaugeEvent) MetricName() string { return g.metricName }
func (g *GaugeEvent) Value() float64 { return g.value }
func (c *GaugeEvent) Labels() map[string]string { return c.labels }
type TimerEvent struct {
metricName string
value float64
labels map[string]string
}
func (t *TimerEvent) MetricName() string { return t.metricName }
func (t *TimerEvent) Value() float64 { return t.value }
func (t *TimerEvent) MetricName() string { return t.metricName }
func (t *TimerEvent) Value() float64 { return t.value }
func (c *TimerEvent) Labels() map[string]string { return c.labels }
type Events []Event
@ -198,7 +205,7 @@ func (b *Exporter) Listen(e <-chan Events) {
}
for _, event := range events {
metricName := ""
prometheusLabels := prometheus.Labels{}
prometheusLabels := event.Labels()
labels, present := b.mapper.getMapping(event.MetricName())
if present {
@ -268,22 +275,25 @@ type StatsDListener struct {
conn *net.UDPConn
}
func buildEvent(statType, metric string, value float64) (Event, error) {
func buildEvent(statType, metric string, value float64, labels map[string]string) (Event, error) {
switch statType {
case "c":
return &CounterEvent{
metricName: metric,
value: float64(value),
labels: labels,
}, nil
case "g":
return &GaugeEvent{
metricName: metric,
value: float64(value),
labels: labels,
}, nil
case "ms":
case "ms", "h":
return &TimerEvent{
metricName: metric,
value: float64(value),
labels: labels,
}, nil
case "s":
return nil, fmt.Errorf("No support for StatsD sets")
@ -303,6 +313,25 @@ func (l *StatsDListener) Listen(e chan<- Events) {
}
}
func parseDogStatsDTagsToLabels(component string) map[string]string {
labels := map[string]string{}
networkStats.WithLabelValues("dogstatsd_tags").Inc()
tags := strings.Split(component, ",")
for _, t := range tags {
t = strings.TrimPrefix(t, "#")
kv := strings.SplitN(t, ":", 2)
if len(kv) < 2 || len(kv[1]) == 0 {
networkStats.WithLabelValues("malformed_dogstatsd_tag").Inc()
log.Printf("Malformed or empty DogStatsD tag %s in component %s", t, component)
continue
}
labels[escapeMetricName(kv[0])] = kv[1]
}
return labels
}
func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) {
lines := strings.Split(string(packet), "\n")
events := Events{}
@ -311,18 +340,24 @@ func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) {
continue
}
elements := strings.Split(line, ":")
elements := strings.SplitN(line, ":", 2)
if len(elements) < 2 {
networkStats.WithLabelValues("malformed_line").Inc()
log.Println("Bad line from StatsD:", line)
continue
}
metric := elements[0]
samples := elements[1:]
var samples []string
if strings.Contains(elements[1], "|#") {
// using datadog extensions, disable multi-metrics
samples = elements[1:]
} else {
samples = strings.Split(elements[1], ":")
}
for _, sample := range samples {
components := strings.Split(sample, "|")
samplingFactor := 1.0
if len(components) < 2 || len(components) > 3 {
if len(components) < 2 || len(components) > 4 {
networkStats.WithLabelValues("malformed_component").Inc()
log.Println("Bad component on line:", line)
continue
@ -335,32 +370,35 @@ func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) {
continue
}
if len(components) == 3 {
if statType != "c" {
log.Println("Illegal sampling factor for non-counter metric on line", line)
networkStats.WithLabelValues("illegal_sample_factor").Inc()
labels := map[string]string{}
if len(components) >= 3 {
for _, component := range components[2:] {
switch component[0] {
case '@':
if statType != "c" {
log.Println("Illegal sampling factor for non-counter metric on line", line)
networkStats.WithLabelValues("illegal_sample_factor").Inc()
}
samplingFactor, err = strconv.ParseFloat(component[1:], 64)
if err != nil {
log.Printf("Invalid sampling factor %s on line %s", component[1:], line)
networkStats.WithLabelValues("invalid_sample_factor").Inc()
}
if samplingFactor == 0 {
samplingFactor = 1
}
value /= samplingFactor
case '#':
labels = parseDogStatsDTagsToLabels(component)
default:
log.Printf("Invalid sampling factor or tag section %s on line %s", components[2], line)
networkStats.WithLabelValues("invalid_sample_factor").Inc()
continue
}
}
samplingStr := components[2]
if samplingStr[0] != '@' {
log.Printf("Invalid sampling factor %s on line %s", samplingStr, line)
networkStats.WithLabelValues("invalid_sample_factor").Inc()
continue
}
samplingFactor, err = strconv.ParseFloat(samplingStr[1:], 64)
if err != nil {
log.Printf("Invalid sampling factor %s on line %s", samplingStr, line)
networkStats.WithLabelValues("invalid_sample_factor").Inc()
continue
}
if samplingFactor == 0 {
// This should never happen, but avoid division by zero if it does.
log.Printf("Invalid zero sampling factor %s on line %s, setting to 1", samplingStr, line)
samplingFactor = 1
}
value /= samplingFactor
}
event, err := buildEvent(statType, metric, value)
event, err := buildEvent(statType, metric, value, labels)
if err != nil {
log.Printf("Error building event on line %s: %s", line, err)
networkStats.WithLabelValues("illegal_event").Inc()