Skip to content

Commit

Permalink
[datadogexporter] Implement translation of metrics to Datadog format (#…
Browse files Browse the repository at this point in the history
…1178)

* Add metrics translation functions

* Improve docs and make newGauge private

* Add basic hostname resolution
This will be expanded in the future to more closely replicate the
Datadog Agent behavior

* Remove Series struct and use []datadog.Metric
We can add it back if necessary in the future
  • Loading branch information
mx-psi authored Oct 5, 2020
1 parent 28d48f1 commit 727b1b6
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 0 deletions.
2 changes: 2 additions & 0 deletions exporter/datadogexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions exporter/datadogexporter/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
32 changes: 32 additions & 0 deletions exporter/datadogexporter/host.go
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions exporter/datadogexporter/host_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
214 changes: 214 additions & 0 deletions exporter/datadogexporter/metrics_translator.go
Original file line number Diff line number Diff line change
@@ -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{&timestamp, &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
}
Loading

0 comments on commit 727b1b6

Please sign in to comment.