-
Notifications
You must be signed in to change notification settings - Fork 70
/
polling_recipe_validator.go
163 lines (131 loc) · 4.07 KB
/
polling_recipe_validator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package validation
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"time"
"github.com/newrelic/newrelic-cli/internal/credentials"
"github.com/newrelic/newrelic-cli/internal/install/types"
"github.com/newrelic/newrelic-cli/internal/install/ux"
"github.com/newrelic/newrelic-client-go/pkg/nrdb"
)
type contextKey int
const (
defaultMaxAttempts = 60
defaultInterval = 5 * time.Second
TestIdentifierKey contextKey = iota
)
// PollingRecipeValidator is an implementation of the RecipeValidator interface
// that polls NRDB to assert data is being reported for the given recipe.
type PollingRecipeValidator struct {
maxAttempts int
interval time.Duration
client nrdbClient
progressIndicator ux.ProgressIndicator
}
// NewPollingRecipeValidator returns a new instance of PollingRecipeValidator.
func NewPollingRecipeValidator(c nrdbClient) *PollingRecipeValidator {
v := PollingRecipeValidator{
maxAttempts: defaultMaxAttempts,
interval: defaultInterval,
client: c,
progressIndicator: ux.NewSpinner(),
}
return &v
}
// Validate polls NRDB to assert data is being reported for the given recipe.
func (m *PollingRecipeValidator) Validate(ctx context.Context, dm types.DiscoveryManifest, r types.Recipe) (string, error) {
return m.waitForData(ctx, dm, r)
}
func (m *PollingRecipeValidator) waitForData(ctx context.Context, dm types.DiscoveryManifest, r types.Recipe) (string, error) {
count := 0
ticker := time.NewTicker(m.interval)
defer ticker.Stop()
progressMsg := "Checking for data in New Relic (this may take a few minutes)..."
m.progressIndicator.Start(progressMsg)
defer m.progressIndicator.Stop()
for {
if count == m.maxAttempts {
m.progressIndicator.Fail("")
return "", fmt.Errorf("reached max validation attempts")
}
ok, entityGUID, err := m.tryValidate(ctx, dm, r)
if err != nil {
m.progressIndicator.Fail("")
return "", err
}
count++
if ok {
m.progressIndicator.Success("")
return entityGUID, nil
}
select {
case <-ticker.C:
continue
case <-ctx.Done():
m.progressIndicator.Fail("")
return "", fmt.Errorf("validation cancelled")
}
}
}
func (m *PollingRecipeValidator) tryValidate(ctx context.Context, dm types.DiscoveryManifest, r types.Recipe) (bool, string, error) {
query, err := substituteHostname(dm, r)
if err != nil {
return false, "", err
}
results, err := m.executeQuery(ctx, query)
if err != nil {
return false, "", err
}
if len(results) == 0 {
return false, "", nil
}
// The query is assumed to use a count aggregate function
count := results[0]["count"].(float64)
if count > 0 {
// Try and parse an entity GUID from the results. The query is assumed to
// optionally use a facet over entityGuid. The standard case seems to be
// that all entities contain a facet of "entityGuid", and so if we find it
// here, we return it.
if entityGUID, ok := results[0]["entityGuid"]; ok {
return true, entityGUID.(string), nil
}
// In the logs integration, the facet doesn't contain "entityGuid", but
// does contain, "entity.guid", so here we check for that also.
if entityGUID, ok := results[0]["entity.guids"]; ok {
return true, entityGUID.(string), nil
}
return true, "", nil
}
return false, "", nil
}
func substituteHostname(dm types.DiscoveryManifest, r types.Recipe) (string, error) {
tmpl, err := template.New("validationNRQL").Parse(r.ValidationNRQL)
if err != nil {
panic(err)
}
v := struct {
HOSTNAME string
}{
HOSTNAME: dm.Hostname,
}
var tpl bytes.Buffer
if err = tmpl.Execute(&tpl, v); err != nil {
return "", err
}
return tpl.String(), nil
}
func (m *PollingRecipeValidator) executeQuery(ctx context.Context, query string) ([]nrdb.NRDBResult, error) {
profile := credentials.DefaultProfile()
if profile == nil || profile.AccountID == 0 {
return nil, errors.New("no account ID found in default profile")
}
nrql := nrdb.NRQL(query)
result, err := m.client.QueryWithContext(ctx, profile.AccountID, nrql)
if err != nil {
return nil, err
}
return result.Results, nil
}