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..8fbb18ded --- /dev/null +++ b/api/admin/admin.go @@ -0,0 +1,25 @@ +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)),