From 31b8f1d40a6c28fb5103f74e9c3f8be96ff85329 Mon Sep 17 00:00:00 2001 From: Marwan Sulaiman Date: Fri, 24 Apr 2020 19:34:36 -0400 Subject: [PATCH] add datadog metrics exporter --- exporters/metric/datadog/datadog.go | 178 +++++++++++++++++++++++ exporters/metric/datadog/example_test.go | 34 +++++ exporters/metric/datadog/go.mod | 9 ++ exporters/metric/datadog/go.sum | 80 ++++++++++ 4 files changed, 301 insertions(+) create mode 100644 exporters/metric/datadog/datadog.go create mode 100644 exporters/metric/datadog/example_test.go create mode 100644 exporters/metric/datadog/go.mod create mode 100644 exporters/metric/datadog/go.sum diff --git a/exporters/metric/datadog/datadog.go b/exporters/metric/datadog/datadog.go new file mode 100644 index 00000000000..f20e323d4f3 --- /dev/null +++ b/exporters/metric/datadog/datadog.go @@ -0,0 +1,178 @@ +package datadog + +import ( + "context" + "fmt" + "regexp" + + "github.com/DataDog/datadog-go/statsd" + "go.opentelemetry.io/otel/api/core" + export "go.opentelemetry.io/otel/sdk/export/metric" + "go.opentelemetry.io/otel/sdk/export/metric/aggregator" +) + +const ( + // DefaultStatsAddrUDP specifies the default protocol (UDP) and address + // for the DogStatsD service. + DefaultStatsAddrUDP = "localhost:8125" +) + +// NewExporter exports to a datadog client +func NewExporter(opts Options) (*Exporter, error) { + if opts.StatsAddr == "" { + opts.StatsAddr = DefaultStatsAddrUDP + } + if opts.MetricNameFormatter == nil { + opts.MetricNameFormatter = defaultFormatter + } + client, err := statsd.New(opts.StatsAddr) + if err != nil { + return nil, err + } + return &Exporter{ + client: client, + opts: opts, + }, nil +} + +// Options contains options for configuring the exporter. +type Options struct { + // StatsAddr specifies the host[:port] address for DogStatsD. It defaults + // to localhost:8125. + StatsAddr string + + // Tags specifies a set of global tags to attach to each metric. + Tags []string + + // UseDistribution uses a DataDog Distribution type instead of Histogram + UseDistribution bool + + // MetricNameFormatter lets you customize the metric name that gets sent to + // datadog before exporting + MetricNameFormatter func(namespace, name string) string +} + +// Exporter forwards metrics to a DataDog agent +type Exporter struct { + opts Options + client *statsd.Client +} + +const rate = 1 + +func defaultFormatter(namespace, name string) string { + return name +} + +func (e *Exporter) Export(ctx context.Context, cs export.CheckpointSet) error { + return cs.ForEach(func(r export.Record) error { + agg := r.Aggregator() + name := e.sanitizeMetricName(r.Descriptor().LibraryName(), r.Descriptor().Name()) + itr := r.Labels().Iter() + tags := append([]string{}, e.opts.Tags...) + for itr.Next() { + label := itr.Label() + tag := string(label.Key) + ":" + label.Value.Emit() + tags = append(tags, tag) + } + switch agg := agg.(type) { + case aggregator.Points: + numbers, err := agg.Points() + if err != nil { + return fmt.Errorf("error getting Points for %s: %w", name, err) + } + f := e.client.Histogram + if e.opts.UseDistribution { + f = e.client.Distribution + } + for _, n := range numbers { + if err := f(name, metricValue(r.Descriptor().NumberKind(), n), tags, rate); err != nil { + return fmt.Errorf("error submitting %s point: %w", name, err) + } + } + case aggregator.MinMaxSumCount: + type record struct { + name string + f func() (core.Number, error) + } + recs := []record{ + { + name: name + ".min", + f: agg.Min, + }, + { + name: name + ".max", + f: agg.Max, + }, + } + if dist, ok := agg.(aggregator.Distribution); ok { + recs = append(recs, + record{name: name + ".median", f: func() (core.Number, error) { + return dist.Quantile(0.5) + }}, + record{name: name + ".p95", f: func() (core.Number, error) { + return dist.Quantile(0.95) + }}, + ) + } + for _, rec := range recs { + val, err := rec.f() + if err != nil { + return fmt.Errorf("error getting MinMaxSumCount value for %s: %w", name, err) + } + if err := e.client.Gauge(rec.name, metricValue(r.Descriptor().NumberKind(), val), tags, rate); err != nil { + return fmt.Errorf("error submitting %s point: %w", name, err) + } + } + case aggregator.Sum: + val, err := agg.Sum() + if err != nil { + return fmt.Errorf("error getting Sum value for %s: %w", name, err) + } + if err := e.client.Count(name, val.AsInt64(), tags, rate); err != nil { + return fmt.Errorf("error submitting %s point: %w", name, err) + } + case aggregator.LastValue: + val, _, err := agg.LastValue() + if err != nil { + return fmt.Errorf("error getting LastValue for %s: %w", name, err) + } + if err := e.client.Gauge(name, metricValue(r.Descriptor().NumberKind(), val), tags, rate); err != nil { + return fmt.Errorf("error submitting %s point: %w", name, err) + } + } + return nil + }) +} + +// Close cloess the underlying datadog client which flushes +// any pending buffers +func (e *Exporter) Close() error { + return e.client.Close() +} + +// sanitizeMetricName formats the custom namespace and view name to +// Datadog's metric naming convention +func (e *Exporter) sanitizeMetricName(namespace, name string) string { + return sanitizeString(e.opts.MetricNameFormatter(namespace, name)) +} + +// regex pattern +var reg = regexp.MustCompile("[^a-zA-Z0-9]+") + +// sanitizeString replaces all non-alphanumerical characters to underscore +func sanitizeString(str string) string { + return reg.ReplaceAllString(str, "_") +} + +func metricValue(kind core.NumberKind, number core.Number) float64 { + switch kind { + case core.Float64NumberKind: + return number.AsFloat64() + case core.Int64NumberKind: + return float64(number.AsInt64()) + case core.Uint64NumberKind: + return float64(number.AsUint64()) + } + return float64(number) +} diff --git a/exporters/metric/datadog/example_test.go b/exporters/metric/datadog/example_test.go new file mode 100644 index 00000000000..e9a86e5c526 --- /dev/null +++ b/exporters/metric/datadog/example_test.go @@ -0,0 +1,34 @@ +package datadog_test + +import ( + "context" + "time" + + "github.com/DataDog/sketches-go/ddsketch" + "go.opentelemetry.io/contrib/exporters/metric/datadog" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/metric" + export "go.opentelemetry.io/otel/sdk/export/metric" + "go.opentelemetry.io/otel/sdk/metric/batcher/ungrouped" + "go.opentelemetry.io/otel/sdk/metric/controller/push" + "go.opentelemetry.io/otel/sdk/metric/selector/simple" +) + +func ExampleExporter() { + selector := simple.NewWithSketchMeasure(ddsketch.NewDefaultConfig()) + batcher := ungrouped.New(selector, export.NewDefaultLabelEncoder(), false) + exp, err := datadog.NewExporter(datadog.Options{ + Tags: []string{"env:dev"}, + }) + if err != nil { + panic(err) + } + defer exp.Close() + pusher := push.New(batcher, exp, time.Second*10) + defer pusher.Stop() + pusher.Start() + global.SetMeterProvider(pusher) + meter := global.Meter("marwandist") + m := metric.Must(meter).NewInt64Counter("mycounter") + meter.RecordBatch(context.Background(), nil, m.Measurement(19)) +} diff --git a/exporters/metric/datadog/go.mod b/exporters/metric/datadog/go.mod new file mode 100644 index 00000000000..1b59051d5c2 --- /dev/null +++ b/exporters/metric/datadog/go.mod @@ -0,0 +1,9 @@ +module go.opentelemetry.io/contrib/exporters/metric/datadog + +go 1.14 + +require ( + github.com/DataDog/datadog-go v3.5.0+incompatible + github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 + go.opentelemetry.io/otel v0.4.2 +) diff --git a/exporters/metric/datadog/go.sum b/exporters/metric/datadog/go.sum new file mode 100644 index 00000000000..0dc7aae7456 --- /dev/null +++ b/exporters/metric/datadog/go.sum @@ -0,0 +1,80 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.5.0+incompatible h1:AShr9cqkF+taHjyQgcBcQUt/ZNK+iPq4ROaZwSX5c/U= +github.com/DataDog/datadog-go v3.5.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w= +github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opentelemetry.io/otel v0.4.2 h1:nT+GOqqRR1cIY92xmo1DeiXLHtIlXH1KLRgnsnhuNrs= +go.opentelemetry.io/otel v0.4.2/go.mod h1:OgNpQOjrlt33Ew6Ds0mGjmcTQg/rhUctsbkRdk/g1fw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=