Merge pull request #267 from twooster/add-librato-tag-support

Add librato tag support
This commit is contained in:
Matthias Rampke 2019-09-20 09:19:46 +02:00 committed by GitHub
commit c1db8ba02d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 175 additions and 46 deletions

View file

@ -29,15 +29,48 @@ 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
### Tagging Extensions
The exporter will convert DogStatsD-style tags to prometheus labels. 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/)
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.
The exporter supports Librato, InfluxDB, and DogStatsD-style tags,
which will be converted into Prometheus labels.
For Librato-style tags, they must be appended to the metric name with a
delimiting `#`, as so:
```
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 InfluxDB-style tags, they must be appended to the metric name with a
delimiting comma, as so:
```
metric.name,tagName=val,tag2Name=val2:0|c
```
See [this InfluxDB blog post](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd)
for a larger overview.
For DogStatsD-style tags, they're appended as a `|#` delimited 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/).
If you encounter problems, note that this tagging style is incompatible with
the original `statsd` implementation.
Be aware: If you mix tag styles (e.g., Librato/InfluxDB with DogStatsD), the
exporter will consider this an error and the sample will be discarded. Also,
tags without values (`#some_tag`) are not supported and will be ignored.
## Building and Running

View file

@ -127,6 +127,46 @@ func TestHandlePacket(t *testing.T) {
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: "influxdb 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: "influxdb 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",
in: "foo:100|c|#tag1:bar,tag2:baz",
@ -197,6 +237,18 @@ func TestHandlePacket(t *testing.T) {
labels: map[string]string{"tag1": "bar", "tag2": "baz"},
},
},
}, {
name: "librato/dogstatsd mixed tag styles without sampling",
in: "foo#tag1=foo,tag3=bing:100|c|#tag1:bar,#tag2:baz",
out: Events{},
}, {
name: "influxdb/dogstatsd mixed tag styles without sampling",
in: "foo,tag1=foo,tag3=bing:100|c|#tag1:bar,#tag2:baz",
out: Events{},
}, {
name: "mixed tag styles with sampling",
in: "foo#tag1=foo,tag3=bing:100|c|@0.1|#tag1:bar,#tag2:baz",
out: Events{},
}, {
name: "histogram with sampling",
in: "foo:0.01|h|@0.2|#tag1:bar,#tag2:baz",

View file

@ -275,60 +275,88 @@ func buildEvent(statType, metric string, value float64, relative bool, labels ma
}
}
func handleDogStatsDTagToKeyValue(labels map[string]string, component, tag string) {
// Bail early if the tag is empty
func parseTag(component, tag string, separator rune, labels map[string]string) {
// Entirely empty tag is an error
if len(tag) == 0 {
tagErrors.Inc()
log.Debugf("Malformed or empty DogStatsD tag %s in component %s", tag, component)
log.Debugf("Empty name tag in component %s", component)
return
}
// Skip hash if found.
if tag[0] == '#' {
tag = tag[1:]
}
// find the first comma and split the tag into key and value.
var k, v string
for i, c := range tag {
if c == ':' {
k = tag[0:i]
v = tag[(i + 1):]
break
}
}
// If either of them is empty, then either the k or v is empty, or we
// didn't find a colon, either way, throw an error and skip ahead.
if c == separator {
k := tag[:i]
v := tag[i+1:]
if len(k) == 0 || len(v) == 0 {
// Empty key or value is an error
tagErrors.Inc()
log.Debugf("Malformed or empty DogStatsD tag %s in component %s", tag, component)
log.Debugf("Malformed name tag %s=%s in component %s", k, v, component)
} else {
labels[escapeMetricName(k)] = v
}
return
}
}
labels[escapeMetricName(k)] = v
return
// Missing separator (no value) is an error
tagErrors.Inc()
log.Debugf("Malformed name tag %s in component %s", tag, component)
}
func parseDogStatsDTagsToLabels(component string) map[string]string {
labels := map[string]string{}
tagsReceived.Inc()
func parseNameTags(component string, labels map[string]string) {
lastTagEndIndex := 0
for i, c := range component {
if c == ',' {
tag := component[lastTagEndIndex:i]
lastTagEndIndex = i + 1
handleDogStatsDTagToKeyValue(labels, component, tag)
parseTag(component, tag, '=', labels)
}
}
// If we're not off the end of the string, add the last tag
if lastTagEndIndex < len(component) {
tag := component[lastTagEndIndex:]
handleDogStatsDTagToKeyValue(labels, component, tag)
parseTag(component, tag, '=', labels)
}
}
func trimLeftHash(s string) string {
if s != "" && s[0] == '#' {
return s[1:]
}
return s
}
func parseDogStatsDTags(component string, labels map[string]string) {
lastTagEndIndex := 0
for i, c := range component {
if c == ',' {
tag := component[lastTagEndIndex:i]
lastTagEndIndex = i + 1
parseTag(component, trimLeftHash(tag), ':', labels)
}
}
return labels
// If we're not off the end of the string, add the last tag
if lastTagEndIndex < len(component) {
tag := component[lastTagEndIndex:]
parseTag(component, trimLeftHash(tag), ':', labels)
}
}
func parseNameAndTags(name string, labels map[string]string) string {
for i, c := range name {
// `#` delimits start of tags by Librato
// https://www.librato.com/docs/kb/collect/collection_agents/stastd/#stat-level-tags
// `,` delimits start of tags by InfluxDB
// https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd
if c == '#' || c == ',' {
parseNameTags(name[i+1:], labels)
return name[:i]
}
}
return name
}
func lineToEvents(line string) Events {
@ -343,10 +371,22 @@ func lineToEvents(line string) Events {
log.Debugln("Bad line from StatsD:", line)
return events
}
metric := elements[0]
labels := map[string]string{}
metric := parseNameAndTags(elements[0], labels)
var samples []string
if strings.Contains(elements[1], "|#") {
// using datadog extensions, disable multi-metrics
// using DogStatsD tags
// don't allow mixed tagging styles
if len(labels) > 0 {
sampleErrors.WithLabelValues("mixed_tagging_styles").Inc()
log.Debugln("Bad line (multiple tagging styles) from StatsD:", line)
return events
}
// disable multi-metrics
samples = elements[1:]
} else {
samples = strings.Split(elements[1], ":")
@ -376,7 +416,6 @@ samples:
}
multiplyEvents := 1
labels := map[string]string{}
if len(components) >= 3 {
for _, component := range components[2:] {
if len(component) == 0 {
@ -407,7 +446,7 @@ samples:
multiplyEvents = int(1 / samplingFactor)
}
case '#':
labels = parseDogStatsDTagsToLabels(component[1:])
parseDogStatsDTags(component[1:], labels)
default:
log.Debugf("Invalid sampling factor or tag section %s on line %s", components[2], line)
sampleErrors.WithLabelValues("invalid_sample_factor").Inc()
@ -416,6 +455,10 @@ samples:
}
}
if len(labels) > 0 {
tagsReceived.Inc()
}
for i := 0; i < multiplyEvents; i++ {
event, err := buildEvent(statType, metric, value, relative, labels)
if err != nil {

View file

@ -941,7 +941,7 @@ func BenchmarkEscapeMetricName(b *testing.B) {
}
}
func BenchmarkParseDogStatsDTagsToLabels(b *testing.B) {
func BenchmarkParseDogStatsDTags(b *testing.B) {
scenarios := map[string]string{
"1 tag w/hash": "#test:tag",
"1 tag w/o hash": "test:tag",
@ -953,7 +953,8 @@ func BenchmarkParseDogStatsDTagsToLabels(b *testing.B) {
for name, tags := range scenarios {
b.Run(name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
parseDogStatsDTagsToLabels(tags)
labels := map[string]string{}
parseDogStatsDTags(tags, labels)
}
})
}