Skip to content

Commit

Permalink
Darren/admin api log toggler (#877)
Browse files Browse the repository at this point in the history
* Adding Health endpoint

* pr comments + 503 if not healthy

* refactored admin server and api + health endpoint tests

* fix health condition

* fix admin routing

* added comments + changed from ChainSync to ChainBootstrapStatus

* Adding healthcheck for solo mode

* adding solo + tests

* fix log_level handler funcs

* feat(admin): toggle api logs via admin API

* feat(admin): add license headers

* refactor health package + add p2p count

* remove solo methods

* moving health service to api pkg

* added defaults + api health query

* pr comments

* pr comments

---------

Co-authored-by: otherview <[email protected]>
  • Loading branch information
darrenvechain and otherview authored Dec 9, 2024
1 parent 7579db4 commit de248a6
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 14 deletions.
8 changes: 6 additions & 2 deletions api/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ package admin
import (
"log/slog"
"net/http"
"sync/atomic"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
healthAPI "github.com/vechain/thor/v2/api/admin/health"
"github.com/vechain/thor/v2/api/admin/apilogs"
"github.com/vechain/thor/v2/api/admin/loglevel"

healthAPI "github.com/vechain/thor/v2/api/admin/health"
)

func New(logLevel *slog.LevelVar, health *healthAPI.Health) http.HandlerFunc {
func New(logLevel *slog.LevelVar, health *healthAPI.Health, apiLogsToggle *atomic.Bool) http.HandlerFunc {
router := mux.NewRouter()
subRouter := router.PathPrefix("/admin").Subrouter()

loglevel.New(logLevel).Mount(subRouter, "/loglevel")
healthAPI.NewAPI(health).Mount(subRouter, "/health")
apilogs.New(apiLogsToggle).Mount(subRouter, "/apilogs")

handler := handlers.CompressHandler(router)

Expand Down
70 changes: 70 additions & 0 deletions api/admin/apilogs/api_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 apilogs

import (
"net/http"
"sync"
"sync/atomic"

"github.com/gorilla/mux"
"github.com/vechain/thor/v2/api/utils"
"github.com/vechain/thor/v2/log"
)

type APILogs struct {
enabled *atomic.Bool
mu sync.Mutex
}

type Status struct {
Enabled bool `json:"enabled"`
}

func New(enabled *atomic.Bool) *APILogs {
return &APILogs{
enabled: enabled,
}
}

func (a *APILogs) Mount(root *mux.Router, pathPrefix string) {
sub := root.PathPrefix(pathPrefix).Subrouter()
sub.Path("").
Methods(http.MethodGet).
Name("get-api-logs-enabled").
HandlerFunc(utils.WrapHandlerFunc(a.areAPILogsEnabled))

sub.Path("").
Methods(http.MethodPost).
Name("post-api-logs-enabled").
HandlerFunc(utils.WrapHandlerFunc(a.setAPILogsEnabled))
}

func (a *APILogs) areAPILogsEnabled(w http.ResponseWriter, _ *http.Request) error {
a.mu.Lock()
defer a.mu.Unlock()

return utils.WriteJSON(w, Status{
Enabled: a.enabled.Load(),
})
}

func (a *APILogs) setAPILogsEnabled(w http.ResponseWriter, r *http.Request) error {
a.mu.Lock()
defer a.mu.Unlock()

var req Status
if err := utils.ParseJSON(r.Body, &req); err != nil {
return utils.BadRequest(err)
}
a.enabled.Store(req.Enabled)

log.Info("api logs updated", "pkg", "apilogs", "enabled", req.Enabled)

return utils.WriteJSON(w, Status{
Enabled: a.enabled.Load(),
})
}
91 changes: 91 additions & 0 deletions api/admin/apilogs/api_logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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 apilogs

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)

type TestCase struct {
name string
method string
expectedHTTP int
startValue bool
expectedEndValue bool
requestBody bool
}

func marshalBody(tt TestCase, t *testing.T) []byte {
var reqBody []byte
var err error
if tt.method == "POST" {
reqBody, err = json.Marshal(Status{Enabled: tt.requestBody})
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 logs to enabled",
method: "POST",
expectedHTTP: http.StatusOK,
startValue: false,
requestBody: true,
expectedEndValue: true,
},
{
name: "Valid POST input - set logs to disabled",
method: "POST",
expectedHTTP: http.StatusOK,
startValue: true,
requestBody: false,
expectedEndValue: false,
},
{
name: "GET request - get current level INFO",
method: "GET",
expectedHTTP: http.StatusOK,
startValue: true,
expectedEndValue: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logLevel := atomic.Bool{}
logLevel.Store(tt.startValue)

reqBodyBytes := marshalBody(tt, t)

req, err := http.NewRequest(tt.method, "/admin/apilogs", bytes.NewBuffer(reqBodyBytes))
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
router := mux.NewRouter()
New(&logLevel).Mount(router, "/admin/apilogs")
router.ServeHTTP(rr, req)

assert.Equal(t, tt.expectedHTTP, rr.Code)
responseBody := Status{}
assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &responseBody))
assert.Equal(t, tt.expectedEndValue, responseBody.Enabled)
})
}
}
2 changes: 2 additions & 0 deletions api/admin/loglevel/log_level.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func (l *LogLevel) postLogLevelHandler(w http.ResponseWriter, r *http.Request) e
return utils.BadRequest(errors.New("Invalid verbosity level"))
}

