Skip to content

Commit

Permalink
Add log encoding option for JSON logging.
Browse files Browse the repository at this point in the history
This option allows selection of either CONSOLE (the default, and the
current behaviour) or JSON for log output, allowing for easier parsing
of output for structued log backends.
  • Loading branch information
mcdee committed Sep 20, 2024
1 parent 37ed095 commit f2a81b9
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 34 deletions.
12 changes: 9 additions & 3 deletions cmd/juno/juno.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Juno is a Go implementation of a Starknet full-node client created by Nethermind
const (
configF = "config"
logLevelF = "log-level"
logEncodingF = "log-encoding"
httpF = "http"
httpHostF = "http-host"
httpPortF = "http-port"
Expand Down Expand Up @@ -122,6 +123,7 @@ const (

configFlagUsage = "The YAML configuration file."
logLevelFlagUsage = "Options: trace, debug, info, warn, error."
logEncodingFlagUsage = "The encoding of the log: console, json."
httpUsage = "Enables the HTTP RPC server on the default port and interface."
httpHostUsage = "The interface on which the HTTP RPC server will listen for requests."
httpPortUsage = "The port on which the HTTP server will listen for requests."
Expand Down Expand Up @@ -191,9 +193,11 @@ func main() {

config := new(node.Config)
cmd := NewCmd(config, func(cmd *cobra.Command, _ []string) error {
_, err := fmt.Fprintf(cmd.OutOrStdout(), greeting, Version)
if err != nil {
return err
if config.LogEncoding == utils.CONSOLE {
_, err := fmt.Fprintf(cmd.OutOrStdout(), greeting, Version)
if err != nil {
return err
}
}

n, err := node.New(config, Version)
Expand Down Expand Up @@ -302,12 +306,14 @@ 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
defaultLogEncoding := utils.CONSOLE
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().Var(&defaultLogEncoding, logEncodingF, logEncodingFlagUsage)
junoCmd.Flags().Bool(httpF, defaultHTTP, httpUsage)
junoCmd.Flags().String(httpHostF, defaulHost, httpHostUsage)
junoCmd.Flags().Uint16(httpPortF, defaultHTTPPort, httpPortUsage)
Expand Down
17 changes: 17 additions & 0 deletions cmd/juno/juno_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestConfigPrecedence(t *testing.T) {
// checks on the config, those will be checked by the node implementation.
defaultHost := "localhost"
defaultLogLevel := utils.INFO
defaultLogEncoding := utils.CONSOLE
defaultHTTP := false
defaultHTTPPort := uint16(6060)
defaultWS := false
Expand Down Expand Up @@ -84,6 +85,7 @@ func TestConfigPrecedence(t *testing.T) {
},
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: utils.CONSOLE,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -115,6 +117,7 @@ func TestConfigPrecedence(t *testing.T) {
"custom network config file": {
cfgFile: true,
cfgFileContents: `log-level: debug
log-encoding: JSON
http-host: 0.0.0.0
http-port: 4576
db-path: /home/.juno
Expand All @@ -129,6 +132,7 @@ cn-unverifiable-range: [0,10]
`,
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: utils.JSON,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -161,6 +165,7 @@ cn-unverifiable-range: [0,10]
inputArgs: []string{""},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down Expand Up @@ -193,6 +198,7 @@ cn-unverifiable-range: [0,10]
inputArgs: []string{"--config", ""},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down Expand Up @@ -230,6 +236,7 @@ cn-unverifiable-range: [0,10]
cfgFileContents: "\n",
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down Expand Up @@ -269,6 +276,7 @@ pprof: true
`,
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -305,6 +313,7 @@ http-port: 4576
`,
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -340,6 +349,7 @@ http-port: 4576
},
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -375,6 +385,7 @@ http-port: 4576
},
expectedConfig: &node.Config{
LogLevel: utils.DEBUG,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -434,6 +445,7 @@ db-cache-size: 8
},
expectedConfig: &node.Config{
LogLevel: utils.ERROR,
LogEncoding: defaultLogEncoding,
HTTP: true,
HTTPHost: "127.0.0.1",
HTTPPort: 4577,
Expand Down Expand Up @@ -472,6 +484,7 @@ network: sepolia
inputArgs: []string{"--db-path", "/home/flag/.juno"},
expectedConfig: &node.Config{
LogLevel: utils.WARN,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 4576,
Expand Down Expand Up @@ -506,6 +519,7 @@ network: sepolia
inputArgs: []string{"--db-path", "/home/flag/.juno", "--pprof"},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down Expand Up @@ -538,6 +552,7 @@ network: sepolia
env: []string{"JUNO_HTTP_PORT", "8080", "JUNO_WS", "true", "JUNO_HTTP_HOST", "0.0.0.0"},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: "0.0.0.0",
HTTPPort: 8080,
Expand Down Expand Up @@ -571,6 +586,7 @@ network: sepolia
inputArgs: []string{"--db-path", "/home/flag/.juno"},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down Expand Up @@ -604,6 +620,7 @@ network: sepolia
env: []string{"JUNO_DB_PATH", "/home/env/.juno", "JUNO_GW_API_KEY", "apikey"},
expectedConfig: &node.Config{
LogLevel: defaultLogLevel,
LogEncoding: defaultLogEncoding,
HTTP: defaultHTTP,
HTTPHost: defaultHost,
HTTPPort: defaultHTTPPort,
Expand Down
4 changes: 2 additions & 2 deletions db/pebble/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ func New(path string) (db.DB, error) {
return newPebble(path, nil)
}

func NewWithOptions(path string, cacheSizeMB uint, maxOpenFiles int, colouredLogger bool) (db.DB, error) {
func NewWithOptions(path string, cacheSizeMB uint, maxOpenFiles int, colouredLogger bool, logEncoding utils.LogEncoding) (db.DB, error) {
// Ensure that the specified cache size meets a minimum threshold.
cacheSizeMB = max(cacheSizeMB, minCacheSizeMB)

dbLog, err := utils.NewZapLogger(utils.ERROR, colouredLogger)
dbLog, err := utils.NewZapLogger(utils.ERROR, colouredLogger, logEncoding)
if err != nil {
return nil, fmt.Errorf("create DB logger: %w", err)
}
Expand Down
47 changes: 24 additions & 23 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,28 @@ 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 utils.LogLevel `mapstructure:"log-level"`
LogEncoding utils.LogEncoding `mapstructure:"log-encoding"`
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"`
Expand Down Expand Up @@ -105,7 +106,7 @@ 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)
log, err := utils.NewZapLogger(cfg.LogLevel, cfg.Colour, cfg.LogEncoding)
if err != nil {
return nil, err
}
Expand All @@ -115,7 +116,7 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen
if dbIsRemote {
database, err = remote.New(cfg.RemoteDB, context.TODO(), log, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
database, err = pebble.NewWithOptions(cfg.DatabasePath, cfg.DBCacheSize, cfg.DBMaxHandles, cfg.Colour)
database, err = pebble.NewWithOptions(cfg.DatabasePath, cfg.DBCacheSize, cfg.DBMaxHandles, cfg.Colour, cfg.LogEncoding)
}

if err != nil {
Expand Down
65 changes: 62 additions & 3 deletions utils/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,65 @@ func (l *LogLevel) UnmarshalText(text []byte) error {
return l.Set(string(text))
}

var ErrUnknownLogEncoding = fmt.Errorf(
"unknown log encoding (known: %s, %s)",
CONSOLE, JSON,
)

type LogEncoding int

// The following are necessary for Cobra and Viper, respectively, to unmarshal log level
// CLI/config parameters properly.
var (
_ pflag.Value = (*LogEncoding)(nil)
_ encoding.TextUnmarshaler = (*LogEncoding)(nil)
)

const (
CONSOLE LogEncoding = iota
JSON
)

func (l LogEncoding) String() string {
switch l {
case CONSOLE:
return "console"
case JSON:
return "json"
default:
// Should not happen.
panic(ErrUnknownLogEncoding)
}
}

func (l LogEncoding) MarshalYAML() (interface{}, error) {
return l.String(), nil
}

func (l *LogEncoding) Set(s string) error {
switch s {
case "CONSOLE", "console":
*l = CONSOLE
case "JSON", "json":
*l = JSON
default:
return ErrUnknownLogEncoding
}
return nil
}

func (l *LogEncoding) Type() string {
return "LogEncoding"
}

func (l *LogEncoding) MarshalText() ([]byte, error) {
return []byte(l.String()), nil
}

func (l *LogEncoding) UnmarshalText(text []byte) error {
return l.Set(string(text))
}

type Logger interface {
SimpleLogger
pebble.Logger
Expand Down Expand Up @@ -127,12 +186,12 @@ func NewNopZapLogger() *ZapLogger {
return &ZapLogger{zap.NewNop().Sugar()}
}

func NewZapLogger(logLevel LogLevel, colour bool) (*ZapLogger, error) {
func NewZapLogger(logLevel LogLevel, colour bool, encoding LogEncoding) (*ZapLogger, error) {
config := zap.NewProductionConfig()
config.Sampling = nil
config.Encoding = "console"
config.Encoding = encoding.String()
config.EncoderConfig.EncodeLevel = capitalColorLevelEncoder
if !colour {
if encoding == JSON || !colour {
config.EncoderConfig.EncodeLevel = capitalLevelEncoder
}
config.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
Expand Down
4 changes: 2 additions & 2 deletions utils/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func TestLogLevelType(t *testing.T) {
func TestZapWithColour(t *testing.T) {
for level, str := range levelStrings {
t.Run("level: "+str, func(t *testing.T) {
_, err := utils.NewZapLogger(level, true)
_, err := utils.NewZapLogger(level, true, utils.CONSOLE)
assert.NoError(t, err)
})
}
Expand All @@ -102,7 +102,7 @@ func TestZapWithColour(t *testing.T) {
func TestZapWithoutColour(t *testing.T) {
for level, str := range levelStrings {
t.Run("level: "+str, func(t *testing.T) {
_, err := utils.NewZapLogger(level, false)
_, err := utils.NewZapLogger(level, false, utils.CONSOLE)
assert.NoError(t, err)
})
}
Expand Down
2 changes: 1 addition & 1 deletion vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestV1Call(t *testing.T) {
*classHash: simpleClass,
}))

log, err := utils.NewZapLogger(utils.ERROR, false)
log, err := utils.NewZapLogger(utils.ERROR, false, utils.CONSOLE)
require.NoError(t, err)

// test_storage_read
Expand Down

0 comments on commit f2a81b9

Please sign in to comment.