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.
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 5c90c23
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

0 comments on commit 5c90c23

Please sign in to comment.