Skip to content

Commit

Permalink
Refactor metrics and admin servers (#825)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MakisChristou authored Aug 29, 2024
1 parent 0ee058d commit 029ac6b
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 74 deletions.
25 changes: 1 addition & 24 deletions admin/admin.go → api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>

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"
)

Expand All @@ -24,15 +22,13 @@ type logLevelResponse struct {
}

type errorResponse struct {
ErrorCode int `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
}

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,
})
}
Expand Down Expand Up @@ -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)
}
55 changes: 55 additions & 0 deletions api/admin_server.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>

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
}
102 changes: 102 additions & 0 deletions api/admin_test.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>

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)
}
}
})
}
}
39 changes: 39 additions & 0 deletions api/metrics_server.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>

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
}
8 changes: 4 additions & 4 deletions cmd/thor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
46 changes: 0 additions & 46 deletions cmd/thor/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 029ac6b

Please sign in to comment.