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 }