Skip to content

Commit

Permalink
Merge pull request #114 from NodeFactoryIo/mmuftic/payout-script
Browse files Browse the repository at this point in the history
Expose stats endpoints
  • Loading branch information
mpetrunic authored Nov 3, 2020
2 parents 51d3e73 + 2c8a78a commit 209deb0
Show file tree
Hide file tree
Showing 23 changed files with 1,700 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Check if port range valid [\#104](https://github.com/NodeFactoryIo/vedran/pull/104) ([mpetrun5](https://github.com/mpetrun5))
- Valid flag on node [\#105](https://github.com/NodeFactoryIo/vedran/pull/105) ([mpetrun5](https://github.com/mpetrun5))
- Passing SSL certificates [\#112](https://github.com/NodeFactoryIo/vedran/pull/112) ([mpetrun5](https://github.com/mpetrun5))
- Expose stats endpoints [\#114](https://github.com/NodeFactoryIo/vedran/pull/114) ([MakMuftic](https://github.com/MakMuftic))

### Fix

Expand Down
7 changes: 3 additions & 4 deletions internal/controllers/ping.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"github.com/NodeFactoryIo/vedran/internal/stats"
"math"
"net/http"

Expand All @@ -9,9 +10,7 @@ import (
log "github.com/sirupsen/logrus"
)

const (
pingIntervalSeconds = 10
)
const pingOffset = 5

func (c ApiController) PingHandler(w http.ResponseWriter, r *http.Request) {
request := r.Context().Value(auth.RequestContextKey).(*auth.RequestContext)
Expand All @@ -21,7 +20,7 @@ func (c ApiController) PingHandler(w http.ResponseWriter, r *http.Request) {
log.Errorf("Unable to calculate node downtime, error: %v", err)
}

if math.Abs(downtimeDuration.Seconds()) > pingIntervalSeconds {
if math.Abs(downtimeDuration.Seconds()) > (stats.PingIntervalInSeconds + pingOffset) {
downtime := models.Downtime{
Start: lastPingTime,
End: request.Timestamp,
Expand Down
8 changes: 4 additions & 4 deletions internal/controllers/ping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestApiController_PingHandler(t *testing.T) {
calculateDowntimeErr: nil,
},
{
name: "Saves downtime if downtime duration more than 10 seconds",
name: "Saves downtime if downtime duration more than 5 seconds",
statusCode: 200,
pingSaveCallCount: 1,
pingSaveErr: nil,
Expand All @@ -65,17 +65,17 @@ func TestApiController_PingHandler(t *testing.T) {
pingSaveErr: fmt.Errorf("ERROR"),
downtimeSaveErr: nil,
downtimeSaveCallCount: 0,
downtimeDuration: time.Duration(time.Second * 9),
downtimeDuration: time.Duration(time.Second * 8),
calculateDowntimeErr: nil,
},
{
name: "Returns 200 and does not save downtime if downtime duration less than 10 seconds",
name: "Returns 200 and does not save downtime if downtime duration less than 5 + 5 seconds",
statusCode: 200,
pingSaveCallCount: 1,
pingSaveErr: nil,
downtimeSaveErr: nil,
downtimeSaveCallCount: 0,
downtimeDuration: time.Duration(time.Second * 9),
downtimeDuration: time.Duration(time.Second * 8),
calculateDowntimeErr: nil,
},
}
Expand Down
52 changes: 52 additions & 0 deletions internal/controllers/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package controllers

import (
"encoding/json"
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/NodeFactoryIo/vedran/internal/stats"
muxhelpper "github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"net/http"
)

type StatsResponse struct {
Stats map[string]models.NodeStatsDetails `json:"stats"`
}

func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http.Request) {
statisticsForPayout, err := stats.CalculateStatisticsFromLastPayout(c.repositories)
if err != nil {
log.Errorf("Failed to calculate statistics, because %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(StatsResponse{
Stats: statisticsForPayout,
})
}

func (c *ApiController) StatisticsHandlerStatsForNode(w http.ResponseWriter, r *http.Request) {
vars := muxhelpper.Vars(r)
nodeId, ok := vars["id"]
if !ok || len(nodeId) < 1 {
log.Error("Missing URL parameter node id")
http.NotFound(w, r)
return
}

nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId)
if err != nil {
log.Errorf("Failed to calculate statistics for node %s, because %v", nodeId, err)
if err.Error() == "not found" {
http.NotFound(w, r)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(nodeStatisticsFromLastPayout)
}
264 changes: 264 additions & 0 deletions internal/controllers/stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package controllers

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/NodeFactoryIo/vedran/internal/repositories"
mocks "github.com/NodeFactoryIo/vedran/mocks/repositories"
muxhelpper "github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestApiController_StatisticsHandlerAllStats(t *testing.T) {
now := time.Now()
tests := []struct {
name string
httpStatus int
nodeId string
// NodeRepo.GetAll
nodeRepoGetAllReturns *[]models.Node
nodeRepoGetAllError error
// RecordRepo.FindSuccessfulRecordsInsideInterval
recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record
recordRepoFindSuccessfulRecordsInsideIntervalError error
// DowntimeRepo.FindDowntimesInsideInterval
downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime
downtimeRepoFindDowntimesInsideIntervalError error
// PingRepo.CalculateDowntime
pingRepoCalculateDowntimeReturnDuration time.Duration
pingRepoCalculateDowntimeError error
// PayoutRepo.FindLatestPayout
payoutRepoFindLatestPayoutReturns *models.Payout
payoutRepoFindLatestPayoutError error
// Stats
nodeNumberOfPings float64
nodeNumberOfRequests float64
}{
{
name: "get valid stats",
nodeId: "1",
httpStatus: http.StatusOK,
// NodeRepo.GetAll
nodeRepoGetAllReturns: &[]models.Node{
{
ID: "1",
},
},
nodeRepoGetAllError: nil,
// RecordRepo.FindSuccessfulRecordsInsideInterval
recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil,
recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"),
// DowntimeRepo.FindDowntimesInsideInterval
downtimeRepoFindDowntimesInsideIntervalReturns: nil,
downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"),
// PingRepo.CalculateDowntime
pingRepoCalculateDowntimeReturnDuration: 5 * time.Second,
pingRepoCalculateDowntimeError: nil,
// PayoutRepo.FindLatestPayout
payoutRepoFindLatestPayoutReturns: &models.Payout{
Timestamp: now.Add(-24 * time.Hour),
PaymentDetails: nil,
},
payoutRepoFindLatestPayoutError: nil,
// Stats
nodeNumberOfRequests: float64(0),
nodeNumberOfPings: float64(8640),
},
{
name: "unable to get latest interval, server error",
httpStatus: http.StatusInternalServerError,
payoutRepoFindLatestPayoutError: errors.New("db-error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create mock controller
nodeRepoMock := mocks.NodeRepository{}
nodeRepoMock.On("GetAll").Return(
test.nodeRepoGetAllReturns, test.nodeRepoGetAllError,
)
recordRepoMock := mocks.RecordRepository{}
recordRepoMock.On("FindSuccessfulRecordsInsideInterval",
test.nodeId, mock.Anything, mock.Anything,
).Return(
test.recordRepoFindSuccessfulRecordsInsideIntervalReturns,
test.recordRepoFindSuccessfulRecordsInsideIntervalError,
)
metricsRepoMock := mocks.MetricsRepository{}
pingRepoMock := mocks.PingRepository{}
pingRepoMock.On("CalculateDowntime",
test.nodeId, mock.Anything,
).Return(
time.Now(),
test.pingRepoCalculateDowntimeReturnDuration,
test.pingRepoCalculateDowntimeError,
)
downtimeRepoMock := mocks.DowntimeRepository{}
downtimeRepoMock.On("FindDowntimesInsideInterval",
test.nodeId, mock.Anything, mock.Anything,
).Return(
test.downtimeRepoFindDowntimesInsideIntervalReturns,
test.downtimeRepoFindDowntimesInsideIntervalError,
)
payoutRepoMock := mocks.PayoutRepository{}
payoutRepoMock.On("FindLatestPayout").Return(
test.payoutRepoFindLatestPayoutReturns,
test.payoutRepoFindLatestPayoutError,
)
apiController := NewApiController(false, repositories.Repos{
NodeRepo: &nodeRepoMock,
PingRepo: &pingRepoMock,
MetricsRepo: &metricsRepoMock,
RecordRepo: &recordRepoMock,
DowntimeRepo: &downtimeRepoMock,
PayoutRepo: &payoutRepoMock,
}, nil)
handler := http.HandlerFunc(apiController.StatisticsHandlerAllStats)
req, _ := http.NewRequest("GET", "/api/v1/stats", bytes.NewReader(nil))
rr := httptest.NewRecorder()

// invoke test request
handler.ServeHTTP(rr, req)

// asserts
assert.Equal(t, test.httpStatus, rr.Code, fmt.Sprintf("Response status code should be %d", test.httpStatus))

var statsResponse StatsResponse
if rr.Code == http.StatusOK {
_ = json.Unmarshal(rr.Body.Bytes(), &statsResponse)
assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].TotalPings)
assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].TotalRequests)
}
})
}
}

func TestApiController_StatisticsHandlerStatsForNode(t *testing.T) {
now := time.Now()
tests := []struct {
name string
httpStatus int
nodeId string
contextKey string
// RecordRepo.FindSuccessfulRecordsInsideInterval
recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record
recordRepoFindSuccessfulRecordsInsideIntervalError error
// DowntimeRepo.FindDowntimesInsideInterval
downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime
downtimeRepoFindDowntimesInsideIntervalError error
// PingRepo.CalculateDowntime
pingRepoCalculateDowntimeReturnDuration time.Duration
pingRepoCalculateDowntimeError error
// PayoutRepo.FindLatestPayout
payoutRepoFindLatestPayoutReturns *models.Payout
payoutRepoFindLatestPayoutError error
// Stats
nodeNumberOfPings float64
nodeNumberOfRequests float64
}{
{
name: "get valid stats",
nodeId: "1",
httpStatus: http.StatusOK,
contextKey: "id",
// RecordRepo.FindSuccessfulRecordsInsideInterval
recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil,
recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"),
// DowntimeRepo.FindDowntimesInsideInterval
downtimeRepoFindDowntimesInsideIntervalReturns: nil,
downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"),
// PingRepo.CalculateDowntime
pingRepoCalculateDowntimeReturnDuration: 5 * time.Second,
pingRepoCalculateDowntimeError: nil,
// PayoutRepo.FindLatestPayout
payoutRepoFindLatestPayoutReturns: &models.Payout{
Timestamp: now.Add(-24 * time.Hour),
PaymentDetails: nil,
},
payoutRepoFindLatestPayoutError: nil,
// Stats
nodeNumberOfRequests: float64(0),
nodeNumberOfPings: float64(8640),
},
{
name: "unable to get latest interval, server error",
httpStatus: http.StatusInternalServerError,
contextKey: "id",
payoutRepoFindLatestPayoutError: errors.New("db-error"),
},
{
name: "unable to find id from request, server error",
httpStatus: http.StatusNotFound,
contextKey: "id",
payoutRepoFindLatestPayoutError: errors.New("not found"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create mock controller
recordRepoMock := mocks.RecordRepository{}
recordRepoMock.On("FindSuccessfulRecordsInsideInterval",
test.nodeId, mock.Anything, mock.Anything,
).Return(
test.recordRepoFindSuccessfulRecordsInsideIntervalReturns,
test.recordRepoFindSuccessfulRecordsInsideIntervalError,
)
metricsRepoMock := mocks.MetricsRepository{}
pingRepoMock := mocks.PingRepository{}
pingRepoMock.On("CalculateDowntime",
test.nodeId, mock.Anything,
).Return(
time.Now(),
test.pingRepoCalculateDowntimeReturnDuration,
test.pingRepoCalculateDowntimeError,
)
downtimeRepoMock := mocks.DowntimeRepository{}
downtimeRepoMock.On("FindDowntimesInsideInterval",
test.nodeId, mock.Anything, mock.Anything,
).Return(
test.downtimeRepoFindDowntimesInsideIntervalReturns,
test.downtimeRepoFindDowntimesInsideIntervalError,
)
payoutRepoMock := mocks.PayoutRepository{}
payoutRepoMock.On("FindLatestPayout").Return(
test.payoutRepoFindLatestPayoutReturns,
test.payoutRepoFindLatestPayoutError,
)
apiController := NewApiController(false, repositories.Repos{
PingRepo: &pingRepoMock,
MetricsRepo: &metricsRepoMock,
RecordRepo: &recordRepoMock,
DowntimeRepo: &downtimeRepoMock,
PayoutRepo: &payoutRepoMock,
}, nil)
type ContextKey string
req, _ := http.NewRequest("GET", "/api/v1/stats/node/1", bytes.NewReader(nil))
req = req.WithContext(context.WithValue(req.Context(), ContextKey(test.contextKey), "1"))
rr := httptest.NewRecorder()

// invoke test request
router := muxhelpper.NewRouter()
router.HandleFunc("/api/v1/stats/node/{id}", apiController.StatisticsHandlerStatsForNode)
router.ServeHTTP(rr, req)

// asserts
assert.Equal(t, test.httpStatus, rr.Code, fmt.Sprintf("Response status code should be %d", test.httpStatus))

var statsResponse models.NodeStatsDetails
if rr.Code == http.StatusOK {
_ = json.Unmarshal(rr.Body.Bytes(), &statsResponse)
assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.TotalPings)
assert.Equal(t, test.nodeNumberOfRequests, statsResponse.TotalRequests)
}
})
}
}
1 change: 1 addition & 0 deletions internal/loadbalancer/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func StartLoadBalancerServer(props configuration.Configuration) {
repos.RecordRepo = repositories.NewRecordRepo(database)
repos.NodeRepo = repositories.NewNodeRepo(database)
repos.DowntimeRepo = repositories.NewDowntimeRepo(database)
repos.PayoutRepo = repositories.NewPayoutRepo(database)
err = repos.PingRepo.ResetAllPings()
if err != nil {
log.Fatalf("Failed reseting pings because of: %v", err)
Expand Down
Loading

0 comments on commit 209deb0

Please sign in to comment.