Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to change log verbosity while the node is running #799

Merged
merged 11 commits into from
Aug 19, 2024
43 changes: 43 additions & 0 deletions admin/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 admin
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be good to have some unit tests ? Since we're setting the package up, perhaps it would make any refactor easy at this stage.


import (
"fmt"
"log/slog"
"net/http"

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

func HTTPHandler(logLevel *slog.LevelVar) http.Handler {
router := mux.NewRouter()
router.PathPrefix("/admin").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
MakisChristou marked this conversation as resolved.
Show resolved Hide resolved
verbosity := r.URL.Query().Get("verbosity")
switch verbosity {
case "debug":
logLevel.Set(log.LevelDebug)
case "info":
logLevel.Set(log.LevelInfo)
case "warn":
logLevel.Set(log.LevelWarn)
case "error":
logLevel.Set(log.LevelError)
case "trace":
logLevel.Set(log.LevelTrace)
case "crit":
logLevel.Set(log.LevelCrit)
default:
http.Error(w, "Invalid verbosity level", http.StatusBadRequest)
return
}

fmt.Fprintln(w, "Verbosity changed to ", verbosity)
}))
return handlers.CompressHandler(router)
}
9 changes: 7 additions & 2 deletions cmd/disco/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"crypto/ecdsa"
"fmt"
"io"
"log/slog"
"os"
"os/user"
"path/filepath"
Expand All @@ -19,15 +20,19 @@ import (
"github.com/vechain/thor/v2/log"
)

func initLogger(lvl int) {
func initLogger(lvl int) *slog.LevelVar {
logLevel := log.FromLegacyLevel(lvl)
var level slog.LevelVar
level.Set(logLevel)
MakisChristou marked this conversation as resolved.
Show resolved Hide resolved
output := io.Writer(os.Stdout)
useColor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
handler := log.NewTerminalHandlerWithLevel(output, logLevel, useColor)
handler := log.NewTerminalHandlerWithLevel(output, &level, useColor)
log.SetDefault(log.NewLogger(handler))
ethlog.Root().SetHandler(&ethLogger{
logger: log.WithContext("pkg", "geth"),
})

return &level
}

type ethLogger struct {
Expand Down
9 changes: 9 additions & 0 deletions cmd/thor/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ var (
Value: "localhost:2112",
Usage: "metrics service listening address",
}
enableAdminFlag = cli.BoolFlag{
MakisChristou marked this conversation as resolved.
Show resolved Hide resolved
Name: "enable-admin",
Usage: "enables admin service",
}
adminAddrFlag = cli.StringFlag{
Name: "admin-addr",
Value: "localhost:2113",
Usage: "admin service listening address",
}

// solo mode only flags
onDemandFlag = cli.BoolFlag{
Expand Down
33 changes: 29 additions & 4 deletions cmd/thor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ func main() {
disablePrunerFlag,
enableMetricsFlag,
metricsAddrFlag,
adminAddrFlag,
enableAdminFlag,
},
Action: defaultAction,
Commands: []cli.Command{
Expand Down Expand Up @@ -125,6 +127,8 @@ func main() {
disablePrunerFlag,
enableMetricsFlag,
metricsAddrFlag,
adminAddrFlag,
enableAdminFlag,
},
Action: soloAction,
},
Expand Down Expand Up @@ -156,7 +160,7 @@ func defaultAction(ctx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "parse verbosity flag")
}
initLogger(lvl, ctx.Bool(jsonLogsFlag.Name))
logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name))

// enable metrics as soon as possible
metricsURL := ""
Expand All @@ -170,6 +174,16 @@ func defaultAction(ctx *cli.Context) error {
defer func() { log.Info("stopping metrics server..."); close() }()
}

adminURL := ""
if ctx.Bool(enableAdminFlag.Name) {
url, close, err := startAdminServer(ctx.String(adminAddrFlag.Name), logLevel)
MakisChristou marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
}
adminURL = url
defer func() { log.Info("stopping admin server..."); close() }()
}

