-
Notifications
You must be signed in to change notification settings - Fork 446
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial integration of DogStatsD (#585)
* Initial integration of DogStatsD including something called mogrifiers Signed-off-by: Josh Jaques <[email protected]> * fix style errors in doc Signed-off-by: Josh Jaques <[email protected]> * Fix variable name in README Signed-off-by: Josh Jaques <[email protected]> * Add validations and test cases Signed-off-by: Josh Jaques <[email protected]> * Amendment with improvements - handle out of bounds match in pattern handler - make it an error if both statsd sink are enabled - improve error wording - add more test cases Signed-off-by: Josh Jaques <[email protected]> * fix incorrect timer manipulation Signed-off-by: Josh Jaques <[email protected]> --------- Signed-off-by: Josh Jaques <[email protected]>
- Loading branch information
Showing
8 changed files
with
483 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package godogstats | ||
|
||
import ( | ||
"regexp" | ||
"strconv" | ||
"time" | ||
|
||
"github.com/DataDog/datadog-go/v5/statsd" | ||
gostats "github.com/lyft/gostats" | ||
) | ||
|
||
type godogStatsSink struct { | ||
client *statsd.Client | ||
config struct { | ||
host string | ||
port int | ||
} | ||
|
||
mogrifier mogrifierMap | ||
} | ||
|
||
// ensure that godogStatsSink implements gostats.Sink | ||
var _ gostats.Sink = (*godogStatsSink)(nil) | ||
|
||
type goDogStatsSinkOption func(*godogStatsSink) | ||
|
||
func WithStatsdHost(host string) goDogStatsSinkOption { | ||
return func(g *godogStatsSink) { | ||
g.config.host = host | ||
} | ||
} | ||
|
||
func WithStatsdPort(port int) goDogStatsSinkOption { | ||
return func(g *godogStatsSink) { | ||
g.config.port = port | ||
} | ||
} | ||
|
||
func WithMogrifier(mogrifiers map[*regexp.Regexp]func([]string) (string, []string)) goDogStatsSinkOption { | ||
return func(g *godogStatsSink) { | ||
g.mogrifier = mogrifiers | ||
} | ||
} | ||
|
||
func WithMogrifierFromEnv(keys []string) goDogStatsSinkOption { | ||
return func(g *godogStatsSink) { | ||
mogrifier, err := newMogrifierMapFromEnv(keys) | ||
if err != nil { | ||
panic(err) | ||
} | ||
g.mogrifier = mogrifier | ||
} | ||
} | ||
|
||
func NewSink(opts ...goDogStatsSinkOption) (*godogStatsSink, error) { | ||
sink := &godogStatsSink{} | ||
for _, opt := range opts { | ||
opt(sink) | ||
} | ||
client, err := statsd.New(sink.config.host+":"+strconv.Itoa(sink.config.port), statsd.WithoutClientSideAggregation()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sink.client = client | ||
return sink, nil | ||
} | ||
|
||
func (g *godogStatsSink) FlushCounter(name string, value uint64) { | ||
name, tags := g.mogrifier.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) | ||
g.client.Gauge(name, float64(value), tags, 1.0) | ||
} | ||
|
||
func (g *godogStatsSink) FlushTimer(name string, milliseconds float64) { | ||
name, tags := g.mogrifier.mogrify(name) | ||
duration := time.Duration(milliseconds) * time.Millisecond | ||
g.client.Timing(name, duration, tags, 1.0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package godogstats | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
|
||
"github.com/kelseyhightower/envconfig" | ||
) | ||
|
||
var varFinder = regexp.MustCompile(`\$\d+`) // matches $0, $1, etc. | ||
|
||
const envPrefix = "DOG_STATSD_MOGRIFIER" // prefix for environment variables | ||
|
||
// mogrifierMap is a map of regular expressions to functions that mogrify a name and return tags | ||
type mogrifierMap map[*regexp.Regexp]func([]string) (string, []string) | ||
|
||
// makePatternHandler returns a function that replaces $0, $1, etc. in the pattern with the corresponding match | ||
func makePatternHandler(pattern string) func([]string) string { | ||
return func(matches []string) string { | ||
return varFinder.ReplaceAllStringFunc(pattern, func(s string) string { | ||
i, err := strconv.Atoi(s[1:]) | ||
if i >= len(matches) || err != nil { | ||
// Return the original placeholder if the index is out of bounds | ||
// or the Atoi fails, though given the varFinder regex it should | ||
// not be possible. | ||
return s | ||
} | ||
return matches[i] | ||
}) | ||
} | ||
} | ||
|
||
// newMogrifierMapFromEnv loads mogrifiers from environment variables | ||
// keys is a list of mogrifier names to load | ||
func newMogrifierMapFromEnv(keys []string) (mogrifierMap, error) { | ||
mogrifiers := mogrifierMap{} | ||
|
||
type config struct { | ||
Pattern string `envconfig:"PATTERN"` | ||
Tags map[string]string `envconfig:"TAGS"` | ||
Name string `envconfig:"NAME"` | ||
} | ||
|
||
for _, mogrifier := range keys { | ||
cfg := config{} | ||
if err := envconfig.Process(envPrefix+"_"+mogrifier, &cfg); err != nil { | ||
return nil, fmt.Errorf("failed to load mogrifier %s: %v", mogrifier, err) | ||
} | ||
|
||
if cfg.Pattern == "" { | ||
return nil, fmt.Errorf("no PATTERN specified for mogrifier %s", mogrifier) | ||
} | ||
|
||
re, err := regexp.Compile(cfg.Pattern) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to compile pattern for %s: %s: %v", mogrifier, cfg.Pattern, err) | ||
} | ||
|
||
if cfg.Name == "" { | ||
return nil, fmt.Errorf("no NAME specified for mogrifier %s", mogrifier) | ||
} | ||
|
||
nameHandler := makePatternHandler(cfg.Name) | ||
tagHandlers := make(map[string]func([]string) string, len(cfg.Tags)) | ||
for key, value := range cfg.Tags { | ||
if key == "" { | ||
return nil, fmt.Errorf("no key specified for tag %s for mogrifier %s", key, mogrifier) | ||
} | ||
tagHandlers[key] = makePatternHandler(value) | ||
if value == "" { | ||
return nil, fmt.Errorf("no value specified for tag %s for mogrifier %s", key, mogrifier) | ||
} | ||
} | ||
|
||
mogrifiers[re] = func(matches []string) (string, []string) { | ||
name := nameHandler(matches) | ||
tags := make([]string, 0, len(tagHandlers)) | ||
for tagKey, handler := range tagHandlers { | ||
tagValue := handler(matches) | ||
tags = append(tags, tagKey+":"+tagValue) | ||
} | ||
return name, tags | ||
} | ||
|
||
} | ||
return mogrifiers, nil | ||
} | ||
|
||
// mogrify applies the first mogrifier in the map that matches the name | ||
func (m mogrifierMap) mogrify(name string) (string, []string) { | ||
if m == nil { | ||
return name, nil | ||
} | ||
for matcher, mogrifier := range m { | ||
matches := matcher.FindStringSubmatch(name) | ||
if len(matches) == 0 { | ||
continue | ||
} | ||
|
||
mogrifiedName, tags := mogrifier(matches) | ||
return mogrifiedName, tags | ||
} | ||
|
||
// no mogrification | ||
return name, nil | ||
} |
Oops, something went wrong.