Skip to content
This repository was archived by the owner on Feb 1, 2024. It is now read-only.

Commit c6374c3

Browse files
Le Cheng Fannikhilsaraf
authored andcommitted
Add health check listener, closes #42
1 parent d0990ec commit c6374c3

13 files changed

Lines changed: 297 additions & 4 deletions

File tree

cmd/trade.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/lightyeario/kelp/model"
1212
"github.com/lightyeario/kelp/plugins"
1313
"github.com/lightyeario/kelp/support/monitoring"
14+
"github.com/lightyeario/kelp/support/networking"
1415
"github.com/lightyeario/kelp/support/utils"
1516
"github.com/lightyeario/kelp/trader"
1617
"github.com/spf13/cobra"
@@ -119,6 +120,7 @@ func init() {
119120
// we want to delete all the offers and exit here since there is something wrong with our setup
120121
deleteAllOffersAndExit(botConfig, client, sdex)
121122
}
123+
122124
bot := trader.MakeBot(
123125
client,
124126
botConfig.AssetBase(),
@@ -136,6 +138,22 @@ func init() {
136138
validateTrustlines(client, &botConfig)
137139
log.Printf("trustlines valid\n")
138140

141+
// --- start initialization of services ---
142+
if botConfig.MonitoringPort != 0 {
143+
go func() {
144+
e := startMonitoringServer(botConfig)
145+
if e != nil {
146+
log.Println()
147+
log.Printf("unable to start the monitoring server or problem encountered while running server: %s\n", e)
148+
// we want to delete all the offers and exit here because we don't want the bot to run if monitoring isn't working
149+
// if monitoring is desired but not working properly, we want the bot to be shut down and guarantee that there
150+
// aren't outstanding offers.
151+
deleteAllOffersAndExit(botConfig, client, sdex)
152+
}
153+
}()
154+
}
155+
// --- end initialization of services ---
156+
139157
log.Println("Starting the trader bot...")
140158
for {
141159
bot.Start()
@@ -144,6 +162,26 @@ func init() {
144162
}
145163
}
146164

165+
func startMonitoringServer(botConfig trader.BotConfig) error {
166+
healthMetrics, e := monitoring.MakeMetricsRecorder(map[string]interface{}{"success": true})
167+
if e != nil {
168+
return fmt.Errorf("unable to make metrics recorder for the health endpoint: %s", e)
169+
}
170+
171+
healthEndpoint, e := monitoring.MakeMetricsEndpoint("/health", healthMetrics, networking.NoAuth)
172+
if e != nil {
173+
return fmt.Errorf("unable to make /health endpoint: %s", e)
174+
}
175+
176+
server, e := networking.MakeServer([]networking.Endpoint{healthEndpoint})
177+
if e != nil {
178+
return fmt.Errorf("unable to initialize the metrics server: %s", e)
179+
}
180+
181+
log.Printf("Starting monitoring server on port %d\n", botConfig.MonitoringPort)
182+
return server.StartServer(botConfig.MonitoringPort, "", "")
183+
}
184+
147185
func validateTrustlines(client *horizon.Client, botConfig *trader.BotConfig) {
148186
account, e := client.LoadAccount(botConfig.TradingAccount())
149187
if e != nil {

examples/configs/trader/sample_trader.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ ISSUER_B="GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI"
1919
TICK_INTERVAL_SECONDS=300
2020
# the url for your horizon instance. If this url contains the string "test" then the bot assumes it is using the test network.
2121
HORIZON_URL="https://horizon-testnet.stellar.org"
22+
23+
# the port that the monitoring server should run on. Uncomment the following line to add monitoring server.
24+
# MONITORING_PORT=8081

glide.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ import:
1717
- support/errors
1818
- package: github.com/PagerDuty/go-pagerduty
1919
version: 635c5ce271490fba94880e62cde4eea3c1c184b9
20+
- package: github.com/go-chi/chi
21+
version: 0ebf7795c516423a110473652e9ba3a59a504863

support/monitoring/factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ func MakeAlert(alertType string, apiKey string) (api.Alert, error) {
2222
default:
2323
return &noopAlert{}, nil
2424
}
25-
}
25+
}

support/monitoring/metrics.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package monitoring
2+
3+
// Metrics is an interface that allows a client to pass in key value pairs (keys must be strings)
4+
// and it can dump the metrics as JSON.
5+
type Metrics interface {
6+
UpdateMetrics(metrics map[string]interface{})
7+
MarshalJSON() ([]byte, error)
8+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package monitoring
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"strings"
8+
9+
"github.com/lightyeario/kelp/support/networking"
10+
)
11+
12+
// metricsEndpoint represents a monitoring API endpoint that always responds with a JSON
13+
// encoding of the provided metrics. The auth level for the endpoint can be NoAuth (public access)
14+
// or GoogleAuth which uses a Google account for authorization.
15+
type metricsEndpoint struct {
16+
path string
17+
metrics Metrics
18+
authLevel networking.AuthLevel
19+
}
20+
21+
// MakeMetricsEndpoint creates an Endpoint for the monitoring server with the desired auth level.
22+
// The endpoint's response is always a JSON dump of the provided metrics.
23+
func MakeMetricsEndpoint(path string, metrics Metrics, authLevel networking.AuthLevel) (networking.Endpoint, error) {
24+
if !strings.HasPrefix(path, "/") {
25+
return nil, fmt.Errorf("endpoint path must begin with /")
26+
}
27+
s := &metricsEndpoint{
28+
path: path,
29+
metrics: metrics,
30+
authLevel: authLevel,
31+
}
32+
return s, nil
33+
}
34+
35+
func (m *metricsEndpoint) GetAuthLevel() networking.AuthLevel {
36+
return m.authLevel
37+
}
38+
39+
func (m *metricsEndpoint) GetPath() string {
40+
return m.path
41+
}
42+
43+
// GetHandlerFunc returns a HandlerFunc that writes the JSON representation of the metrics
44+
// that's passed into the endpoint.
45+
func (m *metricsEndpoint) GetHandlerFunc() http.HandlerFunc {
46+
return func(w http.ResponseWriter, r *http.Request) {
47+
json, e := m.metrics.MarshalJSON()
48+
if e != nil {
49+
log.Printf("error marshalling metrics json: %s\n", e)
50+
http.Error(w, e.Error(), 500)
51+
return
52+
}
53+
w.WriteHeader(200)
54+
w.Header().Set("Content-Type", "application/json")
55+
_, e = w.Write(json)
56+
if e != nil {
57+
log.Printf("error writing to the response writer: %s\n", e)
58+
}
59+
}
60+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package monitoring
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/lightyeario/kelp/support/networking"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestMetricsEndpoint_NoAuthEndpoint(t *testing.T) {
13+
testMetrics, e := MakeMetricsRecorder(map[string]interface{}{"this is a test message": true})
14+
if !assert.Nil(t, e) {
15+
return
16+
}
17+
testEndpoint, e := MakeMetricsEndpoint("/test", testMetrics, networking.NoAuth)
18+
if !assert.Nil(t, e) {
19+
return
20+
}
21+
22+
req, e := http.NewRequest("GET", "/test", nil)
23+
if !assert.Nil(t, e) {
24+
return
25+
}
26+
w := httptest.NewRecorder()
27+
testEndpoint.GetHandlerFunc().ServeHTTP(w, req)
28+
assert.Equal(t, 200, w.Code)
29+
assert.Equal(t, "{\"this is a test message\":true}", w.Body.String())
30+
31+
// Mutate the metrics and test if the server response changes
32+
testMetrics.UpdateMetrics(map[string]interface{}{"this is a test message": false})
33+
34+
w = httptest.NewRecorder()
35+
req = httptest.NewRequest("GET", "/test", nil)
36+
testEndpoint.GetHandlerFunc().ServeHTTP(w, req)
37+
assert.Equal(t, 200, w.Code)
38+
assert.Equal(t, "{\"this is a test message\":false}", w.Body.String())
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package monitoring
2+
3+
import "encoding/json"
4+
5+
// MetricsRecorder uses a map to store metrics and implements the api.Metrics interface.
6+
type metricsRecorder struct {
7+
records map[string]interface{}
8+
}
9+
10+
var _ Metrics = &metricsRecorder{}
11+
12+
// MakeMetricsRecorder makes a metrics recorder with the records map as the underlying map. If records
13+
// is nil, then an empty map will be initialized for you.
14+
func MakeMetricsRecorder(records map[string]interface{}) (Metrics, error) {
15+
if records == nil {
16+
return &metricsRecorder{
17+
records: map[string]interface{}{},
18+
}, nil
19+
}
20+
return &metricsRecorder{
21+
records: records,
22+
}, nil
23+
}
24+
25+
// UpdateMetrics updates (or adds if non-existent) metrics in the records for all key-value
26+
// pairs in the provided map of metrics.
27+
func (m *metricsRecorder) UpdateMetrics(metrics map[string]interface{}) {
28+
for k, v := range metrics {
29+
m.records[k] = v
30+
}
31+
}
32+
33+
// MarshalJSON gives the JSON representation of the records.
34+
func (m *metricsRecorder) MarshalJSON() ([]byte, error) {
35+
return json.Marshal(m.records)
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package monitoring
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestMetricsRecorder_UpdateMetrics(t *testing.T) {
10+
m := &metricsRecorder{
11+
records: map[string]interface{}{},
12+
}
13+
m.UpdateMetrics(map[string]interface{}{
14+
"max_qty": 100,
15+
"volume": 200000,
16+
"kelp_version": "1.1",
17+
})
18+
assert.Equal(t, 100, m.records["max_qty"])
19+
assert.Equal(t, 200000, m.records["volume"])
20+
assert.Equal(t, "1.1", m.records["kelp_version"])
21+
assert.Equal(t, nil, m.records["nonexistent"])
22+
m.UpdateMetrics(map[string]interface{}{
23+
"max_qty": 200,
24+
})
25+
assert.Equal(t, 200, m.records["max_qty"])
26+
}
27+
28+
func TestMetricsRecorder_MarshalJSON(t *testing.T) {
29+
m := &metricsRecorder{
30+
records: map[string]interface{}{},
31+
}
32+
m.UpdateMetrics(map[string]interface{}{
33+
"statuses": map[string]string{
34+
"a": "ok",
35+
"b": "error",
36+
},
37+
"trade_ids": []int64{1, 2, 3, 4, 5},
38+
"version": "10.0.1",
39+
})
40+
json, e := m.MarshalJSON()
41+
if !assert.Nil(t, e) {
42+
return
43+
}
44+
assert.Equal(t, `{"statuses":{"a":"ok","b":"error"},"trade_ids":[1,2,3,4,5],"version":"10.0.1"}`, string(json))
45+
}

0 commit comments

Comments
 (0)