Add support for Librato-style tags

Signed-off-by: Tony Wooster <twooster@gmail.com>
This commit is contained in:
Tony Wooster 2019-09-14 16:29:22 +02:00
parent a35d17c160
commit 2933dd8ad0
4 changed files with 132 additions and 25 deletions

View file

@ -29,15 +29,37 @@ We recommend this only as an intermediate solution and recommend switching to
[native Prometheus instrumentation](http://prometheus.io/docs/instrumenting/clientlibs/) [native Prometheus instrumentation](http://prometheus.io/docs/instrumenting/clientlibs/)
in the long term. in the long term.
### DogStatsD extensions ### Tagging Extensions
The exporter will convert DogStatsD-style tags to prometheus labels. See The exporter supports both Librato-style tags and DogStatsD-style tags,
[Tags](https://docs.datadoghq.com/developers/dogstatsd/data_types/#tagging) in the DogStatsD which will be converted into Prometheus labels.
documentation for the concept description and
[Datagram Format](https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/) For Librato-style tags, they must be appended to the metric name, as so:
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. metric.name#tagName=val,tag2Name=val2:0|c
```
See the [statsd-librato-backend README](https://github.com/librato/statsd-librato-backend#tags)
for a more complete description.
For DogStatsD-style tags, they're appended as another section at the end of the
metric, as so:
```
metric.name:0|c|#tagName=val,tag2Name=val2
```
See [Tags](https://docs.datadoghq.com/developers/dogstatsd/data_types/#tagging)
in the DogStatsD documentation for the concept description and
[Datagram Format](https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/).
Although you can use both tag types simultaneously, this is not recommended.
DogStatsD `name=value` pairs will take priority over Librato tags with the same
name.
For both Librato and DogStatsD tags, tags without values (`#some_tag`) are not
supported.
## Building and Running ## Building and Running

View file

@ -127,6 +127,26 @@ func TestHandlePacket(t *testing.T) {
labels: map[string]string{"tag1": "bar", "tag2": "baz"}, labels: map[string]string{"tag1": "bar", "tag2": "baz"},
}, },
}, },
}, {
name: "librato tag extension",
in: "foo#tag1=bar,tag2=baz:100|c",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"tag1": "bar", "tag2": "baz"},
},
},
}, {
name: "librato tag extension with tag keys unsupported by prometheus",
in: "foo#09digits=0,tag.with.dots=1:100|c",
out: Events{
&CounterEvent{
metricName: "foo",
value: 100,
labels: map[string]string{"_09digits": "0", "tag_with_dots": "1"},
},
},
}, { }, {
name: "datadog tag extension", name: "datadog tag extension",
in: "foo:100|c|#tag1:bar,tag2:baz", in: "foo:100|c|#tag1:bar,tag2:baz",
@ -197,6 +217,16 @@ func TestHandlePacket(t *testing.T) {
labels: map[string]string{"tag1": "bar", "tag2": "baz"}, labels: map[string]string{"tag1": "bar", "tag2": "baz"},
}, },
}, },
}, {
name: "librato and datadog tag extension with sampling",
in: "foo#tag1=foo,tag3=bing:100|c|@0.1|#tag1:bar,#tag2:baz",
out: Events{
&CounterEvent{
metricName: "foo",
value: 1000,
labels: map[string]string{"tag1": "bar", "tag3": "bing", "tag2": "baz"},
},
},
}, { }, {
name: "histogram with sampling", name: "histogram with sampling",
in: "foo:0.01|h|@0.2|#tag1:bar,#tag2:baz", in: "foo:0.01|h|@0.2|#tag1:bar,#tag2:baz",

View file

@ -275,7 +275,63 @@ func buildEvent(statType, metric string, value float64, relative bool, labels ma
} }
} }
func handleDogStatsDTagToKeyValue(labels map[string]string, component, tag string) { func handleLibratoTag(component, tag string, labels map[string]string) {
// Empty tag is an error
if len(tag) == 0 {
tagErrors.Inc()
log.Debugf("Empty Librato tag in component %s", component)
return
}
for i, c := range tag {
if c == '=' {
k := tag[:i]
v := tag[i+1:]
if len(k) == 0 || len(v) == 0 {
// Empty key or value, so it's an error
tagErrors.Inc()
log.Debugf("Malformed Librato tag %s=%s in component %s", k, v, component)
} else {
labels[escapeMetricName(k)] = v
}
return
}
}
// Could not find an equals sign, so it's an error
tagErrors.Inc()
log.Debugf("Malformed Librato tag %s in component %s", tag, component)
}
func parseLibratoTags(component string, labels map[string]string) {
lastTagEndIndex := 0
for i, c := range component {
if c == ',' {
tag := component[lastTagEndIndex:i]
lastTagEndIndex = i + 1
handleLibratoTag(component, tag, labels)
}
}
// If we're not off the end of the string, add the last tag
if lastTagEndIndex < len(component) {
tag := component[lastTagEndIndex:]
handleLibratoTag(component, tag, labels)
}
}
func parseNameAndLibratoLabels(name string, labels map[string]string) string {
for i, c := range name {
if c == '#' {
parseLibratoTags(name[i+1:], labels)
return name[:i]
}
}
return name
}
func handleDogStatsDTag(component, tag string, labels map[string]string) {
// Bail early if the tag is empty // Bail early if the tag is empty
if len(tag) == 0 { if len(tag) == 0 {
tagErrors.Inc() tagErrors.Inc()
@ -305,30 +361,23 @@ func handleDogStatsDTagToKeyValue(labels map[string]string, component, tag strin
} }
labels[escapeMetricName(k)] = v labels[escapeMetricName(k)] = v
return
} }
func parseDogStatsDTagsToLabels(component string) map[string]string { func parseDogStatsDTagsToLabels(component string, labels map[string]string) {
labels := map[string]string{}
tagsReceived.Inc()
lastTagEndIndex := 0 lastTagEndIndex := 0
for i, c := range component { for i, c := range component {
if c == ',' { if c == ',' {
tag := component[lastTagEndIndex:i] tag := component[lastTagEndIndex:i]
lastTagEndIndex = i + 1 lastTagEndIndex = i + 1
handleDogStatsDTagToKeyValue(labels, component, tag) handleDogStatsDTag(component, tag, labels)
} }
} }
// If we're not off the end of the string, add the last tag // If we're not off the end of the string, add the last tag
if lastTagEndIndex < len(component) { if lastTagEndIndex < len(component) {
tag := component[lastTagEndIndex:] tag := component[lastTagEndIndex:]
handleDogStatsDTagToKeyValue(labels, component, tag) handleDogStatsDTag(component, tag, labels)
} }
return labels
} }
func lineToEvents(line string) Events { func lineToEvents(line string) Events {
@ -343,7 +392,9 @@ func lineToEvents(line string) Events {
log.Debugln("Bad line from StatsD:", line) log.Debugln("Bad line from StatsD:", line)
return events return events
} }
metric := elements[0]
labels := map[string]string{}
metric := parseNameAndLibratoLabels(elements[0], labels)
var samples []string var samples []string
if strings.Contains(elements[1], "|#") { if strings.Contains(elements[1], "|#") {
// using datadog extensions, disable multi-metrics // using datadog extensions, disable multi-metrics
@ -376,7 +427,6 @@ samples:
} }
multiplyEvents := 1 multiplyEvents := 1
labels := map[string]string{}
if len(components) >= 3 { if len(components) >= 3 {
for _, component := range components[2:] { for _, component := range components[2:] {
if len(component) == 0 { if len(component) == 0 {
@ -407,7 +457,7 @@ samples:
multiplyEvents = int(1 / samplingFactor) multiplyEvents = int(1 / samplingFactor)
} }
case '#': case '#':
labels = parseDogStatsDTagsToLabels(component[1:]) parseDogStatsDTagsToLabels(component[1:], labels)
default: default:
log.Debugf("Invalid sampling factor or tag section %s on line %s", components[2], line) log.Debugf("Invalid sampling factor or tag section %s on line %s", components[2], line)
sampleErrors.WithLabelValues("invalid_sample_factor").Inc() sampleErrors.WithLabelValues("invalid_sample_factor").Inc()
@ -416,6 +466,10 @@ samples:
} }
} }
if len(labels) > 0 {
tagsReceived.Inc()
}
for i := 0; i < multiplyEvents; i++ { for i := 0; i < multiplyEvents; i++ {
event, err := buildEvent(statType, metric, value, relative, labels) event, err := buildEvent(statType, metric, value, relative, labels)
if err != nil { if err != nil {

View file

@ -953,7 +953,8 @@ func BenchmarkParseDogStatsDTagsToLabels(b *testing.B) {
for name, tags := range scenarios { for name, tags := range scenarios {
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
parseDogStatsDTagsToLabels(tags) labels := map[string]string{}
parseDogStatsDTagsToLabels(tags, labels)
} }
}) })
} }