tangled
alpha
login
or
join now
openstatus.dev
/
openstatus
5
fork
atom
Openstatus
www.openstatus.dev
5
fork
atom
overview
issues
pulls
pipelines
🧪 (#1010)
authored by
Thibault Le Ouay
and committed by
GitHub
2 years ago
b1d3d9c7
e4dc94e9
+305
-11
7 changed files
expand all
collapse all
unified
split
apps
checker
handlers
checker.go
checker_test.go
handler.go
ping.go
ping_test.go
tcp.go
http_test.go
+6
-4
apps/checker/handlers/checker.go
···
8
8
9
9
"github.com/cenkalti/backoff/v4"
10
10
"github.com/gin-gonic/gin"
11
11
+
"github.com/rs/zerolog/log"
12
12
+
11
13
"github.com/openstatushq/openstatus/apps/checker"
12
14
"github.com/openstatushq/openstatus/apps/checker/pkg/assertions"
13
15
"github.com/openstatushq/openstatus/apps/checker/request"
14
14
-
"github.com/rs/zerolog/log"
15
16
)
16
17
17
18
type statusCode int
···
134
135
if err := json.Unmarshal(a, &target); err != nil {
135
136
return fmt.Errorf("unable to unmarshal IntTarget: %w", err)
136
137
}
137
137
-
isSuccessfull = isSuccessfull && target.HeaderEvaluate(data.Headers)
138
138
139
139
+
isSuccessfull = isSuccessfull && target.HeaderEvaluate(data.Headers)
139
140
case request.AssertionTextBody:
140
141
var target assertions.StringTargetType
141
142
if err := json.Unmarshal(a, &target); err != nil {
142
143
return fmt.Errorf("unable to unmarshal IntTarget: %w", err)
143
144
}
145
145
+
144
146
isSuccessfull = isSuccessfull && target.StringEvaluate(data.Body)
145
145
-
146
147
case request.AssertionStatus:
147
148
var target assertions.StatusTarget
148
149
if err := json.Unmarshal(a, &target); err != nil {
149
150
return fmt.Errorf("unable to unmarshal IntTarget: %w", err)
150
151
}
152
152
+
151
153
isSuccessfull = isSuccessfull && target.StatusEvaluate(int64(res.Status))
152
154
case request.AssertionJsonBody:
153
155
fmt.Println("assertion type", assert.AssertionType)
···
174
176
}
175
177
176
178
data.Assertions = assertionAsString
177
177
-
// That part could be refactored
179
179
+
178
180
if !isSuccessfull && req.Status == "active" {
179
181
// Q: Why here we do not check if the status was previously active?
180
182
checker.UpdateStatus(ctx, checker.UpdateData{
+108
apps/checker/handlers/checker_test.go
···
1
1
+
package handlers_test
2
2
+
3
3
+
import (
4
4
+
"encoding/json"
5
5
+
"fmt"
6
6
+
"io"
7
7
+
"net/http"
8
8
+
"net/http/httptest"
9
9
+
"strings"
10
10
+
"testing"
11
11
+
12
12
+
"github.com/gin-gonic/gin"
13
13
+
"github.com/openstatushq/openstatus/apps/checker/handlers"
14
14
+
"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird"
15
15
+
"github.com/openstatushq/openstatus/apps/checker/request"
16
16
+
"github.com/stretchr/testify/assert"
17
17
+
)
18
18
+
19
19
+
func TestHandler_HTTPCheckerHandler(t *testing.T) {
20
20
+
hclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
21
21
+
return &http.Response{
22
22
+
StatusCode: http.StatusAccepted,
23
23
+
Body: io.NopCloser(strings.NewReader(`Status Accepted`)),
24
24
+
}
25
25
+
})}
26
26
+
client := tinybird.NewClient(hclient, "apiKey")
27
27
+
28
28
+
t.Run("it should return 401 if there's no auth", func(t *testing.T) {
29
29
+
30
30
+
region := "local"
31
31
+
h := handlers.Handler{
32
32
+
TbClient: client,
33
33
+
Secret: "",
34
34
+
CloudProvider: "fly",
35
35
+
Region: region,
36
36
+
}
37
37
+
router := gin.New()
38
38
+
router.POST("/checker/:region", h.HTTPCheckerHandler)
39
39
+
40
40
+
w := httptest.NewRecorder()
41
41
+
42
42
+
data := request.HttpCheckerRequest{
43
43
+
URL: "https://www.openstatus.dev",
44
44
+
}
45
45
+
dataJson, _ := json.Marshal(data)
46
46
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
47
47
+
router.ServeHTTP(w, req)
48
48
+
49
49
+
assert.Equal(t, 401, w.Code)
50
50
+
})
51
51
+
52
52
+
t.Run("it should return 400 if the payload is not ok", func(t *testing.T) {
53
53
+
region := "local"
54
54
+
55
55
+
h := handlers.Handler{
56
56
+
TbClient: client,
57
57
+
Secret: "test",
58
58
+
CloudProvider: "fly",
59
59
+
Region: region,
60
60
+
}
61
61
+
router := gin.New()
62
62
+
router.POST("/checker/:region", h.HTTPCheckerHandler)
63
63
+
64
64
+
w := httptest.NewRecorder()
65
65
+
66
66
+
data := request.PingRequest{
67
67
+
URL: "https://www.openstatus.dev",
68
68
+
}
69
69
+
dataJson, _ := json.Marshal(data)
70
70
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
71
71
+
req.Header.Set("Authorization", "Basic test")
72
72
+
router.ServeHTTP(w, req)
73
73
+
74
74
+
assert.Equal(t, 400, w.Code)
75
75
+
assert.Contains(t, w.Body.String(), "{\"error\":\"invalid request\"}")
76
76
+
})
77
77
+
78
78
+
t.Run("it should return 200 if the payload is not ok", func(t *testing.T) {
79
79
+
region := "local"
80
80
+
81
81
+
httptest.NewRequest(http.MethodGet, "http://www.openstatus.dev", nil)
82
82
+
httptest.NewRecorder()
83
83
+
84
84
+
h := handlers.Handler{
85
85
+
TbClient: client,
86
86
+
Secret: "test",
87
87
+
CloudProvider: "fly",
88
88
+
Region: region,
89
89
+
}
90
90
+
router := gin.New()
91
91
+
router.POST("/checker/:region", h.HTTPCheckerHandler)
92
92
+
93
93
+
w := httptest.NewRecorder()
94
94
+
95
95
+
data := request.HttpCheckerRequest{
96
96
+
URL: "https://www.openstatus.dev",
97
97
+
Method: "GET",
98
98
+
Body: "",
99
99
+
}
100
100
+
dataJson, _ := json.Marshal(data)
101
101
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
102
102
+
req.Header.Set("Authorization", "Basic test")
103
103
+
router.ServeHTTP(w, req)
104
104
+
105
105
+
assert.Equal(t, 200, w.Code)
106
106
+
fmt.Println(w.Body.String())
107
107
+
})
108
108
+
}
+6
apps/checker/handlers/handler.go
···
1
1
package handlers
2
2
3
3
import (
4
4
+
"net/http"
5
5
+
4
6
"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird"
5
7
)
6
8
···
12
14
}
13
15
14
16
// Authorization could be handle by middleware
17
17
+
18
18
+
func NewHTTPClient() *http.Client {
19
19
+
return &http.Client{}
20
20
+
}
+1
apps/checker/handlers/ping.go
···
72
72
requestClient := &http.Client{
73
73
Timeout: 45 * time.Second,
74
74
}
75
75
+
75
76
defer requestClient.CloseIdleConnections()
76
77
77
78
var req request.PingRequest
+117
apps/checker/handlers/ping_test.go
···
1
1
+
package handlers_test
2
2
+
3
3
+
import (
4
4
+
"encoding/json"
5
5
+
"fmt"
6
6
+
"io"
7
7
+
"net/http"
8
8
+
"net/http/httptest"
9
9
+
"strings"
10
10
+
"testing"
11
11
+
12
12
+
"github.com/openstatushq/openstatus/apps/checker/handlers"
13
13
+
"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird"
14
14
+
"github.com/openstatushq/openstatus/apps/checker/request"
15
15
+
16
16
+
"github.com/gin-gonic/gin"
17
17
+
"github.com/stretchr/testify/assert"
18
18
+
)
19
19
+
20
20
+
type RoundTripFunc func(req *http.Request) *http.Response
21
21
+
22
22
+
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
23
23
+
return f(req), nil
24
24
+
}
25
25
+
26
26
+
func TestHandler_PingRegion(t *testing.T) {
27
27
+
28
28
+
hclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
29
29
+
return &http.Response{
30
30
+
StatusCode: http.StatusAccepted,
31
31
+
Body: io.NopCloser(strings.NewReader(`Status Accepted`)),
32
32
+
}
33
33
+
})}
34
34
+
client := tinybird.NewClient(hclient, "apiKey")
35
35
+
36
36
+
t.Run("it should return 401 if there's no auth", func(t *testing.T) {
37
37
+
38
38
+
region := "local"
39
39
+
h := handlers.Handler{
40
40
+
TbClient: client,
41
41
+
Secret: "",
42
42
+
CloudProvider: "fly",
43
43
+
Region: region,
44
44
+
}
45
45
+
router := gin.New()
46
46
+
router.POST("/checker/:region", h.PingRegionHandler)
47
47
+
48
48
+
w := httptest.NewRecorder()
49
49
+
50
50
+
data := request.PingRequest{
51
51
+
URL: "https://www.openstatus.dev",
52
52
+
}
53
53
+
dataJson, _ := json.Marshal(data)
54
54
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
55
55
+
router.ServeHTTP(w, req)
56
56
+
57
57
+
assert.Equal(t, 401, w.Code)
58
58
+
})
59
59
+
60
60
+
t.Run("it should return 400 if the payload is not ok", func(t *testing.T) {
61
61
+
region := "local"
62
62
+
63
63
+
h := handlers.Handler{
64
64
+
TbClient: client,
65
65
+
Secret: "test",
66
66
+
CloudProvider: "fly",
67
67
+
Region: region,
68
68
+
}
69
69
+
router := gin.New()
70
70
+
router.POST("/checker/:region", h.PingRegionHandler)
71
71
+
72
72
+
w := httptest.NewRecorder()
73
73
+
74
74
+
data := request.HttpCheckerRequest{
75
75
+
URL: "https://www.openstatus.dev",
76
76
+
}
77
77
+
dataJson, _ := json.Marshal(data)
78
78
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
79
79
+
req.Header.Set("Authorization", "Basic test")
80
80
+
router.ServeHTTP(w, req)
81
81
+
82
82
+
assert.Equal(t, 400, w.Code)
83
83
+
assert.Contains(t, w.Body.String(), "{\"error\":\"invalid request\"}")
84
84
+
})
85
85
+
86
86
+
t.Run("it should return 200 if the payload is ok", func(t *testing.T) {
87
87
+
region := "local"
88
88
+
89
89
+
httptest.NewRequest(http.MethodGet, "http://www.openstatus.dev", nil)
90
90
+
httptest.NewRecorder()
91
91
+
92
92
+
h := handlers.Handler{
93
93
+
TbClient: client,
94
94
+
Secret: "test",
95
95
+
CloudProvider: "fly",
96
96
+
Region: region,
97
97
+
}
98
98
+
router := gin.New()
99
99
+
router.POST("/checker/:region", h.PingRegionHandler)
100
100
+
101
101
+
w := httptest.NewRecorder()
102
102
+
103
103
+
data := request.PingRequest{
104
104
+
URL: "https://www.openstatus.dev",
105
105
+
Method: "GET",
106
106
+
Headers: map[string]string{},
107
107
+
Body: "",
108
108
+
}
109
109
+
dataJson, _ := json.Marshal(data)
110
110
+
req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson)))
111
111
+
req.Header.Set("Authorization", "Basic test")
112
112
+
router.ServeHTTP(w, req)
113
113
+
114
114
+
assert.Equal(t, 200, w.Code)
115
115
+
fmt.Println(w.Body.String())
116
116
+
})
117
117
+
}
+13
apps/checker/handlers/tcp.go
···
25
25
func (h Handler) TCPHandler(c *gin.Context) {
26
26
ctx := c.Request.Context()
27
27
dataSourceName := "tcp_response__v0"
28
28
+
28
29
if c.GetHeader("Authorization") != fmt.Sprintf("Basic %s", h.Secret) {
29
30
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
31
31
+
30
32
return
31
33
}
32
34
···
36
38
if region != "" && region != h.Region {
37
39
c.Header("fly-replay", fmt.Sprintf("region=%s", region))
38
40
c.String(http.StatusAccepted, "Forwarding request to %s", region)
41
41
+
39
42
return
40
43
}
41
44
}
45
45
+
42
46
var req request.TCPCheckerRequest
43
47
if err := c.ShouldBindJSON(&req); err != nil {
44
48
log.Ctx(ctx).Error().Err(err).Msg("failed to decode checker request")
45
49
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
50
50
+
46
51
return
47
52
}
48
53
workspaceId, err := strconv.ParseInt(req.WorkspaceID, 10, 64)
54
54
+
49
55
if err != nil {
50
56
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
57
57
+
51
58
return
52
59
}
53
60
monitorId, err := strconv.ParseInt(req.MonitorID, 10, 64)
61
61
+
54
62
if err != nil {
55
63
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
64
64
+
56
65
return
57
66
}
58
67
···
60
69
op := func() error {
61
70
called++
62
71
res, err := checker.PingTcp(int(req.Timeout), req.URL)
72
72
+
63
73
if err != nil {
64
74
return fmt.Errorf("unable to check tcp %s", err)
65
75
}
···
75
85
MonitorID: monitorId,
76
86
}
77
87
latency := res.TCPDone - res.TCPStart
88
88
+
78
89
if req.Status == "active" && req.DegradedAfter > 0 && latency > req.DegradedAfter {
79
90
checker.UpdateStatus(ctx, checker.UpdateData{
80
91
MonitorId: req.MonitorID,
···
83
94
CronTimestamp: req.CronTimestamp,
84
95
})
85
96
}
97
97
+
86
98
if req.Status == "degraded" && req.DegradedAfter > 0 && latency <= req.DegradedAfter {
87
99
checker.UpdateStatus(ctx, checker.UpdateData{
88
100
MonitorId: req.MonitorID,
···
129
141
})
130
142
}
131
143
}
144
144
+
132
145
c.JSON(http.StatusOK, gin.H{"message": "ok"})
133
146
}
134
147
+54
-7
apps/checker/http_test.go
···
1
1
package checker_test
2
2
3
3
import (
4
4
+
"bytes"
4
5
"context"
6
6
+
"io"
5
7
"net/http"
6
8
"testing"
9
9
+
10
10
+
"github.com/stretchr/testify/assert"
7
11
8
12
"github.com/openstatushq/openstatus/apps/checker"
9
13
"github.com/openstatushq/openstatus/apps/checker/request"
10
14
)
11
15
16
16
+
// RoundTripFunc .
17
17
+
type RoundTripFunc func(req *http.Request) *http.Response
18
18
+
19
19
+
// RoundTrip .
20
20
+
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
21
21
+
return f(req), nil
22
22
+
}
23
23
+
24
24
+
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
25
25
+
func NewTestClient(fn RoundTripFunc) *http.Client {
26
26
+
return &http.Client{
27
27
+
Transport: RoundTripFunc(fn),
28
28
+
}
29
29
+
}
30
30
+
12
31
func Test_ping(t *testing.T) {
32
32
+
13
33
type args struct {
14
34
client *http.Client
15
35
inputData request.HttpCheckerRequest
···
20
40
want checker.Response
21
41
wantErr bool
22
42
}{
23
23
-
{name: "200", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct {
43
43
+
{name: "200", args: args{client: NewTestClient(func(req *http.Request) *http.Response {
44
44
+
return &http.Response{
45
45
+
StatusCode: http.StatusOK,
46
46
+
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
47
47
+
Header: make(http.Header),
48
48
+
}
49
49
+
}), inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct {
24
50
Key string `json:"key"`
25
51
Value string `json:"value"`
26
26
-
}{{Key: "", Value: ""}}}}, want: checker.Response{Status: 200}, wantErr: false},
27
27
-
{name: "200", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct {
52
52
+
}{{Key: "", Value: ""}}}}, want: checker.Response{Status: 200, Body: "OK"}, wantErr: false},
53
53
+
54
54
+
{name: "200 with headers", args: args{client: NewTestClient(func(req *http.Request) *http.Response {
55
55
+
assert.Equal(t, "Value", req.Header.Get("Test"))
56
56
+
return &http.Response{
57
57
+
StatusCode: http.StatusOK,
58
58
+
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
59
59
+
Header: make(http.Header),
60
60
+
}
61
61
+
}), inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct {
28
62
Key string `json:"key"`
29
63
Value string `json:"value"`
30
30
-
}{{Key: "Test", Value: ""}}}}, want: checker.Response{Status: 200}, wantErr: false},
31
31
-
{name: "500", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us/500", CronTimestamp: 1}}, want: checker.Response{Status: 500}, wantErr: false},
32
32
-
{name: "500", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://somethingthatwillfail.ed", CronTimestamp: 1}}, want: checker.Response{Status: 0}, wantErr: true},
64
64
+
}{{Key: "Test", Value: "Value"}}}}, want: checker.Response{Status: 200, Body: "OK"}, wantErr: false},
33
65
34
34
-
// TODO: Add test cases.
66
66
+
{name: "500", args: args{client: NewTestClient(func(req *http.Request) *http.Response {
67
67
+
return &http.Response{
68
68
+
StatusCode: http.StatusInternalServerError,
69
69
+
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
70
70
+
Header: make(http.Header),
71
71
+
}
72
72
+
}), inputData: request.HttpCheckerRequest{URL: "https://openstat.us/500", CronTimestamp: 1}},
73
73
+
want: checker.Response{Status: 500, Body: "OK"}, wantErr: false},
74
74
+
75
75
+
{name: "Wrong url should return an error", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://somethingthatwillfail.ed", CronTimestamp: 1}},
76
76
+
want: checker.Response{Status: 0}, wantErr: true},
35
77
}
36
78
for _, tt := range tests {
37
79
t.Run(tt.name, func(t *testing.T) {
···
41
83
t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr)
42
84
return
43
85
}
86
86
+
44
87
if got.Status != tt.want.Status {
88
88
+
t.Errorf("Ping() = %v, want %v", got, tt.want)
89
89
+
}
90
90
+
91
91
+
if got.Body != tt.want.Body {
45
92
t.Errorf("Ping() = %v, want %v", got, tt.want)
46
93
}
47
94
})