Skip to content

Commit e8642eb

Browse files
committed
feat(newrelic): APM application id can be looked up by name
This is intended to be a quality of life addition to allow `ApplicationSet` resources to minimize boilerplate when dealing with NewRelic APM resources across multiple environments. - An application programmatically [uses the NR SDK to report to NewRelic](https://github.com/newrelic/go-agent/blob/master/v3/examples/server/main.go#L267-L274) - NewRelic APM Application ID has to then be retrieved following the first run of an application from the NR Dashboard and added to an ArgoCD `Application` via the annotation, across multiple environments. Using ArgoCD `ApplicationSet` resources, the annotation can be either provided via app_id, or by name, e.g. `MyApp-{{ .values.env }}` If `dest.Recipient` can be parsed to an int, then app_id is provided and logic remains as before. If `dest.Recipient` cannot be parsed to an integer, then it was passed by name and we call `/v2/applications.json` to query using `filter[name]` If `/v2/applications.json` returns multiple application IDs then an error is returned, as we can't determine which app_id to use to place the deployment marker. Signed-off-by: 3bbbeau <[email protected]>
1 parent ec01d49 commit e8642eb

File tree

2 files changed

+177
-3
lines changed

2 files changed

+177
-3
lines changed

pkg/services/newrelic.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"net/http"
9+
"strconv"
910
"strings"
1011
texttemplate "text/template"
1112

@@ -29,8 +30,10 @@ type NewrelicNotification struct {
2930
}
3031

3132
var (
32-
ErrMissingConfig = errors.New("config is missing")
33-
ErrMissingApiKey = errors.New("apiKey is missing")
33+
ErrMissingConfig = errors.New("config is missing")
34+
ErrMissingApiKey = errors.New("apiKey is missing")
35+
ErrAppIdMultipleMatches = errors.New("multiple matches found for application name")
36+
ErrAppIdNoMatches = errors.New("no matches found for application name")
3437
)
3538

3639
func (n *NewrelicNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) {
@@ -116,6 +119,43 @@ type newrelicService struct {
116119
type newrelicDeploymentMarkerRequest struct {
117120
Deployment NewrelicNotification `json:"deployment"`
118121
}
122+
type newrelicApplicationsResponse struct {
123+
Applications []struct {
124+
ID json.Number `json:"id"`
125+
} `json:"applications"`
126+
}
127+
128+
func (s newrelicService) getApplicationId(client *http.Client, appName string) (string, error) {
129+
applicationsApi := fmt.Sprintf("%s/v2/applications.json?filter[name]=%s", s.opts.ApiURL, appName)
130+
req, err := http.NewRequest(http.MethodGet, applicationsApi, nil)
131+
if err != nil {
132+
return "", fmt.Errorf("Failed to create filtered application request: %s", err)
133+
}
134+
135+
req.Header.Set("Content-Type", "application/json")
136+
req.Header.Set("X-Api-Key", s.opts.ApiKey)
137+
138+
resp, err := client.Do(req)
139+
if err != nil {
140+
return "", err
141+
}
142+
defer resp.Body.Close()
143+
144+
var data newrelicApplicationsResponse
145+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
146+
return "", fmt.Errorf("Failed to decode applications response: %s", err)
147+
}
148+
149+
if len(data.Applications) == 0 {
150+
return "", ErrAppIdNoMatches
151+
}
152+
153+
if len(data.Applications) > 1 {
154+
return "", ErrAppIdMultipleMatches
155+
}
156+
157+
return data.Applications[0].ID.String(), nil
158+
}
119159

120160
func (s newrelicService) Send(notification Notification, dest Destination) (err error) {
121161
if s.opts.ApiKey == "" {
@@ -149,7 +189,22 @@ func (s newrelicService) Send(notification Notification, dest Destination) (err
149189
return err
150190
}
151191

152-
markerApi := fmt.Sprintf(s.opts.ApiURL+"/v2/applications/%s/deployments.json", dest.Recipient)
192+
var appId = dest.Recipient
193+
if dest.Recipient != "" {
194+
_, err := strconv.Atoi(dest.Recipient)
195+
if err != nil {
196+
log.Debugf(
197+
"Recipient was provided by application name. Looking up the application id for %s",
198+
dest.Recipient,
199+
)
200+
appId, err = s.getApplicationId(client, dest.Recipient)
201+
if err != nil {
202+
log.Errorf("Failed to lookup application %s by name: %s", dest.Recipient, err)
203+
return err
204+
}
205+
}
206+
}
207+
markerApi := fmt.Sprintf(s.opts.ApiURL+"/v2/applications/%s/deployments.json", appId)
153208
req, err := http.NewRequest(http.MethodPost, markerApi, bytes.NewBuffer(jsonValue))
154209
if err != nil {
155210
log.Errorf("Failed to create deployment marker request: %s", err)

pkg/services/newrelic_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,59 @@ func TestSend_Newrelic(t *testing.T) {
172172
require.NoError(t, err)
173173
})
174174

175+
t.Run("recipient is application name", func(t *testing.T) {
176+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
177+
if r.URL.Path == "/v2/applications.json" {
178+
_, err := w.Write([]byte(`{
179+
"applications": [
180+
{"id": "123456789"}
181+
]
182+
}`))
183+
if !assert.NoError(t, err) {
184+
t.FailNow()
185+
}
186+
return
187+
}
188+
189+
b, err := io.ReadAll(r.Body)
190+
if !assert.NoError(t, err) {
191+
t.FailNow()
192+
}
193+
194+
assert.Equal(t, "/v2/applications/123456789/deployments.json", r.URL.Path)
195+
assert.Equal(t, []string{"application/json"}, r.Header["Content-Type"])
196+
assert.Equal(t, []string{"NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ"}, r.Header["X-Api-Key"])
197+
198+
assert.JSONEq(t, `{
199+
"deployment": {
200+
"revision": "2027ed5",
201+
"description": "message",
202+
203+
}
204+
}`, string(b))
205+
}))
206+
defer ts.Close()
207+
208+
service := NewNewrelicService(NewrelicOptions{
209+
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
210+
ApiURL: ts.URL,
211+
})
212+
err := service.Send(Notification{
213+
Message: "message",
214+
Newrelic: &NewrelicNotification{
215+
Revision: "2027ed5",
216+
217+
},
218+
}, Destination{
219+
Service: "newrelic",
220+
Recipient: "myapp",
221+
})
222+
223+
if !assert.NoError(t, err) {
224+
t.FailNow()
225+
}
226+
})
227+
175228
t.Run("missing config", func(t *testing.T) {
176229
service := NewNewrelicService(NewrelicOptions{
177230
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
@@ -202,3 +255,69 @@ func TestSend_Newrelic(t *testing.T) {
202255
}
203256
})
204257
}
258+
259+
func TestGetApplicationId(t *testing.T) {
260+
t.Run("successful lookup by application name", func(t *testing.T) {
261+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
262+
assert.Equal(t, "/v2/applications.json", r.URL.Path)
263+
assert.Equal(t, "GET", r.Method)
264+
assert.Equal(t, "myapp", r.URL.Query().Get("filter[name]"))
265+
assert.Equal(t, []string{"application/json"}, r.Header["Content-Type"])
266+
assert.Equal(t, []string{"NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ"}, r.Header["X-Api-Key"])
267+
268+
_, err := w.Write([]byte(`{
269+
"applications": [
270+
{"id": "123456789"}
271+
]
272+
}`))
273+
if !assert.NoError(t, err) {
274+
t.FailNow()
275+
}
276+
}))
277+
defer ts.Close()
278+
service := NewNewrelicService(NewrelicOptions{
279+
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
280+
ApiURL: ts.URL,
281+
}).(*newrelicService)
282+
appId, err := service.getApplicationId(http.DefaultClient, "myapp")
283+
assert.NoError(t, err)
284+
assert.Equal(t, "123456789", appId)
285+
})
286+
287+
t.Run("application not found", func(t *testing.T) {
288+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
289+
_, err := w.Write([]byte(`{"applications": []}`))
290+
if !assert.NoError(t, err) {
291+
t.FailNow()
292+
}
293+
}))
294+
defer ts.Close()
295+
service := NewNewrelicService(NewrelicOptions{
296+
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
297+
ApiURL: ts.URL,
298+
}).(*newrelicService)
299+
_, err := service.getApplicationId(http.DefaultClient, "myapp")
300+
assert.Equal(t, ErrAppIdNoMatches, err)
301+
})
302+
303+
t.Run("multiple matches for application name", func(t *testing.T) {
304+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
305+
_, err := w.Write([]byte(`{
306+
"applications": [
307+
{"id": "123456789"},
308+
{"id": "987654321"}
309+
]
310+
}`))
311+
if !assert.NoError(t, err) {
312+
t.FailNow()
313+
}
314+
}))
315+
defer ts.Close()
316+
service := NewNewrelicService(NewrelicOptions{
317+
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
318+
ApiURL: ts.URL,
319+
}).(*newrelicService)
320+
_, err := service.getApplicationId(http.DefaultClient, "myapp")
321+
assert.Equal(t, ErrAppIdMultipleMatches, err)
322+
})
323+
}

0 commit comments

Comments
 (0)