gene, forkConfig, err := selectGenesis(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -251,7 +265,7 @@ func defaultAction(ctx *cli.Context) error {
}
defer func() { log.Info("stopping API server..."); srvCloser() }()

printStartupMessage2(gene, apiURL, p2pCommunicator.Enode(), metricsURL)
printStartupMessage2(gene, apiURL, p2pCommunicator.Enode(), metricsURL, adminURL)

if err := p2pCommunicator.Start(); err != nil {
return err
Expand Down Expand Up @@ -283,7 +297,8 @@ func soloAction(ctx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "parse verbosity flag")
}
initLogger(lvl, ctx.Bool(jsonLogsFlag.Name))

logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name))

// enable metrics as soon as possible
metricsURL := ""
Expand All @@ -297,6 +312,16 @@ func soloAction(ctx *cli.Context) error {
defer func() { log.Info("stopping metrics server..."); close() }()
}

adminURL := ""
if ctx.Bool(enableAdminFlag.Name) {
url, close, err := startAdminServer(ctx.String(adminAddrFlag.Name), logLevel)
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
}
adminURL = url
defer func() { log.Info("stopping admin server..."); close() }()
}

var (
gene *genesis.Genesis
forkConfig thor.ForkConfig
Expand Down Expand Up @@ -397,7 +422,7 @@ func soloAction(ctx *cli.Context) error {
return errors.New("block-interval cannot be zero")
}

printSoloStartupMessage(gene, repo, instanceDir, apiURL, forkConfig, metricsURL)
printSoloStartupMessage(gene, repo, instanceDir, apiURL, forkConfig, metricsURL, adminURL)

optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name))
defer func() { log.Info("stopping optimizer..."); optimizer.Stop() }()
Expand Down
52 changes: 48 additions & 4 deletions cmd/thor/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"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 @@ -60,21 +61,25 @@ import (

var devNetGenesisID = genesis.NewDevnet().ID()

