From f210c2d5f90856d5e9f3715d7d8829ed47b824aa Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Mon, 19 Jul 2021 18:15:46 -0700 Subject: [PATCH] Use a simple HTTP client instead of datadog api client library. This change brings binary size back to 52M (from 61M). We need only 1% of the datadog API client's functionality and using it increases binary size by 17% to 22% (Ref: https://github.com/google/cloudprober/issues/628). I believe this is happening because in datadog API client library, basic symbols like APIClient or Configuration refer to a lot of other symbols[1]. This means that even after compilation and linking, almost all the symbols become part of the generated binary. I've verified that surfacer continues to work after this change. [1] - https://github.com/DataDog/datadog-api-client-go/blob/5c76e2376ad3d1ceba0eb1199bf9447808e88948/api/v1/datadog/client.go#L43 PiperOrigin-RevId: 385683900 --- surfacers/datadog/client.go | 121 ++++++++++++++++++++++++ surfacers/datadog/client_test.go | 133 +++++++++++++++++++++++++++ surfacers/datadog/datadog.go | 40 +++----- surfacers/datadog/proto/config.proto | 3 + 4 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 surfacers/datadog/client.go create mode 100644 surfacers/datadog/client_test.go 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 fe8012b5..90f3f622 100644 --- a/surfacers/datadog/datadog.go +++ b/surfacers/datadog/datadog.go @@ -24,8 +24,6 @@ import ( "regexp" "time" - "google3/third_party/golang/datadog_api_client/api/v1/datadog/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.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; }