Skip to content

Commit

Permalink
feat: datadog metric provider for KLT (#948)
Browse files Browse the repository at this point in the history
  • Loading branch information
sudiptob2 committed Mar 20, 2023
1 parent 4e69699 commit 597a23f
Show file tree
Hide file tree
Showing 9 changed files with 601 additions and 0 deletions.
1 change: 1 addition & 0 deletions metrics-operator/controllers/common/providers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ package providers
const DynatraceProviderName = "dynatrace"
const DynatraceDQLProviderName = "dql"
const PrometheusProviderName = "prometheus"
const DataDogProviderName = "datadog"
44 changes: 44 additions & 0 deletions metrics-operator/controllers/common/providers/datadog/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package datadog

import (
"context"
"errors"
"fmt"
"strings"

metricsapi "github.com/keptn/lifecycle-toolkit/metrics-operator/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const apiKey, appKey = "DD_CLIENT_API_KEY", "DD_CLIENT_APP_KEY"

var ErrSecretKeyRefNotDefined = errors.New("the SecretKeyRef property with the DataDog API Key is missing")

func hasDDSecretDefined(spec metricsapi.KeptnMetricsProviderSpec) bool {
if spec.SecretKeyRef == (corev1.SecretKeySelector{}) {
return false
}
if strings.TrimSpace(spec.SecretKeyRef.Name) == "" {
return false
}
return true
}

func getDDSecret(ctx context.Context, provider metricsapi.KeptnMetricsProvider, k8sClient client.Client) (string, string, error) {
if !hasDDSecretDefined(provider.Spec) {
return "", "", ErrSecretKeyRefNotDefined
}
ddCredsSecret := &corev1.Secret{}
if err := k8sClient.Get(ctx, types.NamespacedName{Name: provider.Spec.SecretKeyRef.Name, Namespace: provider.Namespace}, ddCredsSecret); err != nil {
return "", "", err
}

apiKeyVal := ddCredsSecret.Data[apiKey]
appKeyVal := ddCredsSecret.Data[appKey]
if len(apiKeyVal) == 0 || len(appKeyVal) == 0 {
return "", "", fmt.Errorf("secret does not contain %s or %s", apiKey, appKey)
}
return string(apiKeyVal), string(appKeyVal), nil
}
120 changes: 120 additions & 0 deletions metrics-operator/controllers/common/providers/datadog/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package datadog

import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"

metricsapi "github.com/keptn/lifecycle-toolkit/metrics-operator/api/v1alpha2"
"github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/fake"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestGetSecret_NoKeyDefined(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(ddPayload))
require.Nil(t, err)
}))
defer svr.Close()
fakeClient := fake.NewClient()

p := metricsapi.KeptnMetricsProvider{
Spec: metricsapi.KeptnMetricsProviderSpec{
TargetServer: svr.URL,
},
}
r1, r2, e := getDDSecret(context.TODO(), p, fakeClient)
require.NotNil(t, e)
require.ErrorIs(t, e, ErrSecretKeyRefNotDefined)
require.Empty(t, r1)
require.Empty(t, r2)

}

func TestGetSecret_NoSecretDefined(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(ddPayload))
require.Nil(t, err)
}))
defer svr.Close()

secretName := "datadogSecret"
apiKey, apiKeyValue := "DD_CLIENT_API_KEY", "fake-api-key"
appKey, appKeyValue := "DD_CLIENT_APP_KEY", "fake-app-key"
apiToken := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "garbage",
Namespace: "",
},
Data: map[string][]byte{
apiKey: []byte(apiKeyValue),
appKey: []byte(appKeyValue),
},
}
fakeClient := fake.NewClient(apiToken)

b := true
p := metricsapi.KeptnMetricsProvider{
Spec: metricsapi.KeptnMetricsProviderSpec{
SecretKeyRef: v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: secretName,
},
Optional: &b,
},
TargetServer: svr.URL,
},
}
r1, r2, e := getDDSecret(context.TODO(), p, fakeClient)
require.NotNil(t, e)
require.True(t, strings.Contains(e.Error(), "secrets \""+secretName+"\" not found"))
require.Empty(t, r1)
require.Empty(t, r2)

}