func initLogger(lvl int, jsonLogs bool) {
func initLogger(lvl int, jsonLogs bool) *slog.LevelVar {
logLevel := log.FromLegacyLevel(lvl)
output := io.Writer(os.Stdout)
var level slog.LevelVar
level.Set(logLevel)

var handler slog.Handler
if jsonLogs {
handler = log.JSONHandlerWithLevel(output, logLevel)
handler = log.JSONHandlerWithLevel(output, &level)
} else {
useColor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
handler = log.NewTerminalHandlerWithLevel(output, logLevel, useColor)
handler = log.NewTerminalHandlerWithLevel(output, &level, useColor)
MakisChristou marked this conversation as resolved.
Show resolved Hide resolved
}
log.SetDefault(log.NewLogger(handler))
ethlog.Root().SetHandler(ethlog.LvlFilterHandler(ethlog.LvlWarn, &ethLogger{
logger: log.WithContext("pkg", "geth"),
}))

return &level
}

type ethLogger struct {
Expand Down Expand Up @@ -587,6 +592,27 @@ func startMetricsServer(addr string) (string, func(), error) {
}, nil
}

func startAdminServer(addr string, logLevel *slog.LevelVar) (string, func(), error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this live in the admin package. And starting the admin server would follow the same patter as the api package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. For this I just used the same pattern that is used in metrics with startMetricsServer living in utils.go.

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 Expand Up @@ -638,8 +664,9 @@ func printStartupMessage2(
apiURL string,
nodeID string,
metricsURL string,
adminURL string,
) {
fmt.Printf(`%v API portal [ %v ]%v%v`,
fmt.Printf(`%v API portal [ %v ]%v%v%v`,
func() string { // node ID
if nodeID == "" {
return ""
Expand All @@ -659,6 +686,15 @@ func printStartupMessage2(
metricsURL)
}
}(),
func() string { // admin URL
if adminURL == "" {
return ""
} else {
return fmt.Sprintf(`
Admin [ %v ]`,
adminURL)
}
}(),
func() string {
// print default dev net's dev accounts info
if gene.ID() == devNetGenesisID {
Expand All @@ -681,6 +717,7 @@ func printSoloStartupMessage(
apiURL string,
forkConfig thor.ForkConfig,
metricsURL string,
adminURL string,
) {
bestBlock := repo.BestBlockSummary()

Expand All @@ -691,6 +728,7 @@ func printSoloStartupMessage(
Data dir [ %v ]
API portal [ %v ]
Metrics [ %v ]
Admin [ %v ]
`,
common.MakeName("Thor solo", fullVersion()),
gene.ID(), gene.Name(),
Expand All @@ -704,6 +742,12 @@ func printSoloStartupMessage(
}
return metricsURL
}(),
func() string {
if adminURL == "" {
return "Disabled"
}
return adminURL
}(),
)

if gene.ID() == devNetGenesisID {
Expand Down
22 changes: 13 additions & 9 deletions log/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (h *discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
type TerminalHandler struct {
mu sync.Mutex
wr io.Writer
lvl slog.Level
lvl *slog.LevelVar
useColor bool
attrs []slog.Attr
// fieldPadding is a map with maximum field value lengths seen until now
Expand All @@ -75,12 +75,14 @@ type TerminalHandler struct {
//
// [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002
func NewTerminalHandler(wr io.Writer, useColor bool) *TerminalHandler {
return NewTerminalHandlerWithLevel(wr, levelMaxVerbosity, useColor)
var level slog.LevelVar
level.Set(levelMaxVerbosity)
return NewTerminalHandlerWithLevel(wr, &level, useColor)
}

// NewTerminalHandlerWithLevel returns the same handler as NewTerminalHandler but only outputs
// records which are less than or equal to the specified verbosity level.
func NewTerminalHandlerWithLevel(wr io.Writer, lvl slog.Level, useColor bool) *TerminalHandler {
func NewTerminalHandlerWithLevel(wr io.Writer, lvl *slog.LevelVar, useColor bool) *TerminalHandler {
return &TerminalHandler{
wr: wr,
lvl: lvl,
Expand All @@ -99,7 +101,7 @@ func (h *TerminalHandler) Handle(_ context.Context, r slog.Record) error {
}

func (h *TerminalHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= h.lvl
return level.Level() >= h.lvl.Level()
}

func (h *TerminalHandler) WithGroup(name string) slog.Handler {
Expand All @@ -123,20 +125,22 @@ func (t *TerminalHandler) ResetFieldPadding() {
t.mu.Unlock()
}

type leveler struct{ minLevel slog.Level }
type leveler struct{ minLevel *slog.LevelVar }

func (l *leveler) Level() slog.Level {
return l.minLevel
return l.minLevel.Level()
}

// JSONHandler returns a handler which prints records in JSON format.
func JSONHandler(wr io.Writer) slog.Handler {
return JSONHandlerWithLevel(wr, levelMaxVerbosity)
var level slog.LevelVar
level.Set(levelMaxVerbosity)
return JSONHandlerWithLevel(wr, &level)
}

// JSONHandlerWithLevel returns a handler which prints records in JSON format that are less than or equal to
// the specified verbosity level.
func JSONHandlerWithLevel(wr io.Writer, level slog.Level) slog.Handler {
func JSONHandlerWithLevel(wr io.Writer, level *slog.LevelVar) slog.Handler {
return slog.NewJSONHandler(wr, &slog.HandlerOptions{
ReplaceAttr: builtinReplaceJSON,
Level: &leveler{level},
Expand All @@ -155,7 +159,7 @@ func LogfmtHandler(wr io.Writer) slog.Handler {

// LogfmtHandlerWithLevel returns the same handler as LogfmtHandler but it only outputs
// records which are less than or equal to the specified verbosity level.
func LogfmtHandlerWithLevel(wr io.Writer, level slog.Level) slog.Handler {
func LogfmtHandlerWithLevel(wr io.Writer, level *slog.LevelVar) slog.Handler {
return slog.NewTextHandler(wr, &slog.HandlerOptions{
ReplaceAttr: builtinReplaceLogfmt,
Level: &leveler{level},
Expand Down
Loading
Loading