diff --git a/surfacers/datadog/client.go b/surfacers/datadog/client.go new file mode 100644 index 00000000..39af2131 --- /dev/null +++ b/surfacers/datadog/client.go @@ -0,0 +1,121 @@ +// Copyright 2021 The Cloudprober 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 datadog + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" +) + +const defaultServer = "api.datadoghq.com" + +type ddClient struct { + apiKey string + appKey string + server string + c http.Client +} + +// ddSeries A metric to submit to Datadog. See: +// https://docs.datadoghq.com/developers/metrics/#custom-metrics-properties +type ddSeries struct { + // The name of the host that produced the metric. + Host *string `json:"host,omitempty"` + // The name of the timeseries. + Metric string `json:"metric"` + // Points relating to a metric. All points must be tuples with timestamp and + // a scalar value (cannot be a string). Timestamps should be in POSIX time in + // seconds, and cannot be more than ten minutes in the future or more than + // one hour in the past. + Points [][]float64 `json:"points"` + // A list of tags associated with the metric. + Tags *[]string `json:"tags,omitempty"` + // The type of the metric either `count`, `gauge`, or `rate`. + Type *string `json:"type,omitempty"` +} + +func newClient(server, apiKey, appKey string) *ddClient { + c := &ddClient{ + apiKey: apiKey, + appKey: appKey, + server: server, + c: http.Client{}, + } + if c.apiKey == "" { + c.apiKey = os.Getenv("DD_API_KEY") + } + + if c.appKey == "" { + c.appKey = os.Getenv("DD_APP_KEY") + } + + if c.server == "" { + c.server = defaultServer + } + + return c +} + +func (c *ddClient) newRequest(series []ddSeries) (*http.Request, error) { + url := fmt.Sprintf("https://%s/api/v1/series", c.server) + + // JSON encoding of the datadog series. + // { + // "series": [{..},{..}] + // } + b, err := json.Marshal(map[string][]ddSeries{"series": series}) + if err != nil { + return nil, err + } + + body := &bytes.Buffer{} + if _, err := body.Write(b); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + + req.Header.Set("DD-API-KEY", c.apiKey) + req.Header.Set("DD-APP-KEY", c.appKey) + + return req, nil +} + +func (c *ddClient) submitMetrics(ctx context.Context, series []ddSeries) error { + req, err := c.newRequest(series) + if err != nil { + return nil + } + + resp, err := c.c.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + if resp.StatusCode >= 300 { + b, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("error, HTTP status: %d, full response: %s", resp.StatusCode, string(b)) + } + + return nil +} diff --git a/surfacers/datadog/client_test.go b/surfacers/datadog/client_test.go new file mode 100644 index 00000000..9b30db0f --- /dev/null +++ b/surfacers/datadog/client_test.go @@ -0,0 +1,133 @@ +// Copyright 2021 The Cloudprober 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 datadog + +import ( + "encoding/json" + "io" + "os" + "reflect" + "testing" + "time" +) + +func TestNewClient(t *testing.T) { + cAPIKey, cAppKey := "c-apiKey", "c-appKey" + eAPIKey, eAppKey := "e-apiKey", "e-appKey" + + tests := []struct { + desc string + apiKey string + appKey string + server string + env map[string]string + wantClient *ddClient + }{ + { + desc: "keys-from-config", + apiKey: cAPIKey, + appKey: cAppKey, + server: "", + wantClient: &ddClient{ + apiKey: cAPIKey, + appKey: cAppKey, + server: defaultServer, + }, + }, + { + desc: "keys-from-env", + env: map[string]string{ + "DD_API_KEY": eAPIKey, + "DD_APP_KEY": eAppKey, + }, + server: "test-server", + wantClient: &ddClient{ + apiKey: eAPIKey, + appKey: eAppKey, + server: "test-server", + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + for k, v := range test.env { + os.Setenv(k, v) + } + + c := newClient(test.server, test.apiKey, test.appKey) + if !reflect.DeepEqual(c, test.wantClient) { + t.Errorf("got client: %v, want client: %v", c, test.wantClient) + } + }) + } +} + +func TestNewRequest(t *testing.T) { + ts := time.Now().Unix() + tags := []string{"probe:cloudprober_http"} + metricType := "count" + + testSeries := []ddSeries{ + { + Metric: "cloudprober.success", + Points: [][]float64{[]float64{float64(ts), 99}}, + Tags: &tags, + Type: &metricType, + }, + { + Metric: "cloudprober.total", + Points: [][]float64{[]float64{float64(ts), 100}}, + Tags: &tags, + Type: &metricType, + }, + } + + testClient := newClient("", "test-api-key", "test-app-key") + req, err := testClient.newRequest(testSeries) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Check URL + wantURL := "https://api.datadoghq.com/api/v1/series" + if req.URL.String() != wantURL { + t.Errorf("Got URL: %s, wanted: %s", req.URL.String(), wantURL) + } + + // Check request headers + for k, v := range map[string]string{ + "DD-API-KEY": "test-api-key", + "DD-APP-KEY": "test-app-key", + } { + if req.Header.Get(k) != v { + t.Errorf("%s header: %s, wanted: %s", k, req.Header.Get(k), v) + } + } + + // Check request body + b, err := io.ReadAll(req.Body) + if err != nil { + t.Errorf("Error reading request body: %v", err) + } + data := map[string][]ddSeries{} + if err := json.Unmarshal(b, &data); err != nil { + t.Errorf("Error unmarshaling request body: %v", err) + } + if !reflect.DeepEqual(data["series"], testSeries) { + t.Errorf("s.Series: %v, testSeries: %v", data["series"], testSeries) + } +} diff --git a/surfacers/datadog/datadog.go b/surfacers/datadog/datadog.go index 2331bab6..90f3f622 100644 --- a/surfacers/datadog/datadog.go +++ b/surfacers/datadog/datadog.go @@ -24,8 +24,6 @@ import ( "regexp" "time" - "github.com/DataDog/datadog-api-client-go/api/v1/datadog" - "github.com/google/cloudprober/logger" "github.com/google/cloudprober/metrics" "github.com/google/cloudprober/surfacers/common/options" @@ -53,12 +51,12 @@ var datadogKind = map[metrics.Kind]string{ type DDSurfacer struct { c *configpb.SurfacerConf writeChan chan *metrics.EventMetrics - client *datadog.APIClient + client *ddClient l *logger.Logger ignoreLabelsRegex *regexp.Regexp prefix string - // A cache of []*datadog.Series, used for batch writing to datadog - ddSeriesCache []datadog.Series + // A cache of []*ddSeries, used for batch writing to datadog + ddSeriesCache []ddSeries } func (dd *DDSurfacer) receiveMetricsFromEvent(ctx context.Context) { @@ -79,7 +77,7 @@ func (dd *DDSurfacer) recordEventMetrics(ctx context.Context, em *metrics.EventM case metrics.NumValue: dd.publishMetrics(ctx, dd.newDDSeries(metricKey, value.Float64(), emLabelsToTags(em), em.Timestamp, em.Kind)) case *metrics.Map: - var series []datadog.Series + var series []ddSeries for _, k := range value.Keys() { tags := emLabelsToTags(em) tags = append(tags, fmt.Sprintf("%s:%s", value.MapName, k)) @@ -93,13 +91,10 @@ func (dd *DDSurfacer) recordEventMetrics(ctx context.Context, em *metrics.EventM } // publish the metrics to datadog, buffering as necessary -func (dd *DDSurfacer) publishMetrics(ctx context.Context, series ...datadog.Series) { +func (dd *DDSurfacer) publishMetrics(ctx context.Context, series ...ddSeries) { if len(dd.ddSeriesCache) >= datadogMaxSeries { - body := *datadog.NewMetricsPayload(dd.ddSeriesCache) - _, r, err := dd.client.MetricsApi.SubmitMetrics(ctx, body) - - if err != nil { - dd.l.Errorf("Failed to publish %d series to datadog: %v. Full response: %v", len(dd.ddSeriesCache), err, r) + if err := dd.client.submitMetrics(ctx, dd.ddSeriesCache); err != nil { + dd.l.Errorf("Failed to publish %d series to datadog: %v", len(dd.ddSeriesCache), err) } dd.ddSeriesCache = dd.ddSeriesCache[:0] @@ -109,8 +104,8 @@ func (dd *DDSurfacer) publishMetrics(ctx context.Context, series ...datadog.Seri } // Create a new datadog series using the values passed in. -func (dd *DDSurfacer) newDDSeries(metricName string, value float64, tags []string, timestamp time.Time, kind metrics.Kind) datadog.Series { - return datadog.Series{ +func (dd *DDSurfacer) newDDSeries(metricName string, value float64, tags []string, timestamp time.Time, kind metrics.Kind) ddSeries { + return ddSeries{ Metric: dd.prefix + metricName, Points: [][]float64{[]float64{float64(timestamp.Unix()), value}}, Tags: &tags, @@ -129,9 +124,9 @@ func emLabelsToTags(em *metrics.EventMetrics) []string { return tags } -func (dd *DDSurfacer) distToDDSeries(d *metrics.DistributionData, metricName string, tags []string, t time.Time, kind metrics.Kind) []datadog.Series { - ret := []datadog.Series{ - datadog.Series{ +func (dd *DDSurfacer) distToDDSeries(d *metrics.DistributionData, metricName string, tags []string, t time.Time, kind metrics.Kind) []ddSeries { + ret := []ddSeries{ + ddSeries{ Metric: dd.prefix + metricName + ".sum", Points: [][]float64{[]float64{float64(t.Unix()), d.Sum}}, Tags: &tags, @@ -153,7 +148,7 @@ func (dd *DDSurfacer) distToDDSeries(d *metrics.DistributionData, metricName str } } - ret = append(ret, datadog.Series{Metric: dd.prefix + metricName, Points: points, Tags: &tags, Type: proto.String(datadogKind[kind])}) + ret = append(ret, ddSeries{Metric: dd.prefix + metricName, Points: points, Tags: &tags, Type: proto.String(datadogKind[kind])}) return ret } @@ -167,11 +162,6 @@ func New(ctx context.Context, config *configpb.SurfacerConf, opts *options.Optio os.Setenv("DD_APP_KEY", config.GetAppKey()) } - ctx = datadog.NewDefaultContext(ctx) - configuration := datadog.NewConfiguration() - - client := datadog.NewAPIClient(configuration) - p := config.GetPrefix() if p[len(p)-1] != '.' { p += "." @@ -180,13 +170,13 @@ func New(ctx context.Context, config *configpb.SurfacerConf, opts *options.Optio dd := &DDSurfacer{ c: config, writeChan: make(chan *metrics.EventMetrics, opts.MetricsBufferSize), - client: client, + client: newClient(config.GetServer(), config.GetApiKey(), config.GetAppKey()), l: l, prefix: p, } // Set the capacity of this slice to the max metric value, to avoid having to grow the slice. - dd.ddSeriesCache = make([]datadog.Series, datadogMaxSeries) + dd.ddSeriesCache = make([]ddSeries, datadogMaxSeries) go dd.receiveMetricsFromEvent(ctx) diff --git a/surfacers/datadog/proto/config.pb.go b/surfacers/datadog/proto/config.pb.go index ac74859c..49e59098 100644 --- a/surfacers/datadog/proto/config.pb.go +++ b/surfacers/datadog/proto/config.pb.go @@ -32,6 +32,8 @@ type SurfacerConf struct { ApiKey *string `protobuf:"bytes,2,opt,name=api_key,json=apiKey" json:"api_key,omitempty"` // Datadog APP key. If not set, DD_APP_KEY env variable is used. AppKey *string `protobuf:"bytes,3,opt,name=app_key,json=appKey" json:"app_key,omitempty"` + // Datadog server, default: "api.datadoghq.com" + Server *string `protobuf:"bytes,4,opt,name=server" json:"server,omitempty"` } // Default values for SurfacerConf fields. @@ -92,6 +94,13 @@ func (x *SurfacerConf) GetAppKey() string { return "" } +func (x *SurfacerConf) GetServer() string { + if x != nil && x.Server != nil { + return *x.Server + } + return "" +} + var File_github_com_google_cloudprober_surfacers_datadog_proto_config_proto protoreflect.FileDescriptor var file_github_com_google_cloudprober_surfacers_datadog_proto_config_proto_rawDesc = []byte{ @@ -101,17 +110,18 @@ var file_github_com_google_cloudprober_surfacers_datadog_proto_config_proto_rawD 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x72, 0x2e, 0x73, 0x75, 0x72, 0x66, 0x61, 0x63, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x64, - 0x6f, 0x67, 0x22, 0x65, 0x0a, 0x0c, 0x53, 0x75, 0x72, 0x66, 0x61, 0x63, 0x65, 0x72, 0x43, 0x6f, + 0x6f, 0x67, 0x22, 0x7d, 0x0a, 0x0c, 0x53, 0x75, 0x72, 0x66, 0x61, 0x63, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x12, 0x23, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x3a, 0x0b, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x72, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x61, 0x70, 0x70, 0x4b, 0x65, 0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x63, - 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x72, 0x2f, 0x73, 0x75, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x72, 0x73, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x64, 0x6f, 0x67, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, + 0x09, 0x52, 0x06, 0x61, 0x70, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x62, + 0x65, 0x72, 0x2f, 0x73, 0x75, 0x72, 0x66, 0x61, 0x63, 0x65, 0x72, 0x73, 0x2f, 0x64, 0x61, 0x74, + 0x61, 0x64, 0x6f, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, } var ( diff --git a/surfacers/datadog/proto/config.proto b/surfacers/datadog/proto/config.proto index 22cb3aa0..df6442ae 100644 --- a/surfacers/datadog/proto/config.proto +++ b/surfacers/datadog/proto/config.proto @@ -14,4 +14,7 @@ message SurfacerConf { // Datadog APP key. If not set, DD_APP_KEY env variable is used. optional string app_key = 3; + + // Datadog server, default: "api.datadoghq.com" + optional string server = 4; }