log.Info("log level changed", "pkg", "loglevel", "level", l.logLevel.Level().String())

return utils.WriteJSON(w, Response{
CurrentLevel: l.logLevel.Level().String(),
})
Expand Down
4 changes: 3 additions & 1 deletion api/admin_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log/slog"
"net"
"net/http"
"sync/atomic"
"time"

"github.com/pkg/errors"
Expand All @@ -24,13 +25,14 @@ func StartAdminServer(
logLevel *slog.LevelVar,
repo *chain.Repository,
p2p *comm.Communicator,
apiLogs *atomic.Bool,
) (string, func(), error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return "", nil, errors.Wrapf(err, "listen admin API addr [%v]", addr)
}

adminHandler := admin.New(logLevel, health.New(repo, p2p))
adminHandler := admin.New(logLevel, health.New(repo, p2p), apiLogs)

srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second}
var goes co.Goes
Expand Down
7 changes: 3 additions & 4 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/http/pprof"
"strings"
"sync/atomic"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
Expand Down Expand Up @@ -39,7 +40,7 @@ type Config struct {
PprofOn bool
SkipLogs bool
AllowCustomTracer bool
EnableReqLogger bool
EnableReqLogger *atomic.Bool
EnableMetrics bool
LogsLimit uint64
AllowedTracers []string
Expand Down Expand Up @@ -115,9 +116,7 @@ func New(
handlers.ExposedHeaders([]string{"x-genesis-id", "x-thorest-ver"}),
)(handler)

if config.EnableReqLogger {
handler = RequestLoggerHandler(handler, logger)
}
handler = RequestLoggerHandler(handler, logger, config.EnableReqLogger)

return handler.ServeHTTP, subs.Close // subscriptions handles hijacked conns, which need to be closed
}
7 changes: 6 additions & 1 deletion api/request_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import (
"bytes"
"io"
"net/http"
"sync/atomic"
"time"

"github.com/vechain/thor/v2/log"
)

