Skip to content

Commit

Permalink
feat(dogstatsd_sink): support EXTRA_TAGS
Browse files Browse the repository at this point in the history
When using the godogstats sink, previously the EXTRA_TAGS would not be
emitted as datadog tags.

Signed-off-by: Josh Jaques <jjaques@gmail.com>
  • Loading branch information
JDeuce committed Jun 20, 2024
1 parent 0ddd444 commit dcbc8a4
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ First, enable an extra mogrifier:
Then, declare additional rules for the `DESCRIPTOR` mogrifier
1. `DOG_STATSD_MOGRIFIER_HITS_PATTERN`: `^ratelimit\.service\.rate_limit\.(.*)\.(.*)\.(.*)$`
1. `DOG_STATSD_MOGRIFIER_HITS_PATTERN`: `^ratelimit\.service\.rate_limit\.([^.]+)\.(.*)\.([^.]+)$`
2. `DOG_STATSD_MOGRIFIER_HITS_NAME`: `ratelimit.service.rate_limit.$3`
3. `DOG_STATSD_MOGRIFIER_HITS_TAGS`: `domain:$1,descriptor:$2`
Expand Down
53 changes: 50 additions & 3 deletions src/godogstats/dogstatsd_sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package godogstats
import (
"regexp"
"strconv"
"strings"
"time"

"github.com/DataDog/datadog-go/v5/statsd"
gostats "github.com/lyft/gostats"
logger "github.com/sirupsen/logrus"
)

type godogStatsSink struct {
Expand Down Expand Up @@ -65,18 +67,63 @@ func NewSink(opts ...goDogStatsSinkOption) (*godogStatsSink, error) {
return sink, nil
}

func (g *godogStatsSink) FlushCounter(name string, value uint64) {
// separateExtraTags separates the metric name and tags from the combined serialized metric name.
// e.g. given input: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT=12345.__DEPLOY=67890"
// this should produce output: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits", ["COMMIT:12345", "DEPLOY:67890"]
// Aligns to how tags are serialized here https://github.com/lyft/gostats/blob/49e70f1b7932d146fecd991be04f8e1ad235452c/internal/tags/tags.go#L335
func separateExtraTags(name string) (string, []string) {
const (
prefix = ".__"
sep = "="
)

// find the position of the first .__ if any
prefixPos := strings.Index(name, prefix)
if prefixPos == -1 {
return name, nil // no extra tags
}

// split the name and tags
shortName := name[:prefixPos]
tagString := name[prefixPos:]

// split the tags
tagPairs := strings.Split(tagString, prefix)
tags := make([]string, 0, len(tagPairs))
for _, tagPair := range tagPairs {
tag := strings.SplitN(tagPair, sep, 2)
if len(tag) != 2 {
logger.Debugf("godogstats sink found malformed extra tag: %v, string: %v", tag, name)
continue
}
tagName := tag[0]
tagValue := tag[1]
tags = append(tags, tagName+":"+tagValue)
}

return shortName, tags
}

// mogrify takes a serialized metric name as input (internal gostats format)
// and returns a metric name and list of tags (dogstatsd output format)
func (g *godogStatsSink) mogrify(name string) (string, []string) {
name, extraTags := separateExtraTags(name)
name, tags := g.mogrifier.mogrify(name)
return name, append(extraTags, tags...)
}

func (g *godogStatsSink) FlushCounter(name string, value uint64) {
name, tags := g.mogrify(name)
g.client.Count(name, int64(value), tags, 1.0)
}

func (g *godogStatsSink) FlushGauge(name string, value uint64) {
name, tags := g.mogrifier.mogrify(name)
name, tags := g.mogrify(name)
g.client.Gauge(name, float64(value), tags, 1.0)
}

func (g *godogStatsSink) FlushTimer(name string, milliseconds float64) {
name, tags := g.mogrifier.mogrify(name)
name, tags := g.mogrify(name)
duration := time.Duration(milliseconds) * time.Millisecond
g.client.Timing(name, duration, tags, 1.0)
}
98 changes: 98 additions & 0 deletions src/godogstats/dogstatsd_sink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package godogstats

import (
"regexp"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSeparateExtraTags(t *testing.T) {
tests := []struct {
name string
givenMetric string
expectOutput string
expectTags []string
}{
{
name: "no extra tags",
givenMetric: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectOutput: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectTags: nil,
},
{
name: "one extra tags",
givenMetric: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT=12345",
expectOutput: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectTags: []string{"COMMIT:12345"},
},
{
name: "two extra tags",
givenMetric: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT=12345.__DEPLOY=6890",
expectOutput: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectTags: []string{"COMMIT:12345", "DEPLOY:6890"},
},
{
name: "invalid extra tag no value",
givenMetric: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT",
expectOutput: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectTags: []string{},
},
}

for _, tt := range tests {
actualName, actualTags := separateExtraTags(tt.givenMetric)

assert.Equal(t, tt.expectOutput, actualName)
assert.Equal(t, tt.expectTags, actualTags)
}
}

func TestSinkMogrify(t *testing.T) {
g := &godogStatsSink{
mogrifier: mogrifierMap{
regexp.MustCompile(`^ratelimit\.(.*)$`): func(matches []string) (string, []string) {
return "custom." + matches[1], []string{"tag1:value1", "tag2:value2"}
},
},
}

tests := []struct {
name string
input string
expectedName string
expectedTags []string
}{
{
name: "mogrify with match and extra tags",
input: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT=12345.__DEPLOY=67890",
expectedName: "custom.service.rate_limit.mongo_cps.database_users.total_hits",
expectedTags: []string{"COMMIT:12345", "DEPLOY:67890", "tag1:value1", "tag2:value2"},
},
{
name: "mogrify with match without extra tags",
input: "ratelimit.service.rate_limit.mongo_cps.database_users.total_hits",
expectedName: "custom.service.rate_limit.mongo_cps.database_users.total_hits",
expectedTags: []string{"tag1:value1", "tag2:value2"},
},
{
name: "extra tags with no match",
input: "foo.service.rate_limit.mongo_cps.database_users.total_hits.__COMMIT=12345.__DEPLOY=67890",
expectedName: "foo.service.rate_limit.mongo_cps.database_users.total_hits",
expectedTags: []string{"COMMIT:12345", "DEPLOY:67890"},
},
{
name: "no mogrification",
input: "other.metric.name",
expectedName: "other.metric.name",
expectedTags: nil,
},
}

for _, tt := range tests {
actualName, actualTags := g.mogrify(tt.input)

assert.Equal(t, tt.expectedName, actualName)
assert.Equal(t, tt.expectedTags, actualTags)
}
}

0 comments on commit dcbc8a4

Please sign in to comment.