Skip to content

Commit

Permalink
feat: Add datadog metric provider
Browse files Browse the repository at this point in the history
  • Loading branch information
stephen-harris committed Sep 7, 2020
1 parent d1fd695 commit 815d4c2
Show file tree
Hide file tree
Showing 15 changed files with 644 additions and 2 deletions.
38 changes: 38 additions & 0 deletions docs/features/analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,3 +873,41 @@ For success conditions that need to evaluate a numeric return value the `asInt`
value: "Bearer {{ args.api-token }}"
jsonPath: "{$.results.successPercent}"
```


## Datadog Metrics

A [Datadog](https://www.datadoghq.com/) query can be used to obtain measurements for analysis.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: loq-error-rate
spec:
args:
- name: service-name
- name: datadog-api-key
valueFrom:
secretKeyRef:
name: datadog
key: apikey
- name: datadog-app-key
valueFrom:
secretKeyRef:
name: datadog
key: appkey
metrics:
- name: error-rate
interval: 5m
successCondition: result <= 0.01
failureLimit: 3
provider:
datadog:
URL: https://api.datadoghq.com
apikey: {{args.datadog-api-key}}
appkey: {{args.datadog-app-key}}
query: |
sum:requests.error.count{service:{{args.service-name}}} /
sum:requests.request.count{service:{{args.service-name}}}
```
16 changes: 16 additions & 0 deletions manifests/crds/analysis-run-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down
16 changes: 16 additions & 0 deletions manifests/crds/analysis-template-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down
16 changes: 16 additions & 0 deletions manifests/crds/cluster-analysis-template-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down
48 changes: 48 additions & 0 deletions manifests/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down Expand Up @@ -2885,6 +2901,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down Expand Up @@ -5617,6 +5649,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down
48 changes: 48 additions & 0 deletions manifests/namespace-install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down Expand Up @@ -2885,6 +2901,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down Expand Up @@ -5617,6 +5649,22 @@ spec:
type: string
provider:
properties:
datadog:
properties:
apikey:
type: string
appkey:
type: string
query:
type: string
url:
type: string
required:
- apikey
- appkey
- query
- url
type: object
job:
properties:
metadata:
Expand Down
147 changes: 147 additions & 0 deletions metricproviders/datadog/datadog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package datadog

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

metricutil "github.com/argoproj/argo-rollouts/utils/metric"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/jsonpath"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/argoproj/argo-rollouts/utils/evaluate"
)

var unixNow = func() int64 { return time.Now().Unix() }

const (
//ProviderType indicates the provider is datadog
ProviderType = "Datadog"
)

// Provider contains all the required components to run a Datadog query
// Implements the Provider Interface
type Provider struct {
logCtx log.Entry
}

// Type incidates provider is a Datadog provider
func (p *Provider) Type() string {
return ProviderType
}

func (p *Provider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1alpha1.Measurement {
startTime := metav1.Now()

// Measurement to pass back
measurement := v1alpha1.Measurement{
StartedAt: &startTime,
}

endpoint := "https://api.datadoghq.com/api/v1/query"
if metric.Provider.Datadog.URL != "" {
endpoint = metric.Provider.Datadog.URL + "/api/v1/query"
}

url, _ := url.Parse(endpoint)

now := unixNow()
q := url.Query()
q.Set("query", metric.Provider.Datadog.Query)
q.Set("from", strconv.FormatInt(now-300, 10))
q.Set("to", strconv.FormatInt(now, 10))
url.RawQuery = q.Encode()

request := &http.Request{Method: "GET"}
request.URL = url
request.Header = make(http.Header)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("DD-API-KEY", metric.Provider.Datadog.APIKey)
request.Header.Set("DD-APPLICATION-KEY", metric.Provider.Datadog.APPKey)

// Send Request
httpClient := &http.Client{
Timeout: time.Duration(10) * time.Second,
}
response, err := httpClient.Do(request)

if err != nil {
return metricutil.MarkMeasurementError(measurement, err)
}

value, status, err := p.parseResponse(metric, response)
if err != nil {
return metricutil.MarkMeasurementError(measurement, err)
}

measurement.Value = value
measurement.Phase = status
finishedTime := metav1.Now()
measurement.FinishedAt = &finishedTime

return measurement
}

func (p *Provider) parseResponse(metric v1alpha1.Metric, response *http.Response) (string, v1alpha1.AnalysisPhase, error) {
var data interface{}

jsonParser := jsonpath.New("metrics")
jsonParser.Parse("{.series[0].pointlist[-1:][1]}")

bodyBytes, err := ioutil.ReadAll(response.Body)

if err != nil {
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Received no bytes in response: %v", err)
}

if response.StatusCode == 401 || response.StatusCode == 403 {
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("received authentication error response code: %v %s", response.StatusCode, string(bodyBytes))
} else if response.StatusCode < 200 || response.StatusCode >= 300 {
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("received non 2xx response code: %v %s", response.StatusCode, string(bodyBytes))
}

err = json.Unmarshal(bodyBytes, &data)
if err != nil {
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not parse JSON body: %v", err)
}

buf := new(bytes.Buffer)
err = jsonParser.Execute(buf, data)
if err != nil {
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not find JSONPath in body: %s", err)
}
out := buf.String()

status := evaluate.EvaluateResult(out, metric, p.logCtx)
return out, status, nil
}

// Resume should not be used the Datadog provider since all the work should occur in the Run method
func (p *Provider) Resume(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement {
p.logCtx.Warn("Datadog provider should not execute the Resume method")
return measurement
}

// Terminate should not be used the Datadog provider since all the work should occur in the Run method
func (p *Provider) Terminate(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement {
p.logCtx.Warn("Datadog provider should not execute the Terminate method")
return measurement
}

// GarbageCollect is a no-op for the Datadog provider
func (p *Provider) GarbageCollect(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, limit int) error {
return nil
}

func NewDatadogProvider(logCtx log.Entry) *Provider {
return &Provider{
logCtx: logCtx,
}
}
Loading

0 comments on commit 815d4c2

Please sign in to comment.