forked from stellar-deprecated/kelp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
metricsTracker.go
398 lines (361 loc) · 14.5 KB
/
metricsTracker.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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
package plugins
import (
"encoding/json"
"fmt"
"log"
"net/http"
"runtime/debug"
"time"
"github.com/stellar/kelp/support/networking"
"github.com/stellar/kelp/support/utils"
)
// we don't want this to be a custom event, custom events should only be added from the amplitude UI
const (
amplitudeAPIURL string = "https://api2.amplitude.com/2/httpapi"
startupEventName string = "bot_startup"
updateEventName string = "update_offers"
deleteEventName string = "delete_offers"
secondsSinceStartKey string = "seconds_since_start"
guiUserIDKey string = "gui_user_id"
)
// MetricsTracker wraps the properties for Amplitude events,
// and can be used to directly send events to the
// Amplitude HTTP API.
type MetricsTracker struct {
client *http.Client
apiKey string
userID string
guiUserID string
deviceID string
eventProps map[string]interface{}
botStartTime time.Time
isDisabled bool
updateEventSentTime *time.Time
cliVersion string
failedStartupSend bool
}
// TODO DS Investigate other fields to add to this top-level event.
// fields for the event object: https://help.amplitude.com/hc/en-us/articles/360032842391-HTTP-API-V2#http-api-v2-events
type event struct {
UserID string `json:"user_id"`
SessionID int64 `json:"session_id"`
DeviceID string `json:"device_id"`
EventType string `json:"event_type"`
Version string `json:"app_version"`
EventProperties interface{} `json:"event_properties"`
}
// CommonPropsStruct holds the properties that are common to all our Amplitude events.
// TODO DS Add geodata.
// TODO DS Add cloud server information.
// TODO DS Add time to run update function as `millisForUpdate`.
type CommonPropsStruct struct {
CliVersion string `json:"cli_version"`
GitHash string `json:"git_hash"`
Env string `json:"env"`
Goos string `json:"goos"`
Goarch string `json:"goarch"`
Goarm string `json:"goarm"`
GoVersion string `json:"go_version"`
SecondsSinceStart float64 `json:"seconds_since_start"`
IsTestnet bool `json:"is_testnet"`
GuiVersion string `json:"gui_version"` // can be present for cli trackers if they are started from the GUI
}
// MakeCommonProps is a factory mmethod for CommonPropsStruct
func MakeCommonProps(
cliVersion string,
gitHash string,
env string,
goos string,
goarch string,
goarm string,
goVersion string,
secondsSinceStart float64,
isTestnet bool,
guiVersion string,
) *CommonPropsStruct {
return &CommonPropsStruct{
CliVersion: cliVersion,
GitHash: gitHash,
Env: env,
Goos: goos,
Goarch: goarch,
Goarm: goarm,
GoVersion: goVersion,
SecondsSinceStart: secondsSinceStart,
IsTestnet: isTestnet,
GuiVersion: guiVersion,
}
}
// CliPropsStruct contains only those props that are needed for all CLI events
type CliPropsStruct struct {
Strategy string `json:"strategy"`
UpdateTimeIntervalSeconds float64 `json:"update_time_interval_seconds"`
Exchange string `json:"exchange"`
TradingPair string `json:"trading_pair"`
MaxTickDelayMillis int64 `json:"max_tick_delay_millis"`
SubmitMode string `json:"submit_mode"`
DeleteCyclesThreshold int64 `json:"delete_cycles_threshold"`
FillTrackerSleepMillis uint32 `json:"fill_tracker_sleep_millis"`
FillTrackerDeleteCyclesThreshold int64 `json:"fill_tracker_delete_cycles_threshold"`
SynchronizeStateLoadEnable bool `json:"synchronize_state_load_enable"`
SynchronizeStateLoadMaxRetries int `json:"synchronize_state_load_max_retries"`
EnabledFeatureDollarValue bool `json:"enabled_feature_dollar_value"`
AlertType string `json:"alert_type"`
EnabledFeatureMonitoring bool `json:"enabled_feature_monitoring"`
EnabledFeatureFilters bool `json:"enabled_feature_filters"`
EnabledFeaturePostgres bool `json:"enabled_feature_postgres"`
EnabledFeatureLogging bool `json:"enabled_feature_logging"`
OperationalBuffer float64 `json:"operational_buffer"`
OperationalBufferNonNativePct float64 `json:"operational_buffer_non_native_pct"`
SimMode bool `json:"sim_mode"`
FixedIterations uint64 `json:"fixed_iterations"`
}
// MakeCliProps is a factory mmethod for CommonPropsStruct
func MakeCliProps(
strategy string,
updateTimeIntervalSeconds float64,
exchange string,
tradingPair string,
maxTickDelayMillis int64,
submitMode string,
deleteCyclesThreshold int64,
fillTrackerSleepMillis uint32,
fillTrackerDeleteCyclesThreshold int64,
synchronizeStateLoadEnable bool,
synchronizeStateLoadMaxRetries int,
enabledFeatureDollarValue bool,
alertType string,
enabledFeatureMonitoring bool,
enabledFeatureFilters bool,
enabledFeaturePostgres bool,
enabledFeatureLogging bool,
operationalBuffer float64,
operationalBufferNonNativePct float64,
simMode bool,
fixedIterations uint64,
) *CliPropsStruct {
return &CliPropsStruct{
Strategy: strategy,
UpdateTimeIntervalSeconds: updateTimeIntervalSeconds,
Exchange: exchange,
TradingPair: tradingPair,
MaxTickDelayMillis: maxTickDelayMillis,
SubmitMode: submitMode,
DeleteCyclesThreshold: deleteCyclesThreshold,
FillTrackerSleepMillis: fillTrackerSleepMillis,
FillTrackerDeleteCyclesThreshold: fillTrackerDeleteCyclesThreshold,
SynchronizeStateLoadEnable: synchronizeStateLoadEnable,
SynchronizeStateLoadMaxRetries: synchronizeStateLoadMaxRetries,
EnabledFeatureDollarValue: enabledFeatureDollarValue,
AlertType: alertType,
EnabledFeatureMonitoring: enabledFeatureMonitoring,
EnabledFeatureFilters: enabledFeatureFilters,
EnabledFeaturePostgres: enabledFeaturePostgres,
EnabledFeatureLogging: enabledFeatureLogging,
OperationalBuffer: operationalBuffer,
OperationalBufferNonNativePct: operationalBufferNonNativePct,
SimMode: simMode,
FixedIterations: fixedIterations,
}
}
// updateProps holds the properties for the update Amplitude event.
type updateProps struct {
Success bool `json:"success"`
MillisForUpdate int64 `json:"millis_for_update"`
SecondsSinceLastUpdateMetric float64 `json:"seconds_since_last_update_metric"` // helps understand total runtime of bot when summing this field across events
NumPruneOps int `json:"num_prune_ops"`
NumUpdateOpsDelete int `json:"num_update_ops_delete"`
NumUpdateOpsUpdate int `json:"num_update_ops_update"`
NumUpdateOpsCreate int `json:"num_update_ops_create"`
}
// deleteProps holds the properties for the delete Amplitude event.
// TODO DS StackTrace may need to be a message instead of or in addition to a
// stack trace. The goal is to get crash logs, Amplitude may not enable this.
type deleteProps struct {
Exit bool `json:"exit"`
StackTrace string `json:"stack_trace"`
}
type eventWrapper struct {
APIKey string `json:"api_key"`
Events []event `json:"events"`
}
// UpdateLoopResult contains the results of the orderbook update.
// Note that this is used in `trader/trader.go`, but it is defined here to avoid an import cycle.
type UpdateLoopResult struct {
Success bool
NumPruneOps int
NumUpdateOpsDelete int
NumUpdateOpsUpdate int
NumUpdateOpsCreate int
}
// response structure taken from here: https://help.amplitude.com/hc/en-us/articles/360032842391-HTTP-API-V2#tocSsuccesssummary
type amplitudeResponse struct {
Code int `json:"code"`
EventsIngested int `json:"events_ingested"`
PayloadSizeBytes int `json:"payload_size_bytes"`
ServerUploadTime int64 `json:"server_upload_time"`
}
// String is the Stringer method
func (ar amplitudeResponse) String() string {
return fmt.Sprintf("amplitudeResponse[Code=%d, EventsIngested=%d, PayloadSizeBytes=%d, ServerUploadTime=%d (%s)]",
ar.Code,
ar.EventsIngested,
ar.PayloadSizeBytes,
ar.ServerUploadTime,
time.Unix(ar.ServerUploadTime, 0).Format("20060102T150405MST"),
)
}
// MakeMetricsTracker is a factory method
func MakeMetricsTracker(
client *http.Client,
apiKey string,
userID string,
guiUserID string,
deviceID string,
botStartTime time.Time,
isDisabled bool,
commonProps *CommonPropsStruct,
cliProps *CliPropsStruct,
) (*MetricsTracker, error) {
if commonProps == nil {
return nil, fmt.Errorf("need a non-nil commonProps to make a metrics tracker")
}
commonPropsMap, e := utils.ToMapStringInterface(*commonProps)
if e != nil {
return nil, fmt.Errorf("could not convert commonProps to map: %s", e)
}
var cliPropsMap map[string]interface{}
if cliProps != nil {
cliPropsMap, e = utils.ToMapStringInterface(*cliProps)
if e != nil {
return nil, fmt.Errorf("could not convert cliProps to map: %s", e)
}
}
mergedProps, e := utils.MergeMaps(commonPropsMap, cliPropsMap)
if e != nil {
return nil, fmt.Errorf("could not merge commonPropsMap and cliPropsMap: %s", e)
}
return &MetricsTracker{
client: client,
apiKey: apiKey,
userID: userID,
guiUserID: guiUserID,
deviceID: deviceID,
eventProps: mergedProps,
botStartTime: botStartTime,
isDisabled: isDisabled,
updateEventSentTime: nil,
cliVersion: commonProps.CliVersion,
failedStartupSend: false,
}, nil
}
// GetUpdateEventSentTime gets the last sent time of the update event.
func (mt *MetricsTracker) GetUpdateEventSentTime() *time.Time {
return mt.updateEventSentTime
}
// SendStartupEvent sends the startup Amplitude event.
func (mt *MetricsTracker) SendStartupEvent(now time.Time) error {
e := mt.sendEvent(startupEventName, mt.eventProps, now)
if e != nil {
mt.failedStartupSend = true
return fmt.Errorf("metric - failed to send startup event: %s", e)
}
return nil
}
// SendUpdateEvent sends the update Amplitude event.
func (mt *MetricsTracker) SendUpdateEvent(now time.Time, updateResult UpdateLoopResult, millisForUpdate int64) error {
var secondsSinceLastUpdateMetric float64
if mt.updateEventSentTime == nil {
secondsSinceLastUpdateMetric = now.Sub(mt.botStartTime).Seconds()
} else {
secondsSinceLastUpdateMetric = now.Sub(*mt.updateEventSentTime).Seconds()
}
updateProps := updateProps{
Success: updateResult.Success,
MillisForUpdate: millisForUpdate,
SecondsSinceLastUpdateMetric: secondsSinceLastUpdateMetric,
NumPruneOps: updateResult.NumPruneOps,
NumUpdateOpsDelete: updateResult.NumUpdateOpsDelete,
NumUpdateOpsUpdate: updateResult.NumUpdateOpsUpdate,
NumUpdateOpsCreate: updateResult.NumUpdateOpsCreate,
}
e := mt.sendEvent(updateEventName, updateProps, now)
if e != nil {
return fmt.Errorf("could not send update event: %s", e)
}
mt.updateEventSentTime = &now
return nil
}
// SendDeleteEvent sends the delete Amplitude event.
func (mt *MetricsTracker) SendDeleteEvent(exit bool) error {
deleteProps := deleteProps{
Exit: exit,
StackTrace: string(debug.Stack()),
}
return mt.sendEvent(deleteEventName, deleteProps, time.Now())
}
// SendEvent sends an event with its type and properties to Amplitude.
func (mt *MetricsTracker) sendEvent(eventType string, eventPropsInterface interface{}, now time.Time) error {
return mt.SendEventForGuiUser(mt.guiUserID, eventType, eventPropsInterface, now)
}
// SendEventForGuiUser sends an event with its type and properties to Amplitude.
func (mt *MetricsTracker) SendEventForGuiUser(guiUserID string, eventType string, eventPropsInterface interface{}, now time.Time) error {
if mt == nil || mt.apiKey == "" || mt.userID == "-1" || mt.isDisabled {
log.Printf("metric - not sending event metric of type '%s' because metrics are disabled", eventType)
return nil
}
if mt.failedStartupSend {
return fmt.Errorf("metric - not sending event metric of type '%s' because we failed to send startup event", eventType)
}
trackerProps := mt.eventProps
trackerProps[secondsSinceStartKey] = now.Sub(mt.botStartTime).Seconds()
if guiUserID != "" {
// add this on for requests that come from the gui either as a web event or a bot spawned from the GUI
trackerProps[guiUserIDKey] = guiUserID
}
inputProps, e := utils.ToMapStringInterface(eventPropsInterface)
if e != nil {
return fmt.Errorf("could not convert event inputProps to map: %s", e)
}
mergedProps, e := utils.MergeMaps(trackerProps, inputProps)
if e != nil {
return fmt.Errorf("could not merge event properties: %s", e)
}
// session_id is the start time of the session in milliseconds since epoch (Unix Timestamp),
// necessary to associate events with a particular system (taken from amplitude docs)
eventW := eventWrapper{
APIKey: mt.apiKey,
Events: []event{{
UserID: mt.userID,
SessionID: mt.botStartTime.Unix() * 1000, // convert to millis based on docs
DeviceID: mt.deviceID,
EventType: eventType,
EventProperties: mergedProps,
Version: mt.cliVersion,
}},
}
requestBody, e := json.Marshal(eventW)
if e != nil {
return fmt.Errorf("could not marshal json request: %s", e)
}
// TODO DS - wrap these API functions into support/sdk/amplitude.go
var responseData amplitudeResponse
e = networking.JSONRequest(mt.client, "POST", amplitudeAPIURL, string(requestBody), map[string]string{}, &responseData, "")
if e != nil {
return fmt.Errorf("could not post amplitude request: %s", e)
}
if responseData.Code == 200 {
log.Printf("metric - successfully sent event metric of type '%s'", eventType)
} else {
// work on copy so we don't modify original (good hygiene)
eventWCensored := *(&eventW)
// we don't want to display the apiKey in the logs so censor it
eventWCensored.APIKey = ""
requestWCensored, e := json.Marshal(eventWCensored)
if e != nil {
return fmt.Errorf("metric - failed to send event metric of type '%s' (response=%s), error while trying to marshall requestWCensored: %s", eventType, responseData.String(), e)
}
return fmt.Errorf("metric - failed to send event metric of type '%s' (requestWCensored=%s; response=%s)", eventType, string(requestWCensored), responseData.String())
}
return nil
}