Skip to content

Commit

Permalink
Add runtime log level control via HTTP (Fix #1181)
Browse files Browse the repository at this point in the history
Introduce HTTP server to dynamically adjust log levels at runtime.
Refactor log level handling to use a new LogLevel struct.
Update tests and documentation to reflect these changes.

Enhances logging flexibility and improves runtime configurability.
  • Loading branch information
derrix060 committed Nov 5, 2024
1 parent 3e1dcf2 commit 911e66a
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 97 deletions.
18 changes: 15 additions & 3 deletions cmd/juno/juno.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const (
corsEnableF = "rpc-cors-enable"
versionedConstantsFileF = "versioned-constants-file"
pluginPathF = "plugin-path"
logHostF = "log-host"
logPortF = "log-port"

defaultConfig = ""
defaulHost = "localhost"
Expand Down Expand Up @@ -123,6 +125,7 @@ const (
defaultCorsEnable = false
defaultVersionedConstantsFile = ""
defaultPluginPath = ""
defaultLogPort = 0

configFlagUsage = "The YAML configuration file."
logLevelFlagUsage = "Options: trace, debug, info, warn, error."
Expand Down Expand Up @@ -176,6 +179,8 @@ const (
corsEnableUsage = "Enable CORS on RPC endpoints"
versionedConstantsFileUsage = "Use custom versioned constants from provided file"
pluginPathUsage = "Path to the plugin .so file"
logHostUsage = "The interface on which the log level HTTP server will listen for requests."
logPortUsage = "The port on which the log level HTTP server will listen for requests."
)

var Version string
Expand All @@ -202,7 +207,13 @@ 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
}
Expand Down Expand Up @@ -307,13 +318,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)
Expand Down Expand Up @@ -364,6 +374,8 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr
junoCmd.Flags().String(versionedConstantsFileF, defaultVersionedConstantsFile, versionedConstantsFileUsage)
junoCmd.MarkFlagsMutuallyExclusive(p2pFeederNodeF, p2pPeersF)
junoCmd.Flags().String(pluginPathF, defaultPluginPath, pluginPathUsage)
junoCmd.Flags().String(logHostF, defaulHost, logHostUsage)
junoCmd.Flags().Uint16(logPortF, defaultLogPort, logPortUsage)

junoCmd.AddCommand(GenP2PKeyPair(), DBCmd(defaultDBPath))

Expand Down
18 changes: 9 additions & 9 deletions cmd/juno/juno_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions db/pebble/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions docs/docs/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,31 @@ 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.

To enable this feature, use the following configuration options:

- `log-host`: The interface to listen for requests. Defaults to `localhost`.
- `log-port`: The port to listen for requests. REQUIRED

Examples:

```console
# Start juno specifying the log port
juno --log-port=6789 --log-level=error ...

# Get current level
curl -X GET 'localhost:6789/log/level'
error

# Set level
curl -X PUT 'localhost:6789/log/level?level=trace'
Replaced log level with 'trace' successfully

# Get new level
curl -X GET 'localhost:6789/log/level'
trace
```
10 changes: 10 additions & 0 deletions node/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ func makeMetrics(host string, port uint16) *httpService {
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{Registry: prometheus.DefaultRegisterer}))
}

// Create a new service that updates the log level setting.
func makeLogService(host string, port uint16, logLevel *utils.LogLevel) *httpService {
mux := http.NewServeMux()
mux.HandleFunc("/log/level", func(w http.ResponseWriter, r *http.Request) {
utils.HTTPLogSettings(w, r, logLevel)
})
var handler http.Handler = mux
return makeHTTPService(host, port, handler)
}