// RequestLoggerHandler returns a http handler to ensure requests are syphoned into the writer
func RequestLoggerHandler(handler http.Handler, logger log.Logger) http.Handler {
func RequestLoggerHandler(handler http.Handler, logger log.Logger, enabled *atomic.Bool) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if !enabled.Load() {
handler.ServeHTTP(w, r)
return
}
// Read and log the body (note: this can only be done once)
// Ensure you don't disrupt the request body for handlers that need to read it
var bodyBytes []byte
Expand Down
5 changes: 4 additions & 1 deletion api/request_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -59,6 +60,8 @@ func (m *mockLogger) GetLoggedData() []interface{} {

func TestRequestLoggerHandler(t *testing.T) {
mockLog := &mockLogger{}
enabled := atomic.Bool{}
enabled.Store(true)

// Define a test handler to wrap
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
Expand All @@ -67,7 +70,7 @@ func TestRequestLoggerHandler(t *testing.T) {
})

// Create the RequestLoggerHandler
loggerHandler := RequestLoggerHandler(testHandler, mockLog)
loggerHandler := RequestLoggerHandler(testHandler, mockLog, &enabled)

// Create a test HTTP request
reqBody := "test body"
Expand Down
23 changes: 20 additions & 3 deletions cmd/thor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"os"
"path/filepath"
"sync/atomic"
"time"

"github.com/ethereum/go-ethereum/accounts/keystore"
Expand Down Expand Up @@ -234,12 +235,15 @@ func defaultAction(ctx *cli.Context) error {
}

adminURL := ""
logAPIRequests := &atomic.Bool{}
logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name))
if ctx.Bool(enableAdminFlag.Name) {
url, closeFunc, err := api.StartAdminServer(
ctx.String(adminAddrFlag.Name),
logLevel,
repo,
p2pCommunicator.Communicator(),
logAPIRequests,
)
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
Expand All @@ -261,7 +265,7 @@ func defaultAction(ctx *cli.Context) error {
bftEngine,
p2pCommunicator.Communicator(),
forkConfig,
makeAPIConfig(ctx, false),
makeAPIConfig(ctx, logAPIRequests, false),
)
defer func() { log.Info("closing API..."); apiCloser() }()

Expand Down Expand Up @@ -370,8 +374,16 @@ func soloAction(ctx *cli.Context) error {
}

adminURL := ""
logAPIRequests := &atomic.Bool{}
logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name))
if ctx.Bool(enableAdminFlag.Name) {
url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, repo, nil)
url, closeFunc, err := api.StartAdminServer(
ctx.String(adminAddrFlag.Name),
logLevel,
repo,
nil,
logAPIRequests,
)
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
}
Expand Down Expand Up @@ -411,7 +423,7 @@ func soloAction(ctx *cli.Context) error {
bftEngine,
&solo.Communicator{},
forkConfig,
makeAPIConfig(ctx, true),
makeAPIConfig(ctx, logAPIRequests, true),
)
defer func() { log.Info("closing API..."); apiCloser() }()

Expand All @@ -424,6 +436,11 @@ func soloAction(ctx *cli.Context) error {
srvCloser()
}()

blockInterval := ctx.Uint64(blockInterval.Name)
if blockInterval == 0 {
return errors.New("block-interval cannot be zero")
}

printStartupMessage2(gene, apiURL, "", metricsURL, adminURL)

optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name))
Expand Down
5 changes: 3 additions & 2 deletions cmd/thor/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"runtime"
"runtime/debug"
"strings"
"sync/atomic"
"syscall"
"time"

Expand Down Expand Up @@ -275,15 +276,15 @@ func parseGenesisFile(filePath string) (*genesis.Genesis, thor.ForkConfig, error
return customGen, forkConfig, nil
}

func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config {
func makeAPIConfig(ctx *cli.Context, logAPIRequests *atomic.Bool, soloMode bool) api.Config {
return api.Config{
AllowedOrigins: ctx.String(apiCorsFlag.Name),
BacktraceLimit: uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
CallGasLimit: ctx.Uint64(apiCallGasLimitFlag.Name),
PprofOn: ctx.Bool(pprofFlag.Name),
SkipLogs: ctx.Bool(skipLogsFlag.Name),
AllowCustomTracer: ctx.Bool(apiAllowCustomTracerFlag.Name),
EnableReqLogger: ctx.Bool(enableAPILogsFlag.Name),
EnableReqLogger: logAPIRequests,
EnableMetrics: ctx.Bool(enableMetricsFlag.Name),
LogsLimit: ctx.Uint64(apiLogsLimitFlag.Name),
AllowedTracers: parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))),
Expand Down

0 comments on commit de248a6

Please sign in to comment.