-
Notifications
You must be signed in to change notification settings - Fork 684
/
reporter.go
207 lines (179 loc) · 5.28 KB
/
reporter.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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Package metriton implements submitting telemetry data to the Metriton database.
//
// Metriton replaced Scout, and was originally going to have its own telemetry API and a
// Scout-compatibility endpoint during the migration. But now the Scout-compatible API is
// the only thing we use.
//
// See also: The old scout.py package <https://pypi.org/project/scout.py/> /
// <https://github.com/datawire/scout.py>.
//
// Things that are in scout.py, but are intentionally left of this package:
// - automatically setting the HTTP client user-agent string
// - an InstallIDFromConfigMap getter
package metriton
import (
"context"
"errors"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
var (
// The endpoint you should use by default
DefaultEndpoint = "https://metriton.datawire.io/scout"
// Use BetaEndpoint for testing purposes without polluting production data
BetaEndpoint = "https://kubernaut.io/beta/scout"
// ScoutPyEndpoint is the default endpoint used by scout.py; it obeys the
// SCOUT_HOST and SCOUT_HTTPS environment variables. I'm not sure when you should
// use it instead of DefaultEndpoint, but I'm putting it in Go so that I never
// have to look at scout.py again.
ScoutPyEndpoint = endpointFromEnv()
)
func getenvDefault(varname, def string) string {
ret := os.Getenv(varname)
if ret == "" {
ret = def
}
return ret
}
func endpointFromEnv() string {
host := getenvDefault("SCOUT_HOST", "kubernaut.io")
useHTTPS, _ := strconv.ParseBool(getenvDefault("SCOUT_HTTPS", "1"))
ret := &url.URL{
Scheme: "http",
Host: host,
Path: "/scout",
}
if useHTTPS {
ret.Scheme = "https"
}
return ret.String()
}
// Reporter is a client to
type Reporter struct {
// Information about the application submitting telemetry.
Application string
Version string
// GetInstallID is a function, instead of a fixed 'InstallID' string, in order to
// facilitate getting it lazily; and possibly updating the BaseMetadata based on
// the journey to getting the install ID. See StaticInstallID and
// InstallIDFromFilesystem.
GetInstallID func(*Reporter) (string, error)
// BaseMetadata will be merged in to the data passed to each call to .Report().
// If the data passed to .Report() and BaseMetadata have a key in common, the
// value passed as an argument to .Report() wins.
BaseMetadata map[string]interface{}
// The HTTP client used to to submit the request; if this is nil, then
// http.DefaultClient is used.
Client *http.Client
// The endpoint URL to submit to; if this is empty, then DefaultEndpoint is used.
Endpoint string
mu sync.Mutex
initialized bool
disabled bool
installID string
}
func (r *Reporter) ensureInitialized() error {
if r.Application == "" {
return errors.New("Reporter.Application may not be empty")
}
if r.Version == "" {
return errors.New("Reporter.Version may not be empty")
}
if r.GetInstallID == nil {
return errors.New("Reporter.GetInstallID may not be nil")
}
if r.initialized {
return nil
}
if r.BaseMetadata == nil {
r.BaseMetadata = make(map[string]interface{})
}
r.disabled = IsDisabledByUser()
installID, err := r.GetInstallID(r)
if err != nil {
return err
}
r.installID = installID
r.initialized = true
return nil
}
// IsDisabledByUser returns whether telemetry reporting is disabled by the user.
func IsDisabledByUser() bool {
// From scout.py
if strings.HasPrefix(os.Getenv("TRAVIS_REPO_SLUG"), "datawire/") {
return true
}
// This mimics the existing ad-hoc Go clients, rather than scout.py; it is a more
// sensitive trigger than scout.py's __is_disabled() (which only accepts "1",
// "true", "yes"; case-insensitive).
return os.Getenv("SCOUT_DISABLE") != ""
}
func (r *Reporter) InstallID() string {
r.mu.Lock()
defer r.mu.Unlock()
_ = r.ensureInitialized()
return r.installID
}
// Report submits a telemetry report to Metriton. It is safe to call .Report() from
// different goroutines. It is NOT safe to mutate the public fields in the Reporter while
// .Report() is being called.
func (r *Reporter) Report(ctx context.Context, metadata map[string]interface{}) (*Response, error) {
r.mu.Lock()
if err := r.ensureInitialized(); err != nil {
r.mu.Unlock()
return nil, err
}
var resp *Response
var err error
if r.disabled {
r.mu.Unlock()
} else {
client := r.Client
if client == nil {
client = http.DefaultClient
}
endpoint := r.Endpoint
if endpoint == "" {
endpoint = DefaultEndpoint
}
mergedMetadata := make(map[string]interface{}, len(r.BaseMetadata)+len(metadata))
// FWIW, the resolution of conflicts between 'r.BaseMetadata' and 'metadata'
// mimics scout.py; I'm not sure whether that aspect of scout.py's API is
// intentional or incidental.
for k, v := range r.BaseMetadata {
mergedMetadata[k] = v
}
for k, v := range metadata {
mergedMetadata[k] = v
}
report := Report{
Application: r.Application,
InstallID: r.installID,
Version: r.Version,
Metadata: mergedMetadata,
}
r.mu.Unlock()
resp, err = report.Send(ctx, client, endpoint)
if err != nil {
return nil, err
}
}
if resp == nil {
// This mimics scout.py
resp = &Response{
AppInfo: AppInfo{
LatestVersion: r.Version,
},
}
}
if resp != nil && resp.DisableScout {
r.mu.Lock()
r.disabled = true
r.mu.Unlock()
}
return resp, nil
}