From e2bf4848145e10ff56a0ef536abe9de2dd11a5cc Mon Sep 17 00:00:00 2001 From: Artem Poltorzhitskiy Date: Sat, 13 Jul 2024 17:39:09 +0200 Subject: [PATCH] Feature: add rollup stats for last hours (#247) --- cmd/api/docs/swagger.json | 71 +++++++++++++++++++++++++ cmd/api/handler/responses/stats.go | 20 +++++++ cmd/api/handler/stats.go | 26 +++++++++ cmd/api/handler/stats_test.go | 36 +++++++++++++ cmd/api/init.go | 1 + cmd/api/routes_test.go | 1 + internal/storage/mock/stats.go | 39 ++++++++++++++ internal/storage/postgres/stats.go | 22 ++++++++ internal/storage/postgres/stats_test.go | 9 ++++ internal/storage/stats.go | 10 ++++ internal/storage/views.go | 3 ++ 11 files changed, 238 insertions(+) diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 9814d45a..fdd1986b 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -3353,6 +3353,42 @@ } } }, + "/stats/rollup_stats_24h": { + "get": { + "description": "Get rollups stats for last 24 hours", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Get rollups stats for last 24 hours", + "operationId": "stats-rollup-24h", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.RollupStats24h" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/stats/series/{name}/{timeframe}": { "get": { "description": "Get histogram with precomputed stats by series name and timeframe", @@ -5842,6 +5878,41 @@ } } }, + "responses.RollupStats24h": { + "type": "object", + "properties": { + "blobs_count": { + "type": "integer", + "format": "integer", + "example": 123 + }, + "fee": { + "type": "integer", + "format": "integer", + "example": 123 + }, + "id": { + "type": "integer", + "format": "integer", + "example": 321 + }, + "logo": { + "type": "string", + "format": "string", + "example": "https://some_link.com/image.png" + }, + "name": { + "type": "string", + "format": "string", + "example": "Rollup name" + }, + "size": { + "type": "integer", + "format": "integer", + "example": 123 + } + } + }, "responses.RollupWithStats": { "type": "object", "properties": { diff --git a/cmd/api/handler/responses/stats.go b/cmd/api/handler/responses/stats.go index 8a1418f0..cd6971b5 100644 --- a/cmd/api/handler/responses/stats.go +++ b/cmd/api/handler/responses/stats.go @@ -131,3 +131,23 @@ func NewSquareSizeResponse(m map[int][]storage.SeriesItem) SquareSizeResponse { } return response } + +type RollupStats24h struct { + Id int64 `example:"321" format:"integer" json:"id,omitempty" swaggertype:"integer"` + Name string `example:"Rollup name" format:"string" json:"name,omitempty" swaggertype:"string"` + Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` + Size int64 `example:"123" format:"integer" json:"size" swaggertype:"integer"` + Fee int64 `example:"123" format:"integer" json:"fee" swaggertype:"integer"` + BlobsCount int64 `example:"123" format:"integer" json:"blobs_count" swaggertype:"integer"` +} + +func NewRollupStats24h(stats storage.RollupStats24h) RollupStats24h { + return RollupStats24h{ + Id: stats.RollupId, + Name: stats.Name, + Logo: stats.Logo, + Size: stats.Size, + Fee: stats.Fee, + BlobsCount: stats.BlobsCount, + } +} diff --git a/cmd/api/handler/stats.go b/cmd/api/handler/stats.go index 6c8689e1..770dedc3 100644 --- a/cmd/api/handler/stats.go +++ b/cmd/api/handler/stats.go @@ -493,3 +493,29 @@ func (sh StatsHandler) SquareSize(c echo.Context) error { return c.JSON(http.StatusOK, responses.NewSquareSizeResponse(histogram)) } + +// RollupStats24h godoc +// +// @Summary Get rollups stats for last 24 hours +// @Description Get rollups stats for last 24 hours +// @Tags stats +// @ID stats-rollup-24h +// @Produce json +// @Success 200 {array} responses.RollupStats24h +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /stats/rollup_stats_24h [get] +func (sh StatsHandler) RollupStats24h(c echo.Context) error { + items, err := sh.repo.RollupStats24h( + c.Request().Context(), + ) + if err != nil { + return handleError(c, err, sh.nsRepo) + } + + response := make([]responses.RollupStats24h, len(items)) + for i := range items { + response[i] = responses.NewRollupStats24h(items[i]) + } + return returnArray(c, response) +} diff --git a/cmd/api/handler/stats_test.go b/cmd/api/handler/stats_test.go index a55ab614..897f89da 100644 --- a/cmd/api/handler/stats_test.go +++ b/cmd/api/handler/stats_test.go @@ -519,3 +519,39 @@ func (s *StatsTestSuite) TestCumulativeSeries() { } } } + +func (s *StatsTestSuite) TestRollupStats24h() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/v1/stats/rollup_stats_24h") + + s.stats.EXPECT(). + RollupStats24h(gomock.Any()). + Return([]storage.RollupStats24h{ + { + Name: "name", + Logo: "logo", + RollupId: 1, + Size: 12, + Fee: 43, + BlobsCount: 123, + }, + }, nil) + + s.Require().NoError(s.handler.RollupStats24h(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var response []responses.RollupStats24h + err := json.NewDecoder(rec.Body).Decode(&response) + s.Require().NoError(err) + s.Require().Len(response, 1) + + item := response[0] + s.Require().EqualValues(1, item.Id) + s.Require().EqualValues(12, item.Size) + s.Require().EqualValues(43, item.Fee) + s.Require().EqualValues(123, item.BlobsCount) + s.Require().EqualValues("name", item.Name) + s.Require().EqualValues("logo", item.Logo) +} diff --git a/cmd/api/init.go b/cmd/api/init.go index c518c0be..378c909a 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -391,6 +391,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto stats.GET("/summary/:table/:function", statsHandler.Summary) stats.GET("/tps", statsHandler.TPS) stats.GET("/changes_24h", statsHandler.Change24hBlockStats) + stats.GET("/rollup_stats_24h", statsHandler.RollupStats24h) stats.GET("/square_size", statsHandler.SquareSize) price := stats.Group("/price") diff --git a/cmd/api/routes_test.go b/cmd/api/routes_test.go index 8b66d1b8..f016a57c 100644 --- a/cmd/api/routes_test.go +++ b/cmd/api/routes_test.go @@ -98,6 +98,7 @@ func TestRoutes(t *testing.T) { "/v1/rollup/:id/export GET": {}, "/v1/docs GET": {}, "/v1/stats/square_size GET": {}, + "/v1/stats/rollup_stats_24h GET": {}, } ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/storage/mock/stats.go b/internal/storage/mock/stats.go index 59d7b7ba..0221685a 100644 --- a/internal/storage/mock/stats.go +++ b/internal/storage/mock/stats.go @@ -199,6 +199,45 @@ func (c *IStatsNamespaceSeriesCall) DoAndReturn(f func(context.Context, storage. return c } +// RollupStats24h mocks base method. +func (m *MockIStats) RollupStats24h(ctx context.Context) ([]storage.RollupStats24h, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RollupStats24h", ctx) + ret0, _ := ret[0].([]storage.RollupStats24h) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RollupStats24h indicates an expected call of RollupStats24h. +func (mr *MockIStatsMockRecorder) RollupStats24h(ctx any) *IStatsRollupStats24hCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollupStats24h", reflect.TypeOf((*MockIStats)(nil).RollupStats24h), ctx) + return &IStatsRollupStats24hCall{Call: call} +} + +// IStatsRollupStats24hCall wrap *gomock.Call +type IStatsRollupStats24hCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *IStatsRollupStats24hCall) Return(arg0 []storage.RollupStats24h, arg1 error) *IStatsRollupStats24hCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *IStatsRollupStats24hCall) Do(f func(context.Context) ([]storage.RollupStats24h, error)) *IStatsRollupStats24hCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *IStatsRollupStats24hCall) DoAndReturn(f func(context.Context) ([]storage.RollupStats24h, error)) *IStatsRollupStats24hCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Series mocks base method. func (m *MockIStats) Series(ctx context.Context, timeframe storage.Timeframe, name string, req storage.SeriesRequest) ([]storage.SeriesItem, error) { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/stats.go b/internal/storage/postgres/stats.go index 48950cb8..d19b3d4b 100644 --- a/internal/storage/postgres/stats.go +++ b/internal/storage/postgres/stats.go @@ -378,3 +378,25 @@ func (s Stats) SquareSize(ctx context.Context, from, to *time.Time) (result map[ return } + +func (s Stats) RollupStats24h(ctx context.Context) (response []storage.RollupStats24h, err error) { + inner := s.db.DB().NewSelect(). + Table(storage.ViewRollupStatsByHour). + Column("namespace_id", "signer_id", "size", "fee", "blobs_count"). + Where("time > now() - '1 day'::interval") + + joined := s.db.DB().NewSelect(). + TableExpr("(?) as data", inner). + ColumnExpr("rollup_id, sum(data.size) as size, sum(data.fee) as fee, sum(data.blobs_count) as blobs_count"). + Join("left join rollup_provider as rp on rp.address_id = data.signer_id AND (rp.namespace_id = data.namespace_id OR rp.namespace_id = 0)"). + Group("rollup_id") + + err = s.db.DB().NewSelect(). + TableExpr("(?) as grouped", joined). + ColumnExpr("grouped.*, r.name, r.logo"). + Join("left join rollup as r on r.id = grouped.rollup_id"). + Order("blobs_count desc"). + Scan(ctx, &response) + + return +} diff --git a/internal/storage/postgres/stats_test.go b/internal/storage/postgres/stats_test.go index 1e1120ec..63c14acc 100644 --- a/internal/storage/postgres/stats_test.go +++ b/internal/storage/postgres/stats_test.go @@ -287,6 +287,15 @@ func (s *StatsTestSuite) TestSquareSize() { s.Require().Len(items, 1) } +func (s *StatsTestSuite) TestRollupStats24h() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + stats, err := s.storage.Stats.RollupStats24h(ctx) + s.Require().NoError(err) + s.Require().Len(stats, 0) +} + func TestSuiteStats_Run(t *testing.T) { suite.Run(t, new(StatsTestSuite)) } diff --git a/internal/storage/stats.go b/internal/storage/stats.go index 0cb3f6a3..45b2b89b 100644 --- a/internal/storage/stats.go +++ b/internal/storage/stats.go @@ -123,6 +123,15 @@ func NewSeriesRequest(from, to int64) SeriesRequest { return seriesRequest } +type RollupStats24h struct { + RollupId int64 `bun:"rollup_id"` + Name string `bun:"name"` + Logo string `bun:"logo"` + Size int64 `bun:"size"` + Fee int64 `bun:"fee"` + BlobsCount int64 `bun:"blobs_count"` +} + type SeriesItem struct { Time time.Time `bun:"ts"` Value string `bun:"value"` @@ -162,6 +171,7 @@ type IStats interface { CumulativeSeries(ctx context.Context, timeframe Timeframe, name string, req SeriesRequest) ([]SeriesItem, error) NamespaceSeries(ctx context.Context, timeframe Timeframe, name string, nsId uint64, req SeriesRequest) (response []SeriesItem, err error) StakingSeries(ctx context.Context, timeframe Timeframe, name string, validatorId uint64, req SeriesRequest) (response []SeriesItem, err error) + RollupStats24h(ctx context.Context) ([]RollupStats24h, error) SquareSize(ctx context.Context, from, to *time.Time) (map[int][]SeriesItem, error) Change24hBlockStats(ctx context.Context) (response Change24hBlockStats, err error) } diff --git a/internal/storage/views.go b/internal/storage/views.go index 85585711..6d9cce45 100644 --- a/internal/storage/views.go +++ b/internal/storage/views.go @@ -20,4 +20,7 @@ const ( ViewStakingByMonth = "staking_by_month" ViewLeaderboard = "leaderboard" ViewSquareSize = "square_size" + ViewRollupStatsByHour = "rollup_stats_by_hour" + ViewRollupStatsByDay = "rollup_stats_by_day" + ViewRollupStatsByMonth = "rollup_stats_by_month" )