From 029ac6ba7ace25fafe26730a43792fb40e0a6dca Mon Sep 17 00:00:00 2001 From: Makis Christou Date: Thu, 29 Aug 2024 10:59:42 +0300 Subject: [PATCH] Refactor metrics and admin servers (#825) * Refactor admin server * Refactor metrics server * Move endpoint mounting in admin.go file * Convert tests to table tests * Directly mount routes, move admin under api package * Move api/admin under api * Remove redundant error code --- {admin => api}/admin.go | 25 +--------- api/admin_server.go | 55 ++++++++++++++++++++++ api/admin_test.go | 102 ++++++++++++++++++++++++++++++++++++++++ api/metrics_server.go | 39 +++++++++++++++ cmd/thor/main.go | 8 ++-- cmd/thor/utils.go | 46 ------------------ 6 files changed, 201 insertions(+), 74 deletions(-) rename {admin => api}/admin.go (73%) create mode 100644 api/admin_server.go create mode 100644 api/admin_test.go create mode 100644 api/metrics_server.go diff --git a/admin/admin.go b/api/admin.go similarity index 73% rename from admin/admin.go rename to api/admin.go index 17e873293..06bf57cfe 100644 --- a/admin/admin.go +++ b/api/admin.go @@ -3,15 +3,13 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package admin +package api import ( "encoding/json" "log/slog" "net/http" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" "github.com/vechain/thor/v2/log" ) @@ -24,7 +22,6 @@ type logLevelResponse struct { } type errorResponse struct { - ErrorCode int `json:"errorCode"` ErrorMessage string `json:"errorMessage"` } @@ -32,7 +29,6 @@ func writeError(w http.ResponseWriter, errCode int, errMsg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(errCode) json.NewEncoder(w).Encode(errorResponse{ - ErrorCode: errCode, ErrorMessage: errMsg, }) } @@ -82,22 +78,3 @@ func postLogLevelHandler(logLevel *slog.LevelVar) http.HandlerFunc { json.NewEncoder(w).Encode(response) } } - -func logLevelHandler(logLevel *slog.LevelVar) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - getLogLevelHandler(logLevel).ServeHTTP(w, r) - case http.MethodPost: - postLogLevelHandler(logLevel).ServeHTTP(w, r) - default: - writeError(w, http.StatusMethodNotAllowed, "method not allowed") - } - } -} - -func HTTPHandler(logLevel *slog.LevelVar) http.Handler { - router := mux.NewRouter() - router.HandleFunc("/admin/loglevel", logLevelHandler(logLevel)) - return handlers.CompressHandler(router) -} diff --git a/api/admin_server.go b/api/admin_server.go new file mode 100644 index 000000000..1984f9c3f --- /dev/null +++ b/api/admin_server.go @@ -0,0 +1,55 @@ +// 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 api + +import ( + "log/slog" + "net" + "net/http" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/vechain/thor/v2/co" +) + +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(getLogLevelHandler(logLevel)) + + sub.Path("/loglevel"). + Methods(http.MethodPost). + Name("post-log-level"). + HandlerFunc(postLogLevelHandler(logLevel)) + + return handlers.CompressHandler(router) +} + +func StartAdminServer(addr string, logLevel *slog.LevelVar) (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) + + srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} + var goes co.Goes + goes.Go(func() { + srv.Serve(listener) + }) + return "http://" + listener.Addr().String() + "/admin", func() { + srv.Close() + goes.Wait() + }, nil +} diff --git a/api/admin_test.go b/api/admin_test.go new file mode 100644 index 000000000..c30e2ae32 --- /dev/null +++ b/api/admin_test.go @@ -0,0 +1,102 @@ +// 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 api + +import ( + "bytes" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "testing" +) + +type TestCase struct { + name string + method string + body interface{} + expectedStatus int + expectedLevel string + expectedErrorMsg string +} + +func marshalBody(tt TestCase, t *testing.T) []byte { + var reqBody []byte + var err error + if tt.body != nil { + reqBody, err = json.Marshal(tt.body) + if err != nil { + t.Fatalf("could not marshal request body: %v", err) + } + } + return reqBody +} + +func TestLogLevelHandler(t *testing.T) { + tests := []TestCase{ + { + name: "Valid POST input - set level to DEBUG", + method: "POST", + body: map[string]string{"level": "debug"}, + expectedStatus: http.StatusOK, + expectedLevel: "DEBUG", + }, + { + name: "Invalid POST input - invalid level", + method: "POST", + body: map[string]string{"level": "invalid_body"}, + expectedStatus: http.StatusBadRequest, + expectedErrorMsg: "Invalid verbosity level", + }, + { + name: "GET request - get current level INFO", + method: "GET", + body: nil, + expectedStatus: http.StatusOK, + expectedLevel: "INFO", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var logLevel slog.LevelVar + logLevel.Set(slog.LevelInfo) + + reqBodyBytes := marshalBody(tt, t) + + req, err := http.NewRequest(tt.method, "/admin/loglevel", bytes.NewBuffer(reqBodyBytes)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HTTPHandler(&logLevel).ServeHTTP) + handler.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 + if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { + t.Fatalf("could not decode response: %v", err) + } + if response.CurrentLevel != tt.expectedLevel { + t.Errorf("handler returned unexpected log level: got %v want %v", response.CurrentLevel, tt.expectedLevel) + } + } else { + var response errorResponse + if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { + t.Fatalf("could not decode response: %v", err) + } + if response.ErrorMessage != tt.expectedErrorMsg { + t.Errorf("handler returned unexpected error message: got %v want %v", response.ErrorMessage, tt.expectedErrorMsg) + } + } + }) + } +} diff --git a/api/metrics_server.go b/api/metrics_server.go new file mode 100644 index 000000000..90972f879 --- /dev/null +++ b/api/metrics_server.go @@ -0,0 +1,39 @@ +// 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 api + +import ( + "net" + "net/http" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/vechain/thor/v2/co" + "github.com/vechain/thor/v2/metrics" +) + +func StartMetricsServer(addr string) (string, func(), error) { + listener, err := net.Listen("tcp", addr) + if err != nil { + return "", nil, errors.Wrapf(err, "listen metrics API addr [%v]", addr) + } + + router := mux.NewRouter() + router.PathPrefix("/metrics").Handler(metrics.HTTPHandler()) + handler := handlers.CompressHandler(router) + + srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} + var goes co.Goes + goes.Go(func() { + srv.Serve(listener) + }) + return "http://" + listener.Addr().String() + "/metrics", func() { + srv.Close() + goes.Wait() + }, nil +} diff --git a/cmd/thor/main.go b/cmd/thor/main.go index 31aed525a..a165b9d06 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -171,7 +171,7 @@ func defaultAction(ctx *cli.Context) error { metricsURL := "" if ctx.Bool(enableMetricsFlag.Name) { metrics.InitializePrometheusMetrics() - url, close, err := startMetricsServer(ctx.String(metricsAddrFlag.Name)) + url, close, err := api.StartMetricsServer(ctx.String(metricsAddrFlag.Name)) if err != nil { return fmt.Errorf("unable to start metrics server - %w", err) } @@ -181,7 +181,7 @@ func defaultAction(ctx *cli.Context) error { adminURL := "" if ctx.Bool(enableAdminFlag.Name) { - url, close, err := startAdminServer(ctx.String(adminAddrFlag.Name), logLevel) + url, close, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) if err != nil { return fmt.Errorf("unable to start admin server - %w", err) } @@ -315,7 +315,7 @@ func soloAction(ctx *cli.Context) error { metricsURL := "" if ctx.Bool(enableMetricsFlag.Name) { metrics.InitializePrometheusMetrics() - url, close, err := startMetricsServer(ctx.String(metricsAddrFlag.Name)) + url, close, err := api.StartMetricsServer(ctx.String(metricsAddrFlag.Name)) if err != nil { return fmt.Errorf("unable to start metrics server - %w", err) } @@ -325,7 +325,7 @@ func soloAction(ctx *cli.Context) error { adminURL := "" if ctx.Bool(enableAdminFlag.Name) { - url, close, err := startAdminServer(ctx.String(adminAddrFlag.Name), logLevel) + url, close, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) if err != nil { return fmt.Errorf("unable to start admin server - %w", err) } diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 88704dd1d..fbbecfc90 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -34,12 +34,9 @@ import ( "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/rlp" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" "github.com/mattn/go-isatty" "github.com/mattn/go-tty" "github.com/pkg/errors" - "github.com/vechain/thor/v2/admin" "github.com/vechain/thor/v2/api/doc" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/node" @@ -49,7 +46,6 @@ import ( "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/log" "github.com/vechain/thor/v2/logdb" - "github.com/vechain/thor/v2/metrics" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/p2psrv" "github.com/vechain/thor/v2/state" @@ -571,48 +567,6 @@ func startAPIServer(ctx *cli.Context, handler http.Handler, genesisID thor.Bytes }, nil } -func startMetricsServer(addr string) (string, func(), error) { - listener, err := net.Listen("tcp", addr) - if err != nil { - return "", nil, errors.Wrapf(err, "listen metrics API addr [%v]", addr) - } - - router := mux.NewRouter() - router.PathPrefix("/metrics").Handler(metrics.HTTPHandler()) - handler := handlers.CompressHandler(router) - - srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} - var goes co.Goes - goes.Go(func() { - srv.Serve(listener) - }) - return "http://" + listener.Addr().String() + "/metrics", func() { - srv.Close() - goes.Wait() - }, nil -} - -func startAdminServer(addr string, logLevel *slog.LevelVar) (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(admin.HTTPHandler(logLevel)) - handler := handlers.CompressHandler(router) - - srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} - var goes co.Goes - goes.Go(func() { - srv.Serve(listener) - }) - return "http://" + listener.Addr().String() + "/admin", func() { - srv.Close() - goes.Wait() - }, nil -} - func printStartupMessage1( gene *genesis.Genesis, repo *chain.Repository,