Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Graphite metrics provider #1406

Merged
merged 13 commits into from
Sep 20, 2021
Merged
35 changes: 35 additions & 0 deletions docs/analysis/graphite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Graphite Metrics

A [Graphite](https://graphiteapp.org/) query can be used to obtain measurements for analysis.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 5m
# Note that the Argo Rollouts Graphite metrics provider returns results as float64s with 6 decimal places.
successCondition: result >= 90.000000
failureLimit: 3
provider:
graphite:
address: http://graphite.example.com:9090
query: |
target=summarize(
asPercent(
sumSeries(
stats.timers.httpServerRequests.app.{{args.service-name}}.exception.*.method.*.outcome.{CLIENT_ERROR,INFORMATIONAL,REDIRECTION,SUCCESS}.status.*.uri.*.count
),
sumSeries(
stats.timers.httpServerRequests.app.{{args.service-name}}.exception.*.method.*.outcome.*.status.*.uri.*.count
)
),
'5min',
'avg'
)
```
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ For these reasons, in large scale high-volume production environments, a rolling
* Customizable metric queries and analysis of business KPIs
* Ingress controller integration: NGINX, ALB
* Service Mesh integration: Istio, Linkerd, SMI
* Metric provider integration: Prometheus, Wavefront, Kayenta, Web, Kubernetes Jobs, Datadog, New Relic
* Metric provider integration: Prometheus, Wavefront, Kayenta, Web, Kubernetes Jobs, Datadog, New Relic, Graphite

## Quick Start

Expand Down
156 changes: 156 additions & 0 deletions metricproviders/graphite/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package graphite

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"time"

log "github.com/sirupsen/logrus"
)

// API represents a Graphite API client
type API interface {
Query(query string) (*float64, error)
}

// GraphiteAPI is a Graphite API client
type APIClient struct {
url url.URL
client *http.Client
timeout time.Duration
logCTX log.Entry
}

// Query performs a Graphite API query with the query it's passed
func (api APIClient) Query(quer string) (*float64, error) {
query := api.trimQuery(quer)
u, err := url.Parse(fmt.Sprintf("./render?%s", query))
if err != nil {
return nil, err
}

q := u.Query()
q.Set("format", "json")
u.RawQuery = q.Encode()

u.Path = path.Join(api.url.Path, u.Path)
u = api.url.ResolveReference(u)

req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}

ctx, cancel := context.WithTimeout(req.Context(), api.timeout)
defer cancel()

r, err := api.client.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer r.Body.Close()

b, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}

if 400 <= r.StatusCode {
return nil, fmt.Errorf("error response: %s", string(b))
}

var result graphiteResponse
err = json.Unmarshal(b, &result)
if err != nil {
return nil, err
}

var value *float64
for _, tr := range result {
for _, dp := range tr.DataPoints {
if dp.Value != nil {
value = dp.Value
}
}
}

return value, nil
mdb marked this conversation as resolved.
Show resolved Hide resolved
}

func (api APIClient) trimQuery(q string) string {
space := regexp.MustCompile(`\s+`)
return space.ReplaceAllString(q, " ")
}

type graphiteDataPoint struct {
Value *float64
TimeStamp time.Time
}

func (gdp *graphiteDataPoint) UnmarshalJSON(data []byte) error {
var v []interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}

if len(v) != 2 {
return fmt.Errorf("error unmarshaling data point: %v", v)
}

switch v[0].(type) {
case nil:
// no value
case float64:
f, _ := v[0].(float64)
gdp.Value = &f
case string:
f, err := strconv.ParseFloat(v[0].(string), 64)
if err != nil {
return err
}
gdp.Value = &f
default:
f, ok := v[0].(float64)
if !ok {
return fmt.Errorf("error unmarshaling value: %v", v[0])
}
gdp.Value = &f
}

switch v[1].(type) {
case nil:
// no value
case float64:
ts := int64(math.Round(v[1].(float64)))
gdp.TimeStamp = time.Unix(ts, 0)
case string:
ts, err := strconv.ParseInt(v[1].(string), 10, 64)
if err != nil {
return err
}
gdp.TimeStamp = time.Unix(ts, 0)
default:
ts, ok := v[1].(int64)
if !ok {
return fmt.Errorf("error unmarshaling timestamp: %v", v[0])
}
gdp.TimeStamp = time.Unix(ts, 0)
}

return nil
}

type graphiteTargetResp struct {
Target string `json:"target"`
DataPoints []graphiteDataPoint `json:"datapoints"`
}

type graphiteResponse []graphiteTargetResp
Loading