From 19db51aa21b0201537db8fa4e1b8a38109b12b6f Mon Sep 17 00:00:00 2001 From: otherview Date: Wed, 4 Sep 2024 16:29:27 +0100 Subject: [PATCH 1/3] Adding Health endpoint --- api/api.go | 6 ++++ api/health/health.go | 41 +++++++++++++++++++++++ api/health/types.go | 10 ++++++ api/node/node_test.go | 3 +- cmd/thor/main.go | 13 ++++++-- cmd/thor/node/node.go | 6 ++++ cmd/thor/solo/solo.go | 6 ++++ cmd/thor/solo/solo_test.go | 3 +- cmd/thor/utils.go | 5 +-- comm/communicator.go | 6 +++- health/health.go | 66 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 api/health/health.go create mode 100644 api/health/types.go create mode 100644 health/health.go diff --git a/api/api.go b/api/api.go index ea33a0c16..d524f4b57 100644 --- a/api/api.go +++ b/api/api.go @@ -17,6 +17,7 @@ import ( "github.com/vechain/thor/v2/api/debug" "github.com/vechain/thor/v2/api/doc" "github.com/vechain/thor/v2/api/events" + "github.com/vechain/thor/v2/api/health" "github.com/vechain/thor/v2/api/node" "github.com/vechain/thor/v2/api/subscriptions" "github.com/vechain/thor/v2/api/transactions" @@ -28,6 +29,8 @@ import ( "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/txpool" + + healthstatus "github.com/vechain/thor/v2/health" ) var logger = log.WithContext("pkg", "api") @@ -40,6 +43,7 @@ func New( logDB *logdb.LogDB, bft bft.Committer, nw node.Network, + healthStatus *healthstatus.Health, forkConfig thor.ForkConfig, allowedOrigins string, backtraceLimit uint32, @@ -74,6 +78,8 @@ func New( accounts.New(repo, stater, callGasLimit, forkConfig, bft). Mount(router, "/accounts") + health.New(healthStatus).Mount(router, "/health") + if !skipLogs { events.New(repo, logDB, logsLimit). Mount(router, "/logs/event") diff --git a/api/health/health.go b/api/health/health.go new file mode 100644 index 000000000..e0b047bbe --- /dev/null +++ b/api/health/health.go @@ -0,0 +1,41 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/health" +) + +type Health struct { + healthStatus *health.Health +} + +func New(healthStatus *health.Health) *Health { + return &Health{ + healthStatus: healthStatus, + } +} + +func (h *Health) handleGetHealth(w http.ResponseWriter, req *http.Request) error { + acc, err := h.healthStatus.Status() + if err != nil { + return err + } + return utils.WriteJSON(w, acc) +} + +func (h *Health) Mount(root *mux.Router, pathPrefix string) { + sub := root.PathPrefix(pathPrefix).Subrouter() + + sub.Path("/"). + Methods(http.MethodGet). + Name("health"). + HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth)) +} diff --git a/api/health/types.go b/api/health/types.go new file mode 100644 index 000000000..6646ecad9 --- /dev/null +++ b/api/health/types.go @@ -0,0 +1,10 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +type Response struct { + Healthy bool `json:"healthy"` +} diff --git a/api/node/node_test.go b/api/node/node_test.go index 90857d0d0..520633322 100644 --- a/api/node/node_test.go +++ b/api/node/node_test.go @@ -18,6 +18,7 @@ import ( "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/comm" "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/txpool" @@ -49,7 +50,7 @@ func initCommServer(t *testing.T) { Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute, - })) + }), &health.Health{}) router := mux.NewRouter() node.New(comm).Mount(router, "/node") ts = httptest.NewServer(router) diff --git a/cmd/thor/main.go b/cmd/thor/main.go index a165b9d06..95d662dc9 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -25,6 +25,7 @@ import ( "github.com/vechain/thor/v2/cmd/thor/optimizer" "github.com/vechain/thor/v2/cmd/thor/solo" "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/logdb" "github.com/vechain/thor/v2/metrics" @@ -222,6 +223,7 @@ func defaultAction(ctx *cli.Context) error { return err } + healthStatus := &health.Health{} printStartupMessage1(gene, repo, master, instanceDir, forkConfig) if !skipLogs { @@ -238,7 +240,7 @@ func defaultAction(ctx *cli.Context) error { txPool := txpool.New(repo, state.NewStater(mainDB), txpoolOpt) defer func() { log.Info("closing tx pool..."); txPool.Close() }() - p2pCommunicator, err := newP2PCommunicator(ctx, repo, txPool, instanceDir) + p2pCommunicator, err := newP2PCommunicator(ctx, repo, txPool, instanceDir, healthStatus) if err != nil { return err } @@ -255,6 +257,7 @@ func defaultAction(ctx *cli.Context) error { logDB, bftEngine, p2pCommunicator.Communicator(), + healthStatus, forkConfig, ctx.String(apiCorsFlag.Name), uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), @@ -297,7 +300,9 @@ func defaultAction(ctx *cli.Context) error { p2pCommunicator.Communicator(), ctx.Uint64(targetGasLimitFlag.Name), skipLogs, - forkConfig).Run(exitSignal) + forkConfig, + healthStatus, + ).Run(exitSignal) } func soloAction(ctx *cli.Context) error { @@ -399,6 +404,8 @@ func soloAction(ctx *cli.Context) error { defer func() { log.Info("closing tx pool..."); txPool.Close() }() bftEngine := solo.NewBFTEngine(repo) + healthStatus := &health.Health{} + apiHandler, apiCloser := api.New( repo, state.NewStater(mainDB), @@ -406,6 +413,7 @@ func soloAction(ctx *cli.Context) error { logDB, bftEngine, &solo.Communicator{}, + healthStatus, forkConfig, ctx.String(apiCorsFlag.Name), uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), @@ -443,6 +451,7 @@ func soloAction(ctx *cli.Context) error { return solo.New(repo, state.NewStater(mainDB), logDB, + healthStatus, txPool, ctx.Uint64(gasLimitFlag.Name), ctx.Bool(onDemandFlag.Name), diff --git a/cmd/thor/node/node.go b/cmd/thor/node/node.go index 663ff185f..7bbe2669e 100644 --- a/cmd/thor/node/node.go +++ b/cmd/thor/node/node.go @@ -26,6 +26,7 @@ import ( "github.com/vechain/thor/v2/co" "github.com/vechain/thor/v2/comm" "github.com/vechain/thor/v2/consensus" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/logdb" "github.com/vechain/thor/v2/packer" @@ -64,6 +65,8 @@ type Node struct { maxBlockNum uint32 processLock sync.Mutex logWorker *worker + + health *health.Health } func New( @@ -78,6 +81,7 @@ func New( targetGasLimit uint64, skipLogs bool, forkConfig thor.ForkConfig, + health *health.Health, ) *Node { return &Node{ packer: packer.New(repo, stater, master.Address(), master.Beneficiary, forkConfig), @@ -92,6 +96,7 @@ func New( targetGasLimit: targetGasLimit, skipLogs: skipLogs, forkConfig: forkConfig, + health: health, } } @@ -387,6 +392,7 @@ func (n *Node) processBlock(newBlock *block.Block, stats *blockStats) (bool, err return err } n.processFork(newBlock, oldBest.Header.ID()) + n.health.NewBestBlock(newBlock.Header().ID()) } commitElapsed := mclock.Now() - startTime - execElapsed diff --git a/cmd/thor/solo/solo.go b/cmd/thor/solo/solo.go index 43dd644a1..6a20579a4 100644 --- a/cmd/thor/solo/solo.go +++ b/cmd/thor/solo/solo.go @@ -23,6 +23,7 @@ import ( "github.com/vechain/thor/v2/cmd/thor/bandwidth" "github.com/vechain/thor/v2/co" "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/logdb" "github.com/vechain/thor/v2/packer" @@ -44,6 +45,7 @@ type Solo struct { txPool *txpool.TxPool packer *packer.Packer logDB *logdb.LogDB + health *health.Health gasLimit uint64 bandwidth bandwidth.Bandwidth blockInterval uint64 @@ -56,6 +58,7 @@ func New( repo *chain.Repository, stater *state.Stater, logDB *logdb.LogDB, + health *health.Health, txPool *txpool.TxPool, gasLimit uint64, onDemand bool, @@ -74,6 +77,7 @@ func New( &genesis.DevAccounts()[0].Address, forkConfig), logDB: logDB, + health: health, gasLimit: gasLimit, blockInterval: blockInterval, skipLogs: skipLogs, @@ -211,6 +215,8 @@ func (s *Solo) packing(pendingTxs tx.Transactions, onDemand bool) error { ) logger.Debug(b.String()) + s.health.NewBestBlock(b.Header().ID()) + return nil } diff --git a/cmd/thor/solo/solo_test.go b/cmd/thor/solo/solo_test.go index a4df3f35d..ac30f4980 100644 --- a/cmd/thor/solo/solo_test.go +++ b/cmd/thor/solo/solo_test.go @@ -14,6 +14,7 @@ import ( "github.com/vechain/thor/v2/builtin" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/logdb" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/state" @@ -30,7 +31,7 @@ func newSolo() *Solo { repo, _ := chain.NewRepository(db, b) mempool := txpool.New(repo, stater, txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) - return New(repo, stater, logDb, mempool, 0, true, false, thor.BlockInterval, thor.ForkConfig{}) + return New(repo, stater, logDb, &health.Health{}, mempool, 0, true, false, thor.BlockInterval, thor.ForkConfig{}) } func TestInitSolo(t *testing.T) { diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index fbbecfc90..763adcae4 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -44,6 +44,7 @@ import ( "github.com/vechain/thor/v2/co" "github.com/vechain/thor/v2/comm" "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/logdb" "github.com/vechain/thor/v2/muxdb" @@ -489,7 +490,7 @@ func loadNodeMaster(ctx *cli.Context) (*node.Master, error) { return master, nil } -func newP2PCommunicator(ctx *cli.Context, repo *chain.Repository, txPool *txpool.TxPool, instanceDir string) (*p2p.P2P, error) { +func newP2PCommunicator(ctx *cli.Context, repo *chain.Repository, txPool *txpool.TxPool, instanceDir string, health *health.Health) (*p2p.P2P, error) { // known peers will be loaded/stored from/in this file peersCachePath := filepath.Join(instanceDir, "peers.cache") @@ -529,7 +530,7 @@ func newP2PCommunicator(ctx *cli.Context, repo *chain.Repository, txPool *txpool } return p2p.New( - comm.New(repo, txPool), + comm.New(repo, txPool, health), key, instanceDir, userNAT, diff --git a/comm/communicator.go b/comm/communicator.go index 4bdd1e2b2..6ec125533 100644 --- a/comm/communicator.go +++ b/comm/communicator.go @@ -20,6 +20,7 @@ import ( "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/co" "github.com/vechain/thor/v2/comm/proto" + "github.com/vechain/thor/v2/health" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tx" @@ -39,12 +40,13 @@ type Communicator struct { newBlockFeed event.Feed announcementCh chan *announcement feedScope event.SubscriptionScope + health *health.Health goes co.Goes onceSynced sync.Once } // New create a new Communicator instance. -func New(repo *chain.Repository, txPool *txpool.TxPool) *Communicator { +func New(repo *chain.Repository, txPool *txpool.TxPool, health *health.Health) *Communicator { ctx, cancel := context.WithCancel(context.Background()) return &Communicator{ repo: repo, @@ -52,6 +54,7 @@ func New(repo *chain.Repository, txPool *txpool.TxPool) *Communicator { ctx: ctx, cancel: cancel, peerSet: newPeerSet(), + health: health, syncedCh: make(chan struct{}), announcementCh: make(chan *announcement), } @@ -118,6 +121,7 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) { if shouldSynced() { delay = syncInterval c.onceSynced.Do(func() { + c.health.ChainSynced() close(c.syncedCh) }) } diff --git a/health/health.go b/health/health.go new file mode 100644 index 000000000..56252ae01 --- /dev/null +++ b/health/health.go @@ -0,0 +1,66 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "sync" + "time" + + "github.com/vechain/thor/v2/thor" +) + +type BlockIngestion struct { + BestBlock *thor.Bytes32 `json:"bestBlock"` + BestBlockTimestamp *time.Time `json:"bestBlockTimestamp"` +} + +type Status struct { + Healthy bool `json:"healthy"` + BlockIngestion *BlockIngestion `json:"blockIngestion"` + ChainSync bool `json:"chainSync"` +} + +type Health struct { + lock sync.RWMutex + newBestBlock time.Time + bestBlockID *thor.Bytes32 + chainSynced bool +} + +func (h *Health) NewBestBlock(ID thor.Bytes32) { + h.lock.Lock() + defer h.lock.Unlock() + + h.newBestBlock = time.Now() + h.bestBlockID = &ID +} + +func (h *Health) Status() (*Status, error) { + h.lock.RLock() + defer h.lock.RUnlock() + + blockIngest := &BlockIngestion{ + BestBlock: h.bestBlockID, + BestBlockTimestamp: &h.newBestBlock, + } + + // todo review time slots + healthy := time.Since(h.newBestBlock) >= 10*time.Second && + h.chainSynced + + return &Status{ + Healthy: healthy, + BlockIngestion: blockIngest, + ChainSync: h.chainSynced, + }, nil +} + +func (h *Health) ChainSynced() { + h.lock.Lock() + defer h.lock.Unlock() + + h.chainSynced = true +} From df30451451194df1a6f8814aa1579efbb5f4dace Mon Sep 17 00:00:00 2001 From: otherview Date: Tue, 29 Oct 2024 10:48:43 +0000 Subject: [PATCH 2/3] pr comments + 503 if not healthy --- api/health/health.go | 8 +++++++- comm/communicator.go | 3 ++- health/health.go | 12 ++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/api/health/health.go b/api/health/health.go index e0b047bbe..f7d0b0c73 100644 --- a/api/health/health.go +++ b/api/health/health.go @@ -23,11 +23,17 @@ func New(healthStatus *health.Health) *Health { } } -func (h *Health) handleGetHealth(w http.ResponseWriter, req *http.Request) error { +func (h *Health) handleGetHealth(w http.ResponseWriter, _ *http.Request) error { acc, err := h.healthStatus.Status() if err != nil { return err } + + if !acc.Healthy { + w.WriteHeader(http.StatusServiceUnavailable) // Set the status to 503 + } else { + w.WriteHeader(http.StatusOK) // Set the status to 200 + } return utils.WriteJSON(w, acc) } diff --git a/comm/communicator.go b/comm/communicator.go index 8a2656363..7fbef2e22 100644 --- a/comm/communicator.go +++ b/comm/communicator.go @@ -119,9 +119,10 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) { syncCount++ if shouldSynced() { + c.health.ChainSyncStatus(false) delay = syncInterval c.onceSynced.Do(func() { - c.health.ChainSynced() + c.health.ChainSyncStatus(true) close(c.syncedCh) }) } diff --git a/health/health.go b/health/health.go index 56252ae01..7d39a94ce 100644 --- a/health/health.go +++ b/health/health.go @@ -13,8 +13,8 @@ import ( ) type BlockIngestion struct { - BestBlock *thor.Bytes32 `json:"bestBlock"` - BestBlockTimestamp *time.Time `json:"bestBlockTimestamp"` + BestBlock *thor.Bytes32 `json:"bestBlock"` + BestBlockIngestionTimestamp *time.Time `json:"bestBlockIngestionTimestamp"` } type Status struct { @@ -43,8 +43,8 @@ func (h *Health) Status() (*Status, error) { defer h.lock.RUnlock() blockIngest := &BlockIngestion{ - BestBlock: h.bestBlockID, - BestBlockTimestamp: &h.newBestBlock, + BestBlock: h.bestBlockID, + BestBlockIngestionTimestamp: &h.newBestBlock, } // todo review time slots @@ -58,9 +58,9 @@ func (h *Health) Status() (*Status, error) { }, nil } -func (h *Health) ChainSynced() { +func (h *Health) ChainSyncStatus(syncStatus bool) { h.lock.Lock() defer h.lock.Unlock() - h.chainSynced = true + h.chainSynced = syncStatus } From 710e6c6adb3b013a57a9a2e2dbb5c5bddcc87449 Mon Sep 17 00:00:00 2001 From: otherview Date: Wed, 30 Oct 2024 13:05:02 +0000 Subject: [PATCH 3/3] refactored admin server and api + health endpoint tests --- api/accounts/accounts_test.go | 44 ---------------- api/admin/admin.go | 30 +++++++++++ api/{ => admin}/health/health.go | 2 +- api/admin/health/health_test.go | 52 +++++++++++++++++++ api/{ => admin}/health/types.go | 0 api/{admin.go => admin/loglevel/log_level.go} | 32 +++++++++--- .../loglevel/log_level_test.go} | 11 ++-- api/admin/loglevel/types.go | 9 ++++ api/admin_server.go | 29 ++--------- api/api.go | 6 --- api/blocks/blocks_test.go | 14 ----- api/events/events_test.go | 19 ------- api/node/node_test.go | 15 ------ cmd/thor/main.go | 10 ++-- 14 files changed, 132 insertions(+), 141 deletions(-) create mode 100644 api/admin/admin.go rename api/{ => admin}/health/health.go (98%) create mode 100644 api/admin/health/health_test.go rename api/{ => admin}/health/types.go (100%) rename api/{admin.go => admin/loglevel/log_level.go} (65%) rename api/{admin_test.go => admin/loglevel/log_level_test.go} (93%) create mode 100644 api/admin/loglevel/types.go diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go index cd4aac3cb..e126960b2 100644 --- a/api/accounts/accounts_test.go +++ b/api/accounts/accounts_test.go @@ -6,10 +6,8 @@ package accounts_test import ( - "bytes" "encoding/json" "fmt" - "io" "math/big" "net/http" "net/http/httptest" @@ -578,45 +576,3 @@ func batchCallWithNonExistingRevision(t *testing.T) { assert.Equal(t, http.StatusBadRequest, statusCode, "bad revision") assert.Equal(t, "revision: leveldb: not found\n", string(res), "revision not found") } - -func httpPost(t *testing.T, url string, body interface{}) ([]byte, int) { - data, err := json.Marshal(body) - if err != nil { - t.Fatal(err) - } - res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) //#nosec G107 - if err != nil { - t.Fatal(err) - } - r, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - return r, res.StatusCode -} - -func httpGet(t *testing.T, url string) ([]byte, int) { - res, err := http.Get(url) //#nosec G107 - if err != nil { - t.Fatal(err) - } - r, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - return r, res.StatusCode -} - -func httpGetAccount(t *testing.T, path string) *accounts.Account { - res, statusCode := httpGet(t, ts.URL+"/accounts/"+path) - var acc accounts.Account - if err := json.Unmarshal(res, &acc); err != nil { - t.Fatal(err) - } - - assert.Equal(t, http.StatusOK, statusCode, "get account failed") - - return &acc -} diff --git a/api/admin/admin.go b/api/admin/admin.go new file mode 100644 index 000000000..95673aa45 --- /dev/null +++ b/api/admin/admin.go @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package admin + +import ( + "log/slog" + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/api/admin/loglevel" + "github.com/vechain/thor/v2/health" + + healthAPI "github.com/vechain/thor/v2/api/admin/health" +) + +func New(logLevel *slog.LevelVar, health *health.Health) http.HandlerFunc { + router := mux.NewRouter() + router.PathPrefix("/admin") + + loglevel.New(logLevel).Mount(router, "/loglevel") + healthAPI.New(health).Mount(router, "/health") + + handler := handlers.CompressHandler(router) + + return handler.ServeHTTP +} diff --git a/api/health/health.go b/api/admin/health/health.go similarity index 98% rename from api/health/health.go rename to api/admin/health/health.go index f7d0b0c73..44de46095 100644 --- a/api/health/health.go +++ b/api/admin/health/health.go @@ -40,7 +40,7 @@ func (h *Health) handleGetHealth(w http.ResponseWriter, _ *http.Request) error { func (h *Health) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("/"). + sub.Path(""). Methods(http.MethodGet). Name("health"). HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth)) diff --git a/api/admin/health/health_test.go b/api/admin/health/health_test.go new file mode 100644 index 000000000..193fe688b --- /dev/null +++ b/api/admin/health/health_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vechain/thor/v2/health" +) + +var ts *httptest.Server + +func TestHealth(t *testing.T) { + initAPIServer(t) + + var healthStatus health.Status + respBody, statusCode := httpGet(t, ts.URL+"/health") + require.NoError(t, json.Unmarshal(respBody, &healthStatus)) + assert.False(t, healthStatus.Healthy) + assert.Equal(t, http.StatusServiceUnavailable, statusCode) +} + +func initAPIServer(_ *testing.T) { + router := mux.NewRouter() + New(&health.Health{}).Mount(router, "/health") + + ts = httptest.NewServer(router) +} + +func httpGet(t *testing.T, url string) ([]byte, int) { + res, err := http.Get(url) //#nosec G107 + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + r, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + return r, res.StatusCode +} diff --git a/api/health/types.go b/api/admin/health/types.go similarity index 100% rename from api/health/types.go rename to api/admin/health/types.go diff --git a/api/admin.go b/api/admin/loglevel/log_level.go similarity index 65% rename from api/admin.go rename to api/admin/loglevel/log_level.go index afd299cfa..27b9a04fa 100644 --- a/api/admin.go +++ b/api/admin/loglevel/log_level.go @@ -3,28 +3,44 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package api +package loglevel import ( "log/slog" "net/http" + "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/utils" "github.com/vechain/thor/v2/log" ) -type logLevelRequest struct { - Level string `json:"level"` +type LogLevel struct { + logLevel *slog.LevelVar } -type logLevelResponse struct { - CurrentLevel string `json:"currentLevel"` +func New(logLevel *slog.LevelVar) *LogLevel { + return &LogLevel{ + logLevel: logLevel, + } +} + +func (l *LogLevel) Mount(root *mux.Router, pathPrefix string) { + sub := root.PathPrefix(pathPrefix).Subrouter() + sub.Path(""). + Methods(http.MethodGet). + Name("get-log-level"). + HandlerFunc(utils.WrapHandlerFunc(getLogLevelHandler(l.logLevel))) + + sub.Path(""). + Methods(http.MethodPost). + Name("post-log-level"). + HandlerFunc(utils.WrapHandlerFunc(postLogLevelHandler(l.logLevel))) } func getLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { - return utils.WriteJSON(w, logLevelResponse{ + return utils.WriteJSON(w, Response{ CurrentLevel: logLevel.Level().String(), }) } @@ -32,7 +48,7 @@ func getLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { func postLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { - var req logLevelRequest + var req Request if err := utils.ParseJSON(r.Body, &req); err != nil { return utils.BadRequest(errors.WithMessage(err, "Invalid request body")) @@ -55,7 +71,7 @@ func postLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { return utils.BadRequest(errors.New("Invalid verbosity level")) } - return utils.WriteJSON(w, logLevelResponse{ + return utils.WriteJSON(w, Response{ CurrentLevel: logLevel.Level().String(), }) } diff --git a/api/admin_test.go b/api/admin/loglevel/log_level_test.go similarity index 93% rename from api/admin_test.go rename to api/admin/loglevel/log_level_test.go index be2847cbf..d04320ce3 100644 --- a/api/admin_test.go +++ b/api/admin/loglevel/log_level_test.go @@ -3,7 +3,7 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package api +package loglevel import ( "bytes" @@ -14,6 +14,8 @@ import ( "strings" "testing" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" ) @@ -76,15 +78,16 @@ func TestLogLevelHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(HTTPHandler(&logLevel).ServeHTTP) - handler.ServeHTTP(rr, req) + router := mux.NewRouter() + New(&logLevel).Mount(router, "/admin/loglevel") + router.ServeHTTP(rr, req) if status := rr.Code; status != tt.expectedStatus { t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus) } if tt.expectedLevel != "" { - var response logLevelResponse + var response Response if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { t.Fatalf("could not decode response: %v", err) } diff --git a/api/admin/loglevel/types.go b/api/admin/loglevel/types.go new file mode 100644 index 000000000..d440d572b --- /dev/null +++ b/api/admin/loglevel/types.go @@ -0,0 +1,9 @@ +package loglevel + +type Request struct { + Level string `json:"level"` +} + +type Response struct { + CurrentLevel string `json:"currentLevel"` +} diff --git a/api/admin_server.go b/api/admin_server.go index 26054e908..95ff613ab 100644 --- a/api/admin_server.go +++ b/api/admin_server.go @@ -11,40 +11,21 @@ import ( "net/http" "time" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" "github.com/pkg/errors" - "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/api/admin" "github.com/vechain/thor/v2/co" + "github.com/vechain/thor/v2/health" ) -func HTTPHandler(logLevel *slog.LevelVar) http.Handler { - router := mux.NewRouter() - sub := router.PathPrefix("/admin").Subrouter() - sub.Path("/loglevel"). - Methods(http.MethodGet). - Name("get-log-level"). - HandlerFunc(utils.WrapHandlerFunc(getLogLevelHandler(logLevel))) - - sub.Path("/loglevel"). - Methods(http.MethodPost). - Name("post-log-level"). - HandlerFunc(utils.WrapHandlerFunc(postLogLevelHandler(logLevel))) - - return handlers.CompressHandler(router) -} - -func StartAdminServer(addr string, logLevel *slog.LevelVar) (string, func(), error) { +func StartAdminServer(addr string, logLevel *slog.LevelVar, health *health.Health) (string, func(), error) { listener, err := net.Listen("tcp", addr) if err != nil { return "", nil, errors.Wrapf(err, "listen admin API addr [%v]", addr) } - router := mux.NewRouter() - router.PathPrefix("/admin").Handler(HTTPHandler(logLevel)) - handler := handlers.CompressHandler(router) + adminHandler := admin.New(logLevel, health) - srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} + srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} var goes co.Goes goes.Go(func() { srv.Serve(listener) diff --git a/api/api.go b/api/api.go index 4d02c152d..38b412a97 100644 --- a/api/api.go +++ b/api/api.go @@ -17,7 +17,6 @@ import ( "github.com/vechain/thor/v2/api/debug" "github.com/vechain/thor/v2/api/doc" "github.com/vechain/thor/v2/api/events" - "github.com/vechain/thor/v2/api/health" "github.com/vechain/thor/v2/api/node" "github.com/vechain/thor/v2/api/subscriptions" "github.com/vechain/thor/v2/api/transactions" @@ -29,8 +28,6 @@ import ( "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/txpool" - - healthstatus "github.com/vechain/thor/v2/health" ) var logger = log.WithContext("pkg", "api") @@ -43,7 +40,6 @@ func New( logDB *logdb.LogDB, bft bft.Committer, nw node.Network, - healthStatus *healthstatus.Health, forkConfig thor.ForkConfig, allowedOrigins string, backtraceLimit uint32, @@ -78,8 +74,6 @@ func New( accounts.New(repo, stater, callGasLimit, forkConfig, bft). Mount(router, "/accounts") - health.New(healthStatus).Mount(router, "/health") - if !skipLogs { events.New(repo, logDB, logsLimit). Mount(router, "/logs/event") diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go index 60b96b299..a203cadd6 100644 --- a/api/blocks/blocks_test.go +++ b/api/blocks/blocks_test.go @@ -7,7 +7,6 @@ package blocks_test import ( "encoding/json" - "io" "math" "math/big" "net/http" @@ -267,16 +266,3 @@ func checkExpandedBlock(t *testing.T, expBl *block.Block, actBl *blocks.JSONExpa assert.Equal(t, tx.ID(), actBl.Transactions[i].ID, "txid should be equal") } } - -func httpGet(t *testing.T, url string) ([]byte, int) { - res, err := http.Get(url) //#nosec G107 - if err != nil { - t.Fatal(err) - } - r, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - return r, res.StatusCode -} diff --git a/api/events/events_test.go b/api/events/events_test.go index 9f859780b..3d28e41dc 100644 --- a/api/events/events_test.go +++ b/api/events/events_test.go @@ -6,9 +6,7 @@ package events_test import ( - "bytes" "encoding/json" - "io" "net/http" "net/http/httptest" "strings" @@ -212,23 +210,6 @@ func createDb(t *testing.T) *logdb.LogDB { } // Utilities functions -func httpPost(t *testing.T, url string, body interface{}) ([]byte, int) { - data, err := json.Marshal(body) - if err != nil { - t.Fatal(err) - } - res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) //#nosec G107 - if err != nil { - t.Fatal(err) - } - r, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - return r, res.StatusCode -} - func insertBlocks(t *testing.T, db *logdb.LogDB, n int) { b := new(block.Builder).Build() for i := 0; i < n; i++ { diff --git a/api/node/node_test.go b/api/node/node_test.go index 6e324efcb..58b1c86a5 100644 --- a/api/node/node_test.go +++ b/api/node/node_test.go @@ -5,8 +5,6 @@ package node_test import ( - "io" - "net/http" "net/http/httptest" "testing" "time" @@ -55,16 +53,3 @@ func initCommServer(t *testing.T) { node.New(comm).Mount(router, "/node") ts = httptest.NewServer(router) } - -func httpGet(t *testing.T, url string) []byte { - res, err := http.Get(url) //#nosec G107 - if err != nil { - t.Fatal(err) - } - r, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - return r -} diff --git a/cmd/thor/main.go b/cmd/thor/main.go index 2997e8ea7..ba34f5543 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -167,6 +167,7 @@ func defaultAction(ctx *cli.Context) error { return errors.Wrap(err, "parse verbosity flag") } logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) + healthStatus := &health.Health{} // enable metrics as soon as possible metricsURL := "" @@ -182,7 +183,7 @@ func defaultAction(ctx *cli.Context) error { adminURL := "" if ctx.Bool(enableAdminFlag.Name) { - url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) + url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, healthStatus) if err != nil { return fmt.Errorf("unable to start admin server - %w", err) } @@ -221,7 +222,6 @@ func defaultAction(ctx *cli.Context) error { return err } - healthStatus := &health.Health{} printStartupMessage1(gene, repo, master, instanceDir, forkConfig) skipLogs := ctx.Bool(skipLogsFlag.Name) @@ -256,7 +256,6 @@ func defaultAction(ctx *cli.Context) error { logDB, bftEngine, p2pCommunicator.Communicator(), - healthStatus, forkConfig, ctx.String(apiCorsFlag.Name), uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), @@ -314,6 +313,7 @@ func soloAction(ctx *cli.Context) error { } logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) + healthStatus := &health.Health{} // enable metrics as soon as possible metricsURL := "" @@ -329,7 +329,7 @@ func soloAction(ctx *cli.Context) error { adminURL := "" if ctx.Bool(enableAdminFlag.Name) { - url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) + url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, healthStatus) if err != nil { return fmt.Errorf("unable to start admin server - %w", err) } @@ -404,7 +404,6 @@ func soloAction(ctx *cli.Context) error { defer func() { log.Info("closing tx pool..."); txPool.Close() }() bftEngine := solo.NewBFTEngine(repo) - healthStatus := &health.Health{} apiHandler, apiCloser := api.New( repo, @@ -413,7 +412,6 @@ func soloAction(ctx *cli.Context) error { logDB, bftEngine, &solo.Communicator{}, - healthStatus, forkConfig, ctx.String(apiCorsFlag.Name), uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),