type grpcService struct {
srv *grpc.Server
host string
Expand Down
55 changes: 31 additions & 24 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const (

// Config is the top-level juno configuration.
type Config struct {
LogLevel utils.LogLevel `mapstructure:"log-level"`
LogLevel string `mapstructure:"log-level"`
HTTP bool `mapstructure:"http"`
HTTPHost string `mapstructure:"http-host"`
HTTPPort uint16 `mapstructure:"http-port"`
Expand Down Expand Up @@ -92,24 +92,27 @@ type Config struct {
GatewayTimeout time.Duration `mapstructure:"gw-timeout"`

PluginPath string `mapstructure:"plugin-path"`

LogHost string `mapstructure:"log-host"`
LogPort uint16 `mapstructure:"log-port"`
}

type Node struct {
cfg *Config
db db.DB
blockchain *blockchain.Blockchain

metricsService service.Service // Start the metrics service earlier than other services.
services []service.Service
log utils.Logger
earlyServices []service.Service // Services that needs to start before than other services and before migration.
services []service.Service
log utils.Logger

version string
}

// 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
}
Expand All @@ -128,6 +131,7 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen
ua := fmt.Sprintf("Juno/%s Starknet Client", version)

services := make([]service.Service, 0)
earlyServices := make([]service.Service, 0)

chain := blockchain.New(database, &cfg.Network)

Expand Down Expand Up @@ -233,7 +237,10 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen
services = append(services,
makeRPCOverWebsocket(cfg.WebsocketHost, cfg.WebsocketPort, rpcServers, log, cfg.Metrics, cfg.RPCCorsEnable))
}
var metricsService service.Service
if cfg.LogPort != 0 {
log.Infow("Log level can be changed via HTTP PUT request to " + cfg.LogHost + ":" + fmt.Sprintf("%d", cfg.LogPort) + "/log/level")
earlyServices = append(earlyServices, makeLogService(cfg.LogHost, cfg.LogPort, logLevel))
}
if cfg.Metrics {
makeJeMallocMetrics()
makeVMThrottlerMetrics(throttledVM)
Expand All @@ -246,7 +253,7 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen
jsonrpcServerLegacy.WithListener(legacyRPCMetrics)
client.WithListener(makeFeederMetrics())
gatewayClient.WithListener(makeGatewayMetrics())
metricsService = makeMetrics(cfg.MetricsHost, cfg.MetricsPort)
earlyServices = append(earlyServices, makeMetrics(cfg.MetricsHost, cfg.MetricsPort))

if synchronizer != nil {
synchronizer.WithListener(makeSyncMetrics(synchronizer, chain))
Expand All @@ -263,14 +270,15 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen
services = append(services, makePPROF(cfg.PprofHost, cfg.PprofPort))
}


n := &Node{
cfg: cfg,
log: log,
version: version,
db: database,
blockchain: chain,
services: services,
metricsService: metricsService,
earlyServices: earlyServices,
}

if !n.cfg.DisableL1Verification {
Expand Down Expand Up @@ -351,13 +359,8 @@ func (n *Node) Run(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

if n.metricsService != nil {
wg.Go(func() {
defer cancel()
if err := n.metricsService.Run(ctx); err != nil {
n.log.Errorw("Metrics error", "err", err)
}
})
for _, s := range n.earlyServices {
n.StartService(wg, ctx, cancel, s)
}

if err := migration.MigrateIfNeeded(ctx, n.db, &n.cfg.Network, n.log); err != nil {
Expand All @@ -370,20 +373,24 @@ func (n *Node) Run(ctx context.Context) {
}

for _, s := range n.services {
wg.Go(func() {
// Immediately acknowledge panicing services by shutting down the node
// Without the deffered cancel(), we would have to wait for user to hit Ctrl+C
defer cancel()
if err := s.Run(ctx); err != nil {
n.log.Errorw("Service error", "name", reflect.TypeOf(s), "err", err)
}
})
n.StartService(wg, ctx, cancel, s)
}

<-ctx.Done()
n.log.Infow("Shutting down Juno...")
}

func (n *Node) StartService (wg *conc.WaitGroup, ctx context.Context, cancel context.CancelFunc, s service.Service) {
wg.Go(func() {
// Immediately acknowledge panicing services by shutting down the node
// Without the deffered cancel(), we would have to wait for user to hit Ctrl+C
defer cancel()
if err := s.Run(ctx); err != nil {
n.log.Errorw("Service error", "name", reflect.TypeOf(s), "err", err)
}
})
}

func (n *Node) Config() Config {
return *n.cfg
}
8 changes: 5 additions & 3 deletions node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,7 +40,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())
Expand Down Expand Up @@ -77,11 +78,12 @@ 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,
DisableL1Verification: true,
}, "v0.1")
}, "v0.1", logLevel)
if test.errString == "" {
require.NoError(t, err)
} else {
Expand Down
Loading

0 comments on commit 911e66a

Please sign in to comment.