forked from mirrors/statsd_exporter
Merge pull request #267 from twooster/add-librato-tag-support
Add librato tag support
This commit is contained in:
commit
c1db8ba02d
4 changed files with 175 additions and 46 deletions
53
README.md
53
README.md
|
@ -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
|
||||
|
||||
|
@ -235,8 +268,8 @@ mappings:
|
|||
|
||||
Note that timers will be accepted with the `ms`, `h`, and `d` statsd types. The first two are timers and histograms and the `d` type is for DataDog's "distribution" type. The distribution type is treated identically to timers and histograms.
|
||||
|
||||
It should be noted that whereas timers in statsd expects the unit of timing data to be in milliseconds,
|
||||
prometheus expects the unit to be seconds. Hence, the exporter converts all timers to seconds
|
||||
It should be noted that whereas timers in statsd expects the unit of timing data to be in milliseconds,
|
||||
prometheus expects the unit to be seconds. Hence, the exporter converts all timers to seconds
|
||||
before exporting them.
|
||||
|
||||
### DogStatsD Client Behavior
|
||||
|
|
|
@ -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",
|
||||
|
|
111
exporter.go
111
exporter.go
|
@ -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 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 name tag %s=%s in component %s", k, v, component)
|
||||
} else {
|
||||
labels[escapeMetricName(k)] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// 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 len(k) == 0 || len(v) == 0 {
|
||||
tagErrors.Inc()
|
||||
log.Debugf("Malformed or empty DogStatsD tag %s in component %s", tag, component)
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue