github.com/google/cloudprober@v0.11.3/surfacers/datadog/datadog.go (about)

     1  // Copyright 2021 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  /*
    16  Package datadog implements a surfacer to export metrics to Datadog.
    17  */
    18  package datadog
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"os"
    24  	"regexp"
    25  	"time"
    26  
    27  	"github.com/google/cloudprober/logger"
    28  	"github.com/google/cloudprober/metrics"
    29  	"github.com/google/cloudprober/surfacers/common/options"
    30  	configpb "github.com/google/cloudprober/surfacers/datadog/proto"
    31  	"google.golang.org/protobuf/proto"
    32  )
    33  
    34  /*
    35  	The datadog surfacer presents EventMetrics to the datadog SubmitMetrics APIs,
    36  	using the config passed in.
    37  
    38  	Some EventMetrics are not supported here, as the datadog SubmitMetrics API only
    39  	supports float64 type values as the metric value.
    40  */
    41  
    42  // Datadog API limit for metrics included in a SubmitMetrics call
    43  const datadogMaxSeries int = 20
    44  
    45  var datadogKind = map[metrics.Kind]string{
    46  	metrics.GAUGE:      "gauge",
    47  	metrics.CUMULATIVE: "count",
    48  }
    49  
    50  // DDSurfacer implements a datadog surfacer for datadog metrics.
    51  type DDSurfacer struct {
    52  	c                 *configpb.SurfacerConf
    53  	writeChan         chan *metrics.EventMetrics
    54  	client            *ddClient
    55  	l                 *logger.Logger
    56  	ignoreLabelsRegex *regexp.Regexp
    57  	prefix            string
    58  	// A cache of []*ddSeries, used for batch writing to datadog
    59  	ddSeriesCache []ddSeries
    60  }
    61  
    62  func (dd *DDSurfacer) receiveMetricsFromEvent(ctx context.Context) {
    63  	for {
    64  		select {
    65  		case <-ctx.Done():
    66  			dd.l.Infof("Context canceled, stopping the surfacer write loop")
    67  			return
    68  		case em := <-dd.writeChan:
    69  			dd.recordEventMetrics(ctx, em)
    70  		}
    71  	}
    72  }
    73  
    74  func (dd *DDSurfacer) recordEventMetrics(ctx context.Context, em *metrics.EventMetrics) {
    75  	for _, metricKey := range em.MetricsKeys() {
    76  		switch value := em.Metric(metricKey).(type) {
    77  		case metrics.NumValue:
    78  			dd.publishMetrics(ctx, dd.newDDSeries(metricKey, value.Float64(), emLabelsToTags(em), em.Timestamp, em.Kind))
    79  		case *metrics.Map:
    80  			var series []ddSeries
    81  			for _, k := range value.Keys() {
    82  				tags := emLabelsToTags(em)
    83  				tags = append(tags, fmt.Sprintf("%s:%s", value.MapName, k))
    84  				series = append(series, dd.newDDSeries(metricKey, value.GetKey(k).Float64(), tags, em.Timestamp, em.Kind))
    85  			}
    86  			dd.publishMetrics(ctx, series...)
    87  		case *metrics.Distribution:
    88  			dd.publishMetrics(ctx, dd.distToDDSeries(value.Data(), metricKey, emLabelsToTags(em), em.Timestamp, em.Kind)...)
    89  		}
    90  	}
    91  }
    92  
    93  // publish the metrics to datadog, buffering as necessary
    94  func (dd *DDSurfacer) publishMetrics(ctx context.Context, series ...ddSeries) {
    95  	if len(dd.ddSeriesCache) >= datadogMaxSeries {
    96  		if err := dd.client.submitMetrics(ctx, dd.ddSeriesCache); err != nil {
    97  			dd.l.Errorf("Failed to publish %d series to datadog: %v", len(dd.ddSeriesCache), err)
    98  		}
    99  
   100  		dd.ddSeriesCache = dd.ddSeriesCache[:0]
   101  	}
   102  
   103  	dd.ddSeriesCache = append(dd.ddSeriesCache, series...)
   104  }
   105  
   106  // Create a new datadog series using the values passed in.
   107  func (dd *DDSurfacer) newDDSeries(metricName string, value float64, tags []string, timestamp time.Time, kind metrics.Kind) ddSeries {
   108  	return ddSeries{
   109  		Metric: dd.prefix + metricName,
   110  		Points: [][]float64{[]float64{float64(timestamp.Unix()), value}},
   111  		Tags:   &tags,
   112  		Type:   proto.String(datadogKind[kind]),
   113  	}
   114  }
   115  
   116  // Take metric labels from an event metric and parse them into a Datadog Dimension struct.
   117  func emLabelsToTags(em *metrics.EventMetrics) []string {
   118  	tags := []string{}
   119  
   120  	for _, k := range em.LabelsKeys() {
   121  		tags = append(tags, fmt.Sprintf("%s:%s", k, em.Label(k)))
   122  	}
   123  
   124  	return tags
   125  }
   126  
   127  func (dd *DDSurfacer) distToDDSeries(d *metrics.DistributionData, metricName string, tags []string, t time.Time, kind metrics.Kind) []ddSeries {
   128  	ret := []ddSeries{
   129  		ddSeries{
   130  			Metric: dd.prefix + metricName + ".sum",
   131  			Points: [][]float64{[]float64{float64(t.Unix()), d.Sum}},
   132  			Tags:   &tags,
   133  			Type:   proto.String(datadogKind[kind]),
   134  		}, {
   135  			Metric: dd.prefix + metricName + ".count",
   136  			Points: [][]float64{[]float64{float64(t.Unix()), float64(d.Count)}},
   137  			Tags:   &tags,
   138  			Type:   proto.String(datadogKind[kind]),
   139  		},
   140  	}
   141  
   142  	// Add one point at the value of the Lower Bound per count in the bucket. Each point represents the
   143  	// minimum poissible value that it could have been.
   144  	var points [][]float64
   145  	for i := range d.LowerBounds {
   146  		for n := 0; n < int(d.BucketCounts[i]); n++ {
   147  			points = append(points, []float64{float64(t.Unix()), d.LowerBounds[i]})
   148  		}
   149  	}
   150  
   151  	ret = append(ret, ddSeries{Metric: dd.prefix + metricName, Points: points, Tags: &tags, Type: proto.String(datadogKind[kind])})
   152  	return ret
   153  }
   154  
   155  // New creates a new instance of a datadog surfacer, based on the config passed in. It then hands off
   156  // to a goroutine to surface metrics to datadog across a buffered channel.
   157  func New(ctx context.Context, config *configpb.SurfacerConf, opts *options.Options, l *logger.Logger) (*DDSurfacer, error) {
   158  	if config.GetApiKey() != "" {
   159  		os.Setenv("DD_API_KEY", config.GetApiKey())
   160  	}
   161  	if config.GetAppKey() != "" {
   162  		os.Setenv("DD_APP_KEY", config.GetAppKey())
   163  	}
   164  
   165  	p := config.GetPrefix()
   166  	if p[len(p)-1] != '.' {
   167  		p += "."
   168  	}
   169  
   170  	dd := &DDSurfacer{
   171  		c:         config,
   172  		writeChan: make(chan *metrics.EventMetrics, opts.MetricsBufferSize),
   173  		client:    newClient(config.GetServer(), config.GetApiKey(), config.GetAppKey()),
   174  		l:         l,
   175  		prefix:    p,
   176  	}
   177  
   178  	// Set the capacity of this slice to the max metric value, to avoid having to grow the slice.
   179  	dd.ddSeriesCache = make([]ddSeries, datadogMaxSeries)
   180  
   181  	go dd.receiveMetricsFromEvent(ctx)
   182  
   183  	dd.l.Info("Initialised Datadog surfacer")
   184  	return dd, nil
   185  }
   186  
   187  // Write is a function defined to comply with the surfacer interface, and enables the
   188  // datadog surfacer to receive EventMetrics over the buffered channel.
   189  func (dd *DDSurfacer) Write(ctx context.Context, em *metrics.EventMetrics) {
   190  	select {
   191  	case dd.writeChan <- em:
   192  	default:
   193  		dd.l.Error("Surfacer's write channel is full, dropping new data.")
   194  	}
   195  }