diff --git a/exporter/datadogexporter/go.mod b/exporter/datadogexporter/go.mod index a36726e1fe8f..00f1a2c51154 100644 --- a/exporter/datadogexporter/go.mod +++ b/exporter/datadogexporter/go.mod @@ -5,4 +5,6 @@ go 1.15 require ( github.com/stretchr/testify v1.6.1 go.opentelemetry.io/collector v0.11.1-0.20201001213035-035aa5cf6c92 + go.uber.org/zap v1.16.0 + gopkg.in/zorkian/go-datadog-api.v2 v2.29.0 ) diff --git a/exporter/datadogexporter/go.sum b/exporter/datadogexporter/go.sum index 0183ba960c8d..f30baab377e2 100644 --- a/exporter/datadogexporter/go.sum +++ b/exporter/datadogexporter/go.sum @@ -1546,6 +1546,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200601152816-913338de1bd2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/zorkian/go-datadog-api.v2 v2.29.0 h1:S4AsWFkQ6JDG7WZfYk6C3EggsO/4IvGUsCfz7I3zjPk= +gopkg.in/zorkian/go-datadog-api.v2 v2.29.0/go.mod h1:kx0CSMRpzEZfx/nFH62GLU4stZjparh/BRpM89t4XCQ= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/exporter/datadogexporter/host.go b/exporter/datadogexporter/host.go new file mode 100644 index 000000000000..e21bd67ce347 --- /dev/null +++ b/exporter/datadogexporter/host.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datadogexporter + +import "os" + +// GetHost gets the hostname according to configuration. +// It gets the configuration hostname and if +// not available it relies on the OS hostname +func GetHost(cfg *Config) *string { + if cfg.TagsConfig.Hostname != "" { + return &cfg.TagsConfig.Hostname + } + + host, err := os.Hostname() + if err != nil || host == "" { + host = "unknown" + } + return &host +} diff --git a/exporter/datadogexporter/host_test.go b/exporter/datadogexporter/host_test.go new file mode 100644 index 000000000000..27f86cb2efc1 --- /dev/null +++ b/exporter/datadogexporter/host_test.go @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datadogexporter + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHost(t *testing.T) { + + host := GetHost(&Config{TagsConfig: TagsConfig{Hostname: "test_host"}}) + assert.Equal(t, *host, "test_host") + + host = GetHost(&Config{}) + osHostname, err := os.Hostname() + require.NoError(t, err) + assert.Equal(t, *host, osHostname) +} diff --git a/exporter/datadogexporter/metrics_translator.go b/exporter/datadogexporter/metrics_translator.go new file mode 100644 index 000000000000..1832028bb277 --- /dev/null +++ b/exporter/datadogexporter/metrics_translator.go @@ -0,0 +1,214 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datadogexporter + +import ( + "fmt" + + "go.opentelemetry.io/collector/consumer/pdata" + "go.uber.org/zap" + "gopkg.in/zorkian/go-datadog-api.v2" +) + +const ( + // Gauge is the Datadog Gauge metric type + Gauge string = "gauge" +) + +// newGauge creates a new Datadog Gauge metric given a name, a Unix nanoseconds timestamp +// a value and a slice of tags +func newGauge(name string, ts uint64, value float64, tags []string) datadog.Metric { + // Transform UnixNano timestamp into Unix timestamp + // 1 second = 1e9 ns + timestamp := float64(ts / 1e9) + + gauge := datadog.Metric{ + Points: []datadog.DataPoint{[2]*float64{×tamp, &value}}, + Tags: tags, + } + gauge.SetMetric(name) + gauge.SetType(Gauge) + return gauge +} + +// getTags maps a stringMap into a slice of Datadog tags +func getTags(labels pdata.StringMap) []string { + tags := make([]string, 0, labels.Len()) + labels.ForEach(func(key string, v pdata.StringValue) { + value := v.Value() + if value == "" { + // Tags can't end with ":" so we replace empty values with "n/a" + value = "n/a" + } + tags = append(tags, fmt.Sprintf("%s:%s", key, value)) + }) + return tags +} + +// mapIntMetrics maps int datapoints into Datadog metrics +func mapIntMetrics(name string, slice pdata.IntDataPointSlice) []datadog.Metric { + // Allocate assuming none are nil + metrics := make([]datadog.Metric, 0, slice.Len()) + for i := 0; i < slice.Len(); i++ { + p := slice.At(i) + if p.IsNil() { + continue + } + metrics = append(metrics, + newGauge(name, uint64(p.Timestamp()), float64(p.Value()), getTags(p.LabelsMap())), + ) + } + return metrics +} + +// mapDoubleMetrics maps double datapoints into Datadog metrics +func mapDoubleMetrics(name string, slice pdata.DoubleDataPointSlice) []datadog.Metric { + // Allocate assuming none are nil + metrics := make([]datadog.Metric, 0, slice.Len()) + for i := 0; i < slice.Len(); i++ { + p := slice.At(i) + if p.IsNil() { + continue + } + metrics = append(metrics, + newGauge(name, uint64(p.Timestamp()), float64(p.Value()), getTags(p.LabelsMap())), + ) + } + return metrics +} + +// mapIntHistogramMetrics maps histogram metrics slices to Datadog metrics +// +// A Histogram metric has: +// - The count of values in the population +// - The sum of values in the population +// - A number of buckets, each of them having +// - the bounds that define the bucket +// - the count of the number of items in that bucket +// - a sample value from each bucket +// +// We follow a similar approach to our OpenCensus exporter: +// we report sum and count by default; buckets count can also +// be reported (opt-in), but bounds are ignored. +func mapIntHistogramMetrics(name string, slice pdata.IntHistogramDataPointSlice, buckets bool) []datadog.Metric { + // Allocate assuming none are nil and no buckets + metrics := make([]datadog.Metric, 0, 2*slice.Len()) + for i := 0; i < slice.Len(); i++ { + p := slice.At(i) + if p.IsNil() { + continue + } + ts := uint64(p.Timestamp()) + tags := getTags(p.LabelsMap()) + + metrics = append(metrics, + newGauge(fmt.Sprintf("%s.count", name), ts, float64(p.Count()), tags), + newGauge(fmt.Sprintf("%s.sum", name), ts, float64(p.Sum()), tags), + ) + + if buckets { + // We have a single metric, 'count_per_bucket', which is tagged with the bucket id. See: + // https://github.com/DataDog/opencensus-go-exporter-datadog/blob/c3b47f1c6dcf1c47b59c32e8dbb7df5f78162daa/stats.go#L99-L104 + fullName := fmt.Sprintf("%s.count_per_bucket", name) + for idx, count := range p.BucketCounts() { + bucketTags := append(tags, fmt.Sprintf("bucket_idx:%d", idx)) + metrics = append(metrics, + newGauge(fullName, ts, float64(count), bucketTags), + ) + } + } + } + return metrics +} + +// mapIntHistogramMetrics maps double histogram metrics slices to Datadog metrics +// +// see mapIntHistogramMetrics docs for further details. +func mapDoubleHistogramMetrics(name string, slice pdata.DoubleHistogramDataPointSlice, buckets bool) []datadog.Metric { + // Allocate assuming none are nil and no buckets + metrics := make([]datadog.Metric, 0, 2*slice.Len()) + for i := 0; i < slice.Len(); i++ { + p := slice.At(i) + if p.IsNil() { + continue + } + ts := uint64(p.Timestamp()) + tags := getTags(p.LabelsMap()) + + metrics = append(metrics, + newGauge(fmt.Sprintf("%s.count", name), ts, float64(p.Count()), tags), + newGauge(fmt.Sprintf("%s.sum", name), ts, float64(p.Sum()), tags), + ) + + if buckets { + // We have a single metric, 'count_per_bucket', which is tagged with the bucket id. See: + // https://github.com/DataDog/opencensus-go-exporter-datadog/blob/c3b47f1c6dcf1c47b59c32e8dbb7df5f78162daa/stats.go#L99-L104 + fullName := fmt.Sprintf("%s.count_per_bucket", name) + for idx, count := range p.BucketCounts() { + bucketTags := append(tags, fmt.Sprintf("bucket_idx:%d", idx)) + metrics = append(metrics, + newGauge(fullName, ts, float64(count), bucketTags), + ) + } + } + } + return metrics +} + +// MapMetrics maps OTLP metrics into the DataDog format +func MapMetrics(logger *zap.Logger, cfg MetricsConfig, md pdata.Metrics) (series []datadog.Metric, droppedTimeSeries int) { + rms := md.ResourceMetrics() + for i := 0; i < rms.Len(); i++ { + rm := rms.At(i) + if rm.IsNil() { + continue + } + ilms := rm.InstrumentationLibraryMetrics() + for j := 0; j < ilms.Len(); j++ { + ilm := ilms.At(j) + if ilm.IsNil() { + continue + } + metrics := ilm.Metrics() + for k := 0; k < metrics.Len(); k++ { + md := metrics.At(k) + if md.IsNil() { + continue + } + var datapoints []datadog.Metric + switch md.DataType() { + case pdata.MetricDataTypeNone: + continue + case pdata.MetricDataTypeIntGauge: + datapoints = mapIntMetrics(md.Name(), md.IntGauge().DataPoints()) + case pdata.MetricDataTypeDoubleGauge: + datapoints = mapDoubleMetrics(md.Name(), md.DoubleGauge().DataPoints()) + case pdata.MetricDataTypeIntSum: + // Ignore aggregation temporality; report raw values + datapoints = mapIntMetrics(md.Name(), md.IntSum().DataPoints()) + case pdata.MetricDataTypeDoubleSum: + // Ignore aggregation temporality; report raw values + datapoints = mapDoubleMetrics(md.Name(), md.DoubleSum().DataPoints()) + case pdata.MetricDataTypeIntHistogram: + datapoints = mapIntHistogramMetrics(md.Name(), md.IntHistogram().DataPoints(), cfg.Buckets) + case pdata.MetricDataTypeDoubleHistogram: + datapoints = mapDoubleHistogramMetrics(md.Name(), md.DoubleHistogram().DataPoints(), cfg.Buckets) + } + series = append(series, datapoints...) + } + } + } + return +} diff --git a/exporter/datadogexporter/metrics_translator_test.go b/exporter/datadogexporter/metrics_translator_test.go new file mode 100644 index 000000000000..b93efb138102 --- /dev/null +++ b/exporter/datadogexporter/metrics_translator_test.go @@ -0,0 +1,162 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datadogexporter + +import ( + "math" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/consumer/pdata" + "gopkg.in/zorkian/go-datadog-api.v2" +) + +func TestMetricValue(t *testing.T) { + var ( + name string = "name" + value float64 = math.Pi + ts uint64 = uint64(time.Now().UnixNano()) + tags []string = []string{"tool:opentelemetry", "version:0.1.0"} + ) + + metric := newGauge(name, ts, value, tags) + assert.Equal(t, Gauge, metric.GetType()) + assert.Equal(t, tags, metric.Tags) +} + +func TestGetTags(t *testing.T) { + labels := pdata.NewStringMap() + labels.InitFromMap(map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "", + }) + + assert.ElementsMatch(t, + getTags(labels), + [...]string{"key1:val1", "key2:val2", "key3:n/a"}, + ) +} + +func TestMapIntMetrics(t *testing.T) { + ts := time.Now().UnixNano() + slice := pdata.NewIntDataPointSlice() + + point := pdata.NewIntDataPoint() + point.InitEmpty() + point.SetValue(17) + point.SetTimestamp(pdata.TimestampUnixNano(ts)) + slice.Append(point) + + nilPoint := pdata.NewIntDataPoint() + slice.Append(nilPoint) + + assert.ElementsMatch(t, + mapIntMetrics("int64.test", slice), + []datadog.Metric{newGauge("int64.test", uint64(ts), 17, []string{})}, + ) +} + +func TestMapDoubleMetrics(t *testing.T) { + ts := time.Now().UnixNano() + slice := pdata.NewDoubleDataPointSlice() + + point := pdata.NewDoubleDataPoint() + point.InitEmpty() + point.SetValue(math.Pi) + point.SetTimestamp(pdata.TimestampUnixNano(ts)) + slice.Append(point) + + nilPoint := pdata.NewDoubleDataPoint() + slice.Append(nilPoint) + + assert.ElementsMatch(t, + mapDoubleMetrics("float64.test", slice), + []datadog.Metric{newGauge("float64.test", uint64(ts), math.Pi, []string{})}, + ) +} + +func TestMapIntHistogramMetrics(t *testing.T) { + ts := time.Now().UnixNano() + slice := pdata.NewIntHistogramDataPointSlice() + + point := pdata.NewIntHistogramDataPoint() + point.InitEmpty() + point.SetCount(20) + point.SetSum(200) + point.SetBucketCounts([]uint64{2, 18}) + point.SetTimestamp(pdata.TimestampUnixNano(ts)) + slice.Append(point) + + nilPoint := pdata.NewIntHistogramDataPoint() + slice.Append(nilPoint) + + noBuckets := []datadog.Metric{ + newGauge("intHist.test.count", uint64(ts), 20, []string{}), + newGauge("intHist.test.sum", uint64(ts), 200, []string{}), + } + + buckets := []datadog.Metric{ + newGauge("intHist.test.count_per_bucket", uint64(ts), 2, []string{"bucket_idx:0"}), + newGauge("intHist.test.count_per_bucket", uint64(ts), 18, []string{"bucket_idx:1"}), + } + + assert.ElementsMatch(t, + mapIntHistogramMetrics("intHist.test", slice, false), // No buckets + noBuckets, + ) + + assert.ElementsMatch(t, + mapIntHistogramMetrics("intHist.test", slice, true), // buckets + append(noBuckets, buckets...), + ) +} + +func TestMapDoubleHistogramMetrics(t *testing.T) { + ts := time.Now().UnixNano() + slice := pdata.NewDoubleHistogramDataPointSlice() + + point := pdata.NewDoubleHistogramDataPoint() + point.InitEmpty() + point.SetCount(20) + point.SetSum(math.Pi) + point.SetBucketCounts([]uint64{2, 18}) + point.SetTimestamp(pdata.TimestampUnixNano(ts)) + slice.Append(point) + + nilPoint := pdata.NewDoubleHistogramDataPoint() + slice.Append(nilPoint) + + noBuckets := []datadog.Metric{ + newGauge("doubleHist.test.count", uint64(ts), 20, []string{}), + newGauge("doubleHist.test.sum", uint64(ts), math.Pi, []string{}), + } + + buckets := []datadog.Metric{ + newGauge("doubleHist.test.count_per_bucket", uint64(ts), 2, []string{"bucket_idx:0"}), + newGauge("doubleHist.test.count_per_bucket", uint64(ts), 18, []string{"bucket_idx:1"}), + } + + assert.ElementsMatch(t, + mapDoubleHistogramMetrics("doubleHist.test", slice, false), // No buckets + noBuckets, + ) + + assert.ElementsMatch(t, + mapDoubleHistogramMetrics("doubleHist.test", slice, true), // buckets + append(noBuckets, buckets...), + ) +}