Skip to content
This repository has been archived by the owner on Nov 5, 2021. It is now read-only.

Commit

Permalink
Use a simple HTTP client instead of datadog api client library. (#631)
Browse files Browse the repository at this point in the history
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: #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
  • Loading branch information
manugarg committed Jul 20, 2021
1 parent fdfc83c commit 748f882
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 31 deletions.
121 changes: 121 additions & 0 deletions surfacers/datadog/client.go
Original file line number Diff line number Diff line change
@@ -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
}
133 changes: 133 additions & 0 deletions surfacers/datadog/client_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
40 changes: 15 additions & 25 deletions surfacers/datadog/datadog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand All @@ -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))
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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 += "."
Expand All @@ -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)

Expand Down
Loading

0 comments on commit 748f882

Please sign in to comment.