diff --git a/cmd/juno/juno.go b/cmd/juno/juno.go index 08ce3f840..3d8a8abd9 100644 --- a/cmd/juno/juno.go +++ b/cmd/juno/juno.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "net/http" "os" "os/signal" "path/filepath" @@ -199,11 +200,35 @@ func main() { return err } - n, err := node.New(config, Version) + logLevel := utils.NewLogLevel(utils.INFO) + err = logLevel.Set(config.LogLevel) if err != nil { return err } + n, err := node.New(config, Version, logLevel) + if err != nil { + return err + } + + // Create a HTTP server to control the log level. + http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) { + utils.HTTPLogSettings(w, r, logLevel) + }) + go func() { + server := &http.Server{ + Addr: ":6789", + Handler: nil, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 15 * time.Second, + } + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + fmt.Printf("HTTP server ListenAndServe: %v", err) + } + }() + n.Run(cmd.Context()) return nil }) @@ -304,13 +329,12 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr // For testing purposes, these variables cannot be declared outside the function because Cobra // may mutate their values. - defaultLogLevel := utils.INFO defaultNetwork := utils.Mainnet defaultMaxVMs := 3 * runtime.GOMAXPROCS(0) defaultCNUnverifiableRange := []int{} // Uint64Slice is not supported in Flags() junoCmd.Flags().StringVar(&cfgFile, configF, defaultConfig, configFlagUsage) - junoCmd.Flags().Var(&defaultLogLevel, logLevelF, logLevelFlagUsage) + junoCmd.Flags().String(logLevelF, utils.INFO.String(), logLevelFlagUsage) junoCmd.Flags().Bool(httpF, defaultHTTP, httpUsage) junoCmd.Flags().String(httpHostF, defaulHost, httpHostUsage) junoCmd.Flags().Uint16(httpPortF, defaultHTTPPort, httpPortUsage) diff --git a/cmd/juno/juno_test.go b/cmd/juno/juno_test.go index 7a3865407..be92ab8d2 100644 --- a/cmd/juno/juno_test.go +++ b/cmd/juno/juno_test.go @@ -30,7 +30,7 @@ func TestConfigPrecedence(t *testing.T) { // tested for sanity. These tests are not intended to perform semantics // checks on the config, those will be checked by the node implementation. defaultHost := "localhost" - defaultLogLevel := utils.INFO + defaultLogLevel := "info" defaultHTTP := false defaultHTTPPort := uint16(6060) defaultWS := false @@ -83,7 +83,7 @@ func TestConfigPrecedence(t *testing.T) { "--cn-core-contract-address", "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4", }, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -128,7 +128,7 @@ cn-core-contract-address: 0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4 cn-unverifiable-range: [0,10] `, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -268,7 +268,7 @@ network: sepolia pprof: true `, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -304,7 +304,7 @@ http-host: 0.0.0.0 http-port: 4576 `, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -339,7 +339,7 @@ http-port: 4576 "--db-path", "/home/.juno", "--network", "sepolia-integration", "--pprof", "--db-cache-size", "1024", }, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -374,7 +374,7 @@ http-port: 4576 "--network", "sepolia", }, expectedConfig: &node.Config{ - LogLevel: utils.DEBUG, + LogLevel: "debug", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, @@ -433,7 +433,7 @@ db-cache-size: 1024 "--db-cache-size", "9", }, expectedConfig: &node.Config{ - LogLevel: utils.ERROR, + LogLevel: "error", HTTP: true, HTTPHost: "127.0.0.1", HTTPPort: 4577, @@ -471,7 +471,7 @@ network: sepolia `, inputArgs: []string{"--db-path", "/home/flag/.juno"}, expectedConfig: &node.Config{ - LogLevel: utils.WARN, + LogLevel: "warn", HTTP: defaultHTTP, HTTPHost: "0.0.0.0", HTTPPort: 4576, diff --git a/db/pebble/db.go b/db/pebble/db.go index 5974edf72..1a10d35bb 100644 --- a/db/pebble/db.go +++ b/db/pebble/db.go @@ -39,8 +39,8 @@ func New(path string) (db.DB, error) { func NewWithOptions(path string, cacheSizeMB uint, maxOpenFiles int, colouredLogger bool) (db.DB, error) { // Ensure that the specified cache size meets a minimum threshold. cacheSizeMB = max(cacheSizeMB, minCacheSizeMB) - - dbLog, err := utils.NewZapLogger(utils.ERROR, colouredLogger) + log := utils.NewLogLevel(utils.ERROR) + dbLog, err := utils.NewZapLogger(log, colouredLogger) if err != nil { return nil, fmt.Errorf("create DB logger: %w", err) } diff --git a/docs/docs/monitoring.md b/docs/docs/monitoring.md index f0abb49b3..8cd67cee8 100644 --- a/docs/docs/monitoring.md +++ b/docs/docs/monitoring.md @@ -85,3 +85,15 @@ To have Juno write logs to a file, use the command: ![Grafana dashboard](/img/grafana-1.png) ![Grafana dashboard](/img/grafana-2.png) + +## Change log level in runtime + +In case you want to change the log level in runtime without the need to restart the juno process, you can do it via HTTP calls. + +Examples: + +```console +curl -X PUT 'localhost:6789/log?level=trace' +curl -X PUT 'localhost:6789/log?level=info' +curl -X GET 'localhost:6789/log' +``` diff --git a/node/node.go b/node/node.go index 63f0bb132..23b12728f 100644 --- a/node/node.go +++ b/node/node.go @@ -46,27 +46,27 @@ const ( // Config is the top-level juno configuration. type Config struct { - LogLevel utils.LogLevel `mapstructure:"log-level"` - HTTP bool `mapstructure:"http"` - HTTPHost string `mapstructure:"http-host"` - HTTPPort uint16 `mapstructure:"http-port"` - RPCCorsEnable bool `mapstructure:"rpc-cors-enable"` - Websocket bool `mapstructure:"ws"` - WebsocketHost string `mapstructure:"ws-host"` - WebsocketPort uint16 `mapstructure:"ws-port"` - GRPC bool `mapstructure:"grpc"` - GRPCHost string `mapstructure:"grpc-host"` - GRPCPort uint16 `mapstructure:"grpc-port"` - DatabasePath string `mapstructure:"db-path"` - Network utils.Network `mapstructure:"network"` - EthNode string `mapstructure:"eth-node"` - Pprof bool `mapstructure:"pprof"` - PprofHost string `mapstructure:"pprof-host"` - PprofPort uint16 `mapstructure:"pprof-port"` - Colour bool `mapstructure:"colour"` - PendingPollInterval time.Duration `mapstructure:"pending-poll-interval"` - RemoteDB string `mapstructure:"remote-db"` - VersionedConstantsFile string `mapstructure:"versioned-constants-file"` + LogLevel string `mapstructure:"log-level"` + HTTP bool `mapstructure:"http"` + HTTPHost string `mapstructure:"http-host"` + HTTPPort uint16 `mapstructure:"http-port"` + RPCCorsEnable bool `mapstructure:"rpc-cors-enable"` + Websocket bool `mapstructure:"ws"` + WebsocketHost string `mapstructure:"ws-host"` + WebsocketPort uint16 `mapstructure:"ws-port"` + GRPC bool `mapstructure:"grpc"` + GRPCHost string `mapstructure:"grpc-host"` + GRPCPort uint16 `mapstructure:"grpc-port"` + DatabasePath string `mapstructure:"db-path"` + Network utils.Network `mapstructure:"network"` + EthNode string `mapstructure:"eth-node"` + Pprof bool `mapstructure:"pprof"` + PprofHost string `mapstructure:"pprof-host"` + PprofPort uint16 `mapstructure:"pprof-port"` + Colour bool `mapstructure:"colour"` + PendingPollInterval time.Duration `mapstructure:"pending-poll-interval"` + RemoteDB string `mapstructure:"remote-db"` + VersionedConstantsFile string `mapstructure:"versioned-constants-file"` Metrics bool `mapstructure:"metrics"` MetricsHost string `mapstructure:"metrics-host"` @@ -107,8 +107,8 @@ type Node struct { // New sets the config and logger to the StarknetNode. // Any errors while parsing the config on creating logger will be returned. -func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen - log, err := utils.NewZapLogger(cfg.LogLevel, cfg.Colour) +func New(cfg *Config, version string, logLevel *utils.LogLevel) (*Node, error) { //nolint:gocyclo,funlen + log, err := utils.NewZapLogger(logLevel, cfg.Colour) if err != nil { return nil, err } diff --git a/node/node_test.go b/node/node_test.go index a9287e0e4..a2d7fd457 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -18,7 +18,7 @@ import ( // Create a new node with all services enabled. func TestNewNode(t *testing.T) { config := &node.Config{ - LogLevel: utils.INFO, + LogLevel: "info", HTTP: true, HTTPPort: 0, Websocket: true, @@ -39,7 +39,8 @@ func TestNewNode(t *testing.T) { P2PPeers: "", } - n, err := node.New(config, "v0.3") + logLevel := utils.NewLogLevel(utils.INFO) + n, err := node.New(config, "v0.3", logLevel) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) @@ -76,10 +77,11 @@ func TestNetworkVerificationOnNonEmptyDB(t *testing.T) { cancel() require.NoError(t, database.Close()) + logLevel := utils.NewLogLevel(utils.INFO) _, err = node.New(&node.Config{ DatabasePath: dbPath, Network: test.network, - }, "v0.1") + }, "v0.1", logLevel) if test.errString == "" { require.NoError(t, err) } else { diff --git a/utils/log.go b/utils/log.go index 3d88ef3cb..45aa5db03 100644 --- a/utils/log.go +++ b/utils/log.go @@ -3,6 +3,7 @@ package utils import ( "encoding" "fmt" + "net/http" "strings" "time" @@ -17,7 +18,13 @@ var ErrUnknownLogLevel = fmt.Errorf( TRACE, DEBUG, INFO, WARN, ERROR, ) -type LogLevel int +type LogLevel struct { + atomicLevel zap.AtomicLevel +} + +func NewLogLevel(level zapcore.Level) *LogLevel { + return &LogLevel{atomicLevel: zap.NewAtomicLevelAt(level)} +} // The following are necessary for Cobra and Viper, respectively, to unmarshal log level // CLI/config parameters properly. @@ -27,15 +34,15 @@ var ( ) const ( - DEBUG LogLevel = iota + TRACE zapcore.Level = iota - 2 + DEBUG INFO WARN ERROR - TRACE ) func (l LogLevel) String() string { - switch l { + switch l.Level() { case DEBUG: return "debug" case INFO: @@ -52,22 +59,26 @@ func (l LogLevel) String() string { } } +func (l LogLevel) Level() zapcore.Level { + return l.atomicLevel.Level() +} + func (l LogLevel) MarshalYAML() (interface{}, error) { return l.String(), nil } func (l *LogLevel) Set(s string) error { - switch s { - case "DEBUG", "debug": - *l = DEBUG - case "INFO", "info": - *l = INFO - case "WARN", "warn": - *l = WARN - case "ERROR", "error": - *l = ERROR - case "TRACE", "trace": - *l = TRACE + switch strings.ToUpper(s) { + case "DEBUG": + l.atomicLevel.SetLevel(DEBUG) + case "INFO": + l.atomicLevel.SetLevel(INFO) + case "WARN": + l.atomicLevel.SetLevel(WARN) + case "ERROR": + l.atomicLevel.SetLevel(ERROR) + case "TRACE": + l.atomicLevel.SetLevel(TRACE) default: return ErrUnknownLogLevel } @@ -103,10 +114,8 @@ type ZapLogger struct { *zap.SugaredLogger } -const traceLevel = zapcore.Level(-2) - func (l *ZapLogger) IsTraceEnabled() bool { - return l.Desugar().Core().Enabled(traceLevel) + return l.Desugar().Core().Enabled(TRACE) } func (l *ZapLogger) Tracew(msg string, keysAndValues ...interface{}) { @@ -117,7 +126,7 @@ func (l *ZapLogger) Tracew(msg string, keysAndValues ...interface{}) { // also check this issue https://github.com/uber-go/zap/issues/930 for updates // AddCallerSkip(1) is necessary to skip the caller of this function - l.WithOptions(zap.AddCallerSkip(1)).Logw(traceLevel, msg, keysAndValues...) + l.WithOptions(zap.AddCallerSkip(1)).Logw(TRACE, msg, keysAndValues...) } } @@ -127,7 +136,7 @@ func NewNopZapLogger() *ZapLogger { return &ZapLogger{zap.NewNop().Sugar()} } -func NewZapLogger(logLevel LogLevel, colour bool) (*ZapLogger, error) { +func NewZapLogger(logLevel *LogLevel, colour bool) (*ZapLogger, error) { config := zap.NewProductionConfig() config.Sampling = nil config.Encoding = "console" @@ -139,18 +148,7 @@ func NewZapLogger(logLevel LogLevel, colour bool) (*ZapLogger, error) { enc.AppendString(t.Local().Format("15:04:05.000 02/01/2006 -07:00")) } - var level zapcore.Level - var err error - levelStr := logLevel.String() - if logLevel == TRACE { - level = traceLevel - } else { - level, err = zapcore.ParseLevel(levelStr) - if err != nil { - return nil, err - } - } - config.Level.SetLevel(level) + config.Level = logLevel.atomicLevel log, err := config.Build() if err != nil { return nil, err @@ -179,9 +177,8 @@ func (c colour) Add(s string) string { // capitalColorLevelEncoder adds support for TRACE log level to the default CapitalColorLevelEncoder func capitalColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { - if l == traceLevel { - tracePrefix := strings.ToUpper(TRACE.String()) - enc.AppendString(cyan.Add(tracePrefix)) + if l == TRACE { + enc.AppendString(cyan.Add("TRACE")) } else { zapcore.CapitalColorLevelEncoder(l, enc) } @@ -189,10 +186,35 @@ func capitalColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder // capitalLevelEncoder adds support for TRACE log level to the default CapitalLevelEncoder func capitalLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { - if l == traceLevel { - tracePrefix := strings.ToUpper(TRACE.String()) - enc.AppendString(tracePrefix) + if l == TRACE { + enc.AppendString("TRACE") } else { zapcore.CapitalLevelEncoder(l, enc) } } + +// HTTPLogSettings is an HTTP handler that allows changing the log level of the logger. +// It can also be used to query what's the current log level. +func HTTPLogSettings(w http.ResponseWriter, r *http.Request, log *LogLevel) { + switch r.Method { + case http.MethodGet: + fmt.Fprint(w, log.String()) + w.WriteHeader(http.StatusOK) + case http.MethodPut: + levelStr := r.URL.Query().Get("level") + if levelStr == "" { + http.Error(w, "missing level query parameter", http.StatusBadRequest) + return + } + + err := log.Set(levelStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Fprint(w, "Replaced log level with ", levelStr, " successfully") + w.WriteHeader(http.StatusOK) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/utils/log_test.go b/utils/log_test.go index 8a5954b87..6c4458a05 100644 --- a/utils/log_test.go +++ b/utils/log_test.go @@ -10,12 +10,12 @@ import ( "github.com/stretchr/testify/require" ) -var levelStrings = map[utils.LogLevel]string{ - utils.DEBUG: "debug", - utils.INFO: "info", - utils.WARN: "warn", - utils.ERROR: "error", - utils.TRACE: "trace", +var levelStrings = map[*utils.LogLevel]string{ + utils.NewLogLevel(utils.DEBUG): "debug", + utils.NewLogLevel(utils.INFO): "info", + utils.NewLogLevel(utils.WARN): "warn", + utils.NewLogLevel(utils.ERROR): "error", + utils.NewLogLevel(utils.TRACE): "trace", } func TestLogLevelString(t *testing.T) { @@ -30,20 +30,18 @@ func TestLogLevelString(t *testing.T) { // both implement the pflag.Value and encoding.TextUnmarshaller interfaces. // We can open a PR on github.com/thediveo/enumflag to add TextUnmarshaller // support. -// -//nolint:dupl func TestLogLevelSet(t *testing.T) { for level, str := range levelStrings { t.Run("level "+str, func(t *testing.T) { - l := new(utils.LogLevel) + l := utils.NewLogLevel(utils.TRACE) require.NoError(t, l.Set(str)) - assert.Equal(t, level, *l) + assert.Equal(t, *level, *l) }) uppercase := strings.ToUpper(str) t.Run("level "+uppercase, func(t *testing.T) { - l := new(utils.LogLevel) + l := utils.NewLogLevel(utils.TRACE) require.NoError(t, l.Set(uppercase)) - assert.Equal(t, level, *l) + assert.Equal(t, *level, *l) }) } @@ -56,15 +54,15 @@ func TestLogLevelSet(t *testing.T) { func TestLogLevelUnmarshalText(t *testing.T) { for level, str := range levelStrings { t.Run("level "+str, func(t *testing.T) { - l := new(utils.LogLevel) + l := utils.NewLogLevel(utils.TRACE) require.NoError(t, l.UnmarshalText([]byte(str))) - assert.Equal(t, level, *l) + assert.Equal(t, *level, *l) }) uppercase := strings.ToUpper(str) t.Run("level "+uppercase, func(t *testing.T) { - l := new(utils.LogLevel) + l := utils.NewLogLevel(utils.TRACE) require.NoError(t, l.UnmarshalText([]byte(uppercase))) - assert.Equal(t, level, *l) + assert.Equal(t, *level, *l) }) } diff --git a/utils/network_test.go b/utils/network_test.go index 778f66545..33d5b7ec5 100644 --- a/utils/network_test.go +++ b/utils/network_test.go @@ -83,7 +83,6 @@ func TestNetwork(t *testing.T) { }) } -//nolint:dupl // see comment in utils/log_test.go func TestNetworkSet(t *testing.T) { for network, str := range networkStrings { t.Run("network "+str, func(t *testing.T) { diff --git a/vm/vm_test.go b/vm/vm_test.go index 00832b606..b56b26b78 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -110,7 +110,8 @@ func TestV1Call(t *testing.T) { *classHash: simpleClass, })) - log, err := utils.NewZapLogger(utils.ERROR, false) + logLevel := utils.NewLogLevel(utils.ERROR) + log, err := utils.NewZapLogger(logLevel, false) require.NoError(t, err) // test_storage_read