func TestGetSecret_HappyPath(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(ddPayload))
require.Nil(t, err)
}))
defer svr.Close()

secretName := "datadogSecret"
apiKey, apiKeyValue := "DD_CLIENT_API_KEY", "fake-api-key"
appKey, appKeyValue := "DD_CLIENT_APP_KEY", "fake-app-key"
apiToken := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: "",
},
Data: map[string][]byte{
apiKey: []byte(apiKeyValue),
appKey: []byte(appKeyValue),
},
}
fakeClient := fake.NewClient(apiToken)

b := true
p := metricsapi.KeptnMetricsProvider{
Spec: metricsapi.KeptnMetricsProviderSpec{
SecretKeyRef: v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: secretName,
},
Optional: &b,
},
TargetServer: svr.URL,
},
}
r1, r2, e := getDDSecret(context.TODO(), p, fakeClient)
require.Nil(t, e)
require.Equal(t, apiKeyValue, r1)
require.Equal(t, appKeyValue, r2)

}
101 changes: 101 additions & 0 deletions metrics-operator/controllers/common/providers/datadog/datadog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package datadog

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/go-logr/logr"
metricsapi "github.com/keptn/lifecycle-toolkit/metrics-operator/api/v1alpha2"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type KeptnDataDogProvider struct {
Log logr.Logger
HttpClient http.Client
K8sClient client.Client
}

// EvaluateQuery fetches the SLI values from datadog provider
func (d *KeptnDataDogProvider) EvaluateQuery(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) (string, []byte, error) {
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()

// Assumed default metric duration as 5 minutes
// Think a better way to handle this
intervalInMin := 5
fromTime := time.Now().Add(time.Duration(-intervalInMin) * time.Minute).Unix()
toTime := time.Now().Unix()
qURL := provider.Spec.TargetServer + "/api/v1/query?from=" + strconv.Itoa(int(fromTime)) + "&to=" + strconv.Itoa(int(toTime)) + "&query=" + url.QueryEscape(metric.Spec.Query)
req, err := http.NewRequestWithContext(ctx, "GET", qURL, nil)
if err != nil {
d.Log.Error(err, "Error while creating request")
return "", nil, err
}

apiKeyVal, appKeyVal, err := getDDSecret(ctx, provider, d.K8sClient)
if err != nil {
return "", nil, err
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Dd-Api-Key", apiKeyVal)
req.Header.Set("Dd-Application-Key", appKeyVal)

res, err := d.HttpClient.Do(req)
if err != nil {
d.Log.Error(err, "Error while creating request")
return "", nil, err
}
defer func() {
err := res.Body.Close()
if err != nil {
d.Log.Error(err, "Could not close request body")
}
}()

b, _ := io.ReadAll(res.Body)
result := datadogV1.MetricsQueryResponse{}
err = json.Unmarshal(b, &result)
if err != nil {
d.Log.Error(err, "Error while parsing response")
return "", nil, err
}

if len(result.Series) == 0 {
d.Log.Info("No values in query result")
return "", nil, fmt.Errorf("no values in query result")
}

points := (result.Series)[0].Pointlist
if len(points) == 0 {
d.Log.Info("No metric points in query result")
return "", nil, fmt.Errorf("no metric points in query result")
}

r := d.getSingleValue(points)
value := strconv.FormatFloat(r, 'g', 5, 64)
return value, b, nil
}

func (d *KeptnDataDogProvider) getSingleValue(points [][]*float64) float64 {
var sum float64 = 0
var count uint64 = 0
for _, point := range points {
if point[1] != nil {
sum += *point[1]
count++
}
}
if count < 1 {
// cannot dive by zero
return 0
}
return sum / float64(count)
}
Loading

0 comments on commit 597a23f

Please sign in to comment.