diff --git a/README.md b/README.md index 90bea40..af1f3d3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bridge_test.go b/bridge_test.go index 4f180a2..05cc90a 100644 --- a/bridge_test.go +++ b/bridge_test.go @@ -58,42 +58,82 @@ func TestHandlePacket(t *testing.T) { }, }, { name: "datadog tag extension", - in: "foo:100|c|#tag1:bar,tag2:baz,tag3,tag4", + in: "foo:100|c|#tag1:bar,tag2:baz", out: Events{ &CounterEvent{ metricName: "foo", value: 100, - labels: map[string]string{"tag1": "bar", "tag2": "baz", "tag3": ".", "tag4": "."}, + 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,#tag3,#tag4", + in: "foo:100|c|#tag1:bar,#tag2:baz", out: Events{ &CounterEvent{ metricName: "foo", value: 100, - labels: map[string]string{"tag1": "bar", "tag2": "baz", "tag3": ".", "tag4": "."}, + labels: map[string]string{"tag1": "bar", "tag2": "baz"}, }, }, }, { - name: "datadog tag extension with tags unsupported by prometheus", - in: "foo:100|c|#09digits:0,tag.with.dots,tag_with_empty_value:", + 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": ".", "tag_with_empty_value": "."}, + 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,tag3:baz", + in: "foo:100|c|@0.1|#tag1:bar,#tag2:baz", out: Events{ &CounterEvent{ metricName: "foo", value: 1000, - labels: map[string]string{"tag1": "bar", "tag2": ".", "tag3": "baz"}, + 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"}, }, }, }, { diff --git a/exporter.go b/exporter.go index 1a0b369..9fd55d8 100644 --- a/exporter.go +++ b/exporter.go @@ -303,6 +303,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{} @@ -334,7 +353,6 @@ func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) { continue } valueStr, statType := components[0], components[1] - labels := map[string]string{} value, err := strconv.ParseFloat(valueStr, 64) if err != nil { log.Printf("Bad value %s on line: %s", valueStr, line) @@ -342,6 +360,7 @@ func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) { continue } + labels := map[string]string{} if len(components) >= 3 { for _, component := range components[2:] { switch component[0] { @@ -360,26 +379,7 @@ func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) { } value /= samplingFactor case '#': - networkStats.WithLabelValues("dogstatsd_tags").Inc() - tags := strings.Split(component, ",") - for _, t := range tags { - t = strings.TrimPrefix(t, "#") - kv := strings.Split(t, ":") - tag_key := kv[0] - tag_key = escapeMetricName(tag_key) - - var tag_value string - if len(kv) == 2 { - if len(kv[1]) > 0 { - tag_value = kv[1] - } else { - tag_value = "." - } - } else if len(kv) == 1 { - tag_value = "." - } - labels[tag_key] = tag_value - } + 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()