Skip to content

Commit ab28006

Browse files
author
Filip Petkovski
committed
Add New Relic as a metrics provider
1 parent c6f3a87 commit ab28006

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed

pkg/metrics/providers/factory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func (factory Factory) Provider(
1818
return NewDatadogProvider(metricInterval, provider, credentials)
1919
case "cloudwatch":
2020
return NewCloudWatchProvider(metricInterval, provider)
21+
case "newrelic":
22+
return NewNewRelicProvider(metricInterval, provider, credentials)
2123
default:
2224
return NewPrometheusProvider(provider, credentials)
2325
}

pkg/metrics/providers/newrelic.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package providers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"time"
10+
11+
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
12+
)
13+
14+
const (
15+
newrelicInsightsDefaultHost = "https://insights-api.newrelic.com"
16+
17+
newrelicQueryKeySecretKey = "newrelic_query_key"
18+
newrelicAccountIdSecretKey = "newrelic_account_id"
19+
20+
newrelicQueryKeyHeaderKey = "X-Query-Key"
21+
)
22+
23+
// NewRelicProvider executes newrelic queries
24+
type NewRelicProvider struct {
25+
insightsQueryEndpoint string
26+
27+
timeout time.Duration
28+
queryKey string
29+
fromDelta int64
30+
}
31+
32+
type newRelicResponse struct {
33+
Results []struct {
34+
Result *float64 `json:"result"`
35+
} `json:"results"`
36+
}
37+
38+
// NewNewRelicProvider takes a canary spec, a provider spec and the credentials map, and
39+
// returns a NewRelic client ready to execute queries against the Insights API
40+
func NewNewRelicProvider(
41+
metricInterval string,
42+
provider flaggerv1.MetricTemplateProvider,
43+
credentials map[string][]byte,
44+
) (*NewRelicProvider, error) {
45+
address := provider.Address
46+
if address == "" {
47+
address = newrelicInsightsDefaultHost
48+
}
49+
50+
accountId, ok := credentials[newrelicAccountIdSecretKey]
51+
if !ok {
52+
return nil, fmt.Errorf("newrelic credentials does not contain " + newrelicAccountIdSecretKey)
53+
}
54+
55+
queryEndpoint := fmt.Sprintf("%s/v1/accounts/%s/query", address, accountId)
56+
nr := NewRelicProvider{
57+
timeout: 5 * time.Second,
58+
insightsQueryEndpoint: queryEndpoint,
59+
}
60+
61+
if b, ok := credentials[newrelicQueryKeySecretKey]; ok {
62+
nr.queryKey = string(b)
63+
} else {
64+
return nil, fmt.Errorf("newrelic credentials does not contain " + newrelicQueryKeySecretKey)
65+
}
66+
67+
md, err := time.ParseDuration(metricInterval)
68+
if err != nil {
69+
return nil, fmt.Errorf("error parsing metric interval: %w", err)
70+
}
71+
72+
nr.fromDelta = int64(md.Seconds())
73+
return &nr, nil
74+
}
75+
76+
// RunQuery executes the new relic query against the New Relic Insights API
77+
// and returns the the first result
78+
func (p *NewRelicProvider) RunQuery(query string) (float64, error) {
79+
req, err := p.newInsightsRequest(query)
80+
if err != nil {
81+
return 0, err
82+
}
83+
84+
ctx, cancel := context.WithTimeout(req.Context(), p.timeout)
85+
defer cancel()
86+
r, err := http.DefaultClient.Do(req.WithContext(ctx))
87+
if err != nil {
88+
return 0, fmt.Errorf("request failed: %w", err)
89+
}
90+
91+
defer r.Body.Close()
92+
b, err := ioutil.ReadAll(r.Body)
93+
if err != nil {
94+
return 0, fmt.Errorf("error reading body: %w", err)
95+
}
96+
97+
if r.StatusCode != http.StatusOK {
98+
return 0, fmt.Errorf("error response: %s: %w", string(b), err)
99+
}
100+
101+
var res newRelicResponse
102+
if err := json.Unmarshal(b, &res); err != nil {
103+
return 0, fmt.Errorf("error unmarshaling result: %w, '%s'", err, string(b))
104+
}
105+
106+
if len(res.Results) != 1 {
107+
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
108+
}
109+
110+
if res.Results[0].Result == nil {
111+
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
112+
}
113+
114+
return *res.Results[0].Result, nil
115+
}
116+
117+
// IsOnline calls the NewRelic's insights API with
118+
// and returns an error if the request is rejected
119+
func (p *NewRelicProvider) IsOnline() (bool, error) {
120+
req, err := p.newInsightsRequest("SELECT * FROM Metric")
121+
if err != nil {
122+
return false, fmt.Errorf("error http.NewRequest: %w", err)
123+
}
124+
125+
ctx, cancel := context.WithTimeout(req.Context(), p.timeout)
126+
defer cancel()
127+
r, err := http.DefaultClient.Do(req.WithContext(ctx))
128+
if err != nil {
129+
return false, fmt.Errorf("request failed: %w", err)
130+
}
131+
132+
defer r.Body.Close()
133+
134+
b, err := ioutil.ReadAll(r.Body)
135+
if err != nil {
136+
return false, fmt.Errorf("error reading body: %w", err)
137+
}
138+
139+
if r.StatusCode != http.StatusOK {
140+
return false, fmt.Errorf("error response: %s", string(b))
141+
}
142+
143+
return true, nil
144+
}
145+
146+
func (p *NewRelicProvider) newInsightsRequest(query string) (*http.Request, error) {
147+
req, err := http.NewRequest("GET", p.insightsQueryEndpoint, nil)
148+
if err != nil {
149+
return nil, fmt.Errorf("error http.NewRequest: %w", err)
150+
}
151+
152+
req.Header.Set(newrelicQueryKeyHeaderKey, p.queryKey)
153+
154+
q := req.URL.Query()
155+
q.Add("nrql", fmt.Sprintf("%s SINCE %d seconds ago", query, p.fromDelta))
156+
req.URL.RawQuery = q.Encode()
157+
158+
return req, nil
159+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package providers
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
15+
)
16+
17+
func TestNewNewRelicProvider(t *testing.T) {
18+
queryKey := "query-key"
19+
accountId := "51312"
20+
cs := map[string][]byte{
21+
"newrelic_query_key": []byte(queryKey),
22+
"newrelic_account_id": []byte(accountId),
23+
}
24+
25+
duration := "100s"
26+
secondsDuration, err := time.ParseDuration(duration)
27+
require.NoError(t, err)
28+
29+
nr, err := NewNewRelicProvider("100s", flaggerv1.MetricTemplateProvider{}, cs)
30+
require.NoError(t, err)
31+
assert.Equal(t, "https://insights-api.newrelic.com/v1/accounts/51312/query", nr.insightsQueryEndpoint)
32+
assert.Equal(t, int64(secondsDuration.Seconds()), nr.fromDelta)
33+
assert.Equal(t, queryKey, nr.queryKey)
34+
}
35+
36+
func TestNewRelicProvider_RunQuery(t *testing.T) {
37+
queryKey := "query-key"
38+
accountId := "51312"
39+
t.Run("ok", func(t *testing.T) {
40+
q := `SELECT sum(nginx_ingress_controller_requests) / 1 FROM Metric WHERE status = '200'`
41+
eq := `SELECT sum(nginx_ingress_controller_requests) / 1 FROM Metric WHERE status = '200' SINCE 60 seconds ago`
42+
er := 1.11111
43+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
aq := r.URL.Query().Get("nrql")
45+
assert.Equal(t, eq, aq)
46+
assert.Equal(t, queryKey, r.Header.Get(newrelicQueryKeyHeaderKey))
47+
48+
json := fmt.Sprintf(`{"results":[{"result": %f}]}`, er)
49+
w.Write([]byte(json))
50+
}))
51+
defer ts.Close()
52+
53+
nr, err := NewNewRelicProvider("1m",
54+
flaggerv1.MetricTemplateProvider{
55+
Address: ts.URL,
56+
},
57+
map[string][]byte{
58+
"newrelic_query_key": []byte(queryKey),
59+
"newrelic_account_id": []byte(accountId),
60+
},
61+
)
62+
require.NoError(t, err)
63+
64+
f, err := nr.RunQuery(q)
65+
assert.NoError(t, err)
66+
assert.Equal(t, er, f)
67+
})
68+
69+
t.Run("no values", func(t *testing.T) {
70+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71+
json := fmt.Sprintf(`{"results": []}`)
72+
w.Write([]byte(json))
73+
}))
74+
defer ts.Close()
75+
76+
dp, err := NewNewRelicProvider(
77+
"1m",
78+
flaggerv1.MetricTemplateProvider{Address: ts.URL},
79+
map[string][]byte{
80+
"newrelic_query_key": []byte(queryKey),
81+
"newrelic_account_id": []byte(accountId)},
82+
)
83+
require.NoError(t, err)
84+
_, err = dp.RunQuery("")
85+
require.True(t, errors.Is(err, ErrNoValuesFound))
86+
})
87+
}
88+
89+
func TestNewReelicProvider_IsOnline(t *testing.T) {
90+
for _, c := range []struct {
91+
code int
92+
errExpected bool
93+
}{
94+
{code: http.StatusOK, errExpected: false},
95+
{code: http.StatusUnauthorized, errExpected: true},
96+
} {
97+
t.Run(fmt.Sprintf("%d", c.code), func(t *testing.T) {
98+
queryKey := "query-key"
99+
accountId := "51312"
100+
query := `SELECT * FROM Metric SINCE 60 seconds ago`
101+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102+
assert.Equal(t, queryKey, r.Header.Get(newrelicQueryKeyHeaderKey))
103+
assert.Equal(t, query, r.URL.Query().Get("nrql"))
104+
w.WriteHeader(c.code)
105+
}))
106+
defer ts.Close()
107+
108+
dp, err := NewNewRelicProvider(
109+
"1m",
110+
flaggerv1.MetricTemplateProvider{Address: ts.URL},
111+
map[string][]byte{
112+
"newrelic_query_key": []byte(queryKey),
113+
"newrelic_account_id": []byte(accountId),
114+
},
115+
)
116+
require.NoError(t, err)
117+
118+
_, err = dp.IsOnline()
119+
if c.errExpected {
120+
require.Error(t, err)
121+
} else {
122+
require.NoError(t, err)
123+
}
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)