diff --git a/cmd/eris-db.go b/cmd/eris-db.go index 605b12060b4f669c249dd4a04b05937f6b568262..bcd555c555d474df9736cba01280ce666da6edb1 100644 --- a/cmd/eris-db.go +++ b/cmd/eris-db.go @@ -21,17 +21,12 @@ import ( "strconv" "strings" - cobra "github.com/spf13/cobra" + "github.com/spf13/cobra" - log "github.com/eris-ltd/eris-logger" - - definitions "github.com/eris-ltd/eris-db/definitions" - version "github.com/eris-ltd/eris-db/version" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/version" ) -// Global Do struct -var do *definitions.Do - var ErisDbCmd = &cobra.Command{ Use: "eris-db", Short: "Eris-DB is the server side of the eris chain.", @@ -43,38 +38,26 @@ Made with <3 by Eris Industries. Complete documentation is available at https://monax.io/docs/documentation ` + "\nVERSION:\n " + version.VERSION, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - - log.SetLevel(log.WarnLevel) - if do.Verbose { - log.SetLevel(log.InfoLevel) - } else if do.Debug { - log.SetLevel(log.DebugLevel) - } - }, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, } func Execute() { - InitErisDbCli() - AddGlobalFlags() - AddCommands() + do := definitions.NewDo() + AddGlobalFlags(do) + AddCommands(do) ErisDbCmd.Execute() } -func InitErisDbCli() { - // initialise an empty Do struct for command execution - do = definitions.NewDo() -} - -func AddGlobalFlags() { - ErisDbCmd.PersistentFlags().BoolVarP(&do.Verbose, "verbose", "v", defaultVerbose(), "verbose output; more output than no output flags; less output than debug level; default respects $ERIS_DB_VERBOSE") - ErisDbCmd.PersistentFlags().BoolVarP(&do.Debug, "debug", "d", defaultDebug(), "debug level output; the most output available for eris-db; if it is too chatty use verbose flag; default respects $ERIS_DB_DEBUG") +func AddGlobalFlags(do *definitions.Do) { + ErisDbCmd.PersistentFlags().BoolVarP(&do.Verbose, "verbose", "v", + defaultVerbose(), + "verbose output; more output than no output flags; less output than debug level; default respects $ERIS_DB_VERBOSE") + ErisDbCmd.PersistentFlags().BoolVarP(&do.Debug, "debug", "d", defaultDebug(), + "debug level output; the most output available for eris-db; if it is too chatty use verbose flag; default respects $ERIS_DB_DEBUG") } -func AddCommands() { - buildServeCommand() - ErisDbCmd.AddCommand(ServeCmd) +func AddCommands(do *definitions.Do) { + ErisDbCmd.AddCommand(buildServeCommand(do)) } //------------------------------------------------------------------------------ diff --git a/cmd/serve.go b/cmd/serve.go index eccf3388ab0c14bc167c8211ce3a9e82208a39f4..8d23e4b3b585108040e78671be510d281d2d60f3 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -17,19 +17,19 @@ package commands import ( + "fmt" "os" "os/signal" "path" "syscall" - cobra "github.com/spf13/cobra" - - log "github.com/eris-ltd/eris-logger" - - "fmt" + "github.com/spf13/cobra" - core "github.com/eris-ltd/eris-db/core" - util "github.com/eris-ltd/eris-db/util" + "github.com/eris-ltd/eris-db/core" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/logging/lifecycle" + "github.com/eris-ltd/eris-db/logging/structure" + "github.com/eris-ltd/eris-db/util" ) const ( @@ -41,148 +41,157 @@ var DefaultConfigFilename = fmt.Sprintf("%s.%s", DefaultConfigBasename, DefaultConfigType) -var ServeCmd = &cobra.Command{ - Use: "serve", - Short: "Eris-DB serve starts an eris-db node with client API enabled by default.", - Long: `Eris-DB serve starts an eris-db node with client API enabled by default. +// build the serve subcommand +func buildServeCommand(do *definitions.Do) *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Eris-DB serve starts an eris-db node with client API enabled by default.", + Long: `Eris-DB serve starts an eris-db node with client API enabled by default. The Eris-DB node is modularly configured for the consensus engine and application manager. The client API can be disabled.`, - Example: fmt.Sprintf(`$ eris-db serve -- will start the Eris-DB node based on the configuration file "%s" in the current working directory + Example: fmt.Sprintf(`$ eris-db serve -- will start the Eris-DB node based on the configuration file "%s" in the current working directory $ eris-db serve --work-dir <path-to-working-directory> -- will start the Eris-DB node based on the configuration file "%s" in the provided working directory $ eris-db serve --chain-id <CHAIN_ID> -- will overrule the configuration entry assert_chain_id`, - DefaultConfigFilename, DefaultConfigFilename), - PreRun: func(cmd *cobra.Command, args []string) { - // if WorkDir was not set by a flag or by $ERIS_DB_WORKDIR - // NOTE [ben]: we can consider an `Explicit` flag that eliminates - // the use of any assumptions while starting Eris-DB - if do.WorkDir == "" { - if currentDirectory, err := os.Getwd(); err != nil { - log.Fatalf("No directory provided and failed to get current working directory: %v", err) + DefaultConfigFilename, DefaultConfigFilename), + PreRun: func(cmd *cobra.Command, args []string) { + // if WorkDir was not set by a flag or by $ERIS_DB_WORKDIR + // NOTE [ben]: we can consider an `Explicit` flag that eliminates + // the use of any assumptions while starting Eris-DB + if do.WorkDir == "" { + if currentDirectory, err := os.Getwd(); err != nil { + panic(fmt.Sprintf("No directory provided and failed to get current "+ + "working directory: %v", err)) + os.Exit(1) + } else { + do.WorkDir = currentDirectory + } + } + if !util.IsDir(do.WorkDir) { + panic(fmt.Sprintf("Provided working directory %s is not a directory", + do.WorkDir)) os.Exit(1) - } else { - do.WorkDir = currentDirectory } - } - if !util.IsDir(do.WorkDir) { - log.Fatalf("Provided working directory %s is not a directory", do.WorkDir) - } - }, - Run: Serve, -} - -// build the serve subcommand -func buildServeCommand() { - addServeFlags() + }, + Run: ServeRunner(do), + } + addServeFlags(do, cmd) + return cmd } -func addServeFlags() { - ServeCmd.PersistentFlags().StringVarP(&do.ChainId, "chain-id", "c", +func addServeFlags(do *definitions.Do, serveCmd *cobra.Command) { + serveCmd.PersistentFlags().StringVarP(&do.ChainId, "chain-id", "c", defaultChainId(), "specify the chain id to use for assertion against the genesis file or the existing state. If omitted, and no id is set in $CHAIN_ID, then assert_chain_id is used from the configuration file.") - ServeCmd.PersistentFlags().StringVarP(&do.WorkDir, "work-dir", "w", + serveCmd.PersistentFlags().StringVarP(&do.WorkDir, "work-dir", "w", defaultWorkDir(), "specify the working directory for the chain to run. If omitted, and no path set in $ERIS_DB_WORKDIR, the current working directory is taken.") - ServeCmd.PersistentFlags().StringVarP(&do.DataDir, "data-dir", "", + serveCmd.PersistentFlags().StringVarP(&do.DataDir, "data-dir", "", defaultDataDir(), "specify the data directory. If omitted and not set in $ERIS_DB_DATADIR, <working_directory>/data is taken.") - ServeCmd.PersistentFlags().BoolVarP(&do.DisableRpc, "disable-rpc", "", + serveCmd.PersistentFlags().BoolVarP(&do.DisableRpc, "disable-rpc", "", defaultDisableRpc(), "indicate for the RPC to be disabled. If omitted the RPC is enabled by default, unless (deprecated) $ERISDB_API is set to false.") } //------------------------------------------------------------------------------ // functions - -// serve() prepares the environment and sets up the core for Eris_DB to run. -// After the setup succeeds, serve() starts the core and halts for core to -// terminate. -func Serve(cmd *cobra.Command, args []string) { - // load configuration from a single location to avoid a wrong configuration - // file is loaded. - err := do.ReadConfig(do.WorkDir, DefaultConfigBasename, DefaultConfigType) - if err != nil { - log.WithFields(log.Fields{ - "directory": do.WorkDir, - "file": DefaultConfigFilename, - }).Fatalf("Fatal error reading configuration") - os.Exit(1) - } - // if do.ChainId is not yet set, load chain_id for assertion from configuration file - if do.ChainId == "" { - if do.ChainId = do.Config.GetString("chain.assert_chain_id"); do.ChainId == "" { - log.Fatalf("Failed to read non-empty string for ChainId from config.") - os.Exit(1) - } - } +func NewCoreFromDo(do *definitions.Do) (*core.Core, error) { // load the genesis file path do.GenesisFile = path.Join(do.WorkDir, do.Config.GetString("chain.genesis_file")) + if do.Config.GetString("chain.genesis_file") == "" { - log.Fatalf("Failed to read non-empty string for genesis file from config.") - os.Exit(1) + return nil, fmt.Errorf("The config value chain.genesis_file is empty, " + + "but should be set to the location of the genesis.json file.") } // Ensure data directory is set and accessible if err := do.InitialiseDataDirectory(); err != nil { - log.Fatalf("Failed to initialise data directory (%s): %v", do.DataDir, err) - os.Exit(1) + return nil, fmt.Errorf("Failed to initialise data directory (%s): %v", do.DataDir, err) } - log.WithFields(log.Fields{ - "chainId": do.ChainId, - "workingDirectory": do.WorkDir, - "dataDirectory": do.DataDir, - "genesisFile": do.GenesisFile, - }).Info("Eris-DB serve configuring") - consensusConfig, err := core.LoadConsensusModuleConfig(do) + loggerConfig, err := core.LoadLoggingConfig(do) if err != nil { - log.Fatalf("Failed to load consensus module configuration: %s.", err) - os.Exit(1) + return nil, fmt.Errorf("Failed to load logging config: %s", err) } - managerConfig, err := core.LoadApplicationManagerModuleConfig(do) + // Create a root logger to pass through to dependencies + logger := lifecycle.NewLoggerFromConfig(*loggerConfig) + // Capture all logging from tendermint/tendermint and tendermint/go-* + // dependencies + lifecycle.CaptureTendermintLog15Output(logger) + + cmdLogger := logger.With("command", "serve") + + // if do.ChainId is not yet set, load chain_id for assertion from configuration file + + if do.ChainId == "" { + if do.ChainId = do.Config.GetString("chain.assert_chain_id"); do.ChainId == "" { + return nil, fmt.Errorf("The config chain.assert_chain_id is empty, " + + "but should be set to the chain_id of the chain we are trying to run.") + } + } + + cmdLogger.Info("chainId", do.ChainId, + "workingDirectory", do.WorkDir, + "dataDirectory", do.DataDir, + "genesisFile", do.GenesisFile, + structure.MessageKey, "Loading configuration for serve command") + + consensusConfig, err := core.LoadConsensusModuleConfig(do) if err != nil { - log.Fatalf("Failed to load application manager module configuration: %s.", err) - os.Exit(1) + return nil, fmt.Errorf("Failed to load consensus module configuration: %s.", err) } - log.WithFields(log.Fields{ - "consensusModule": consensusConfig.Version, - "applicationManager": managerConfig.Version, - }).Debug("Modules configured") - newCore, err := core.NewCore(do.ChainId, consensusConfig, managerConfig) + managerConfig, err := core.LoadApplicationManagerModuleConfig(do) if err != nil { - log.Fatalf("Failed to load core: %s", err) + return nil, fmt.Errorf("Failed to load application manager module configuration: %s.", err) } - if !do.DisableRpc { - serverConfig, err := core.LoadServerConfig(do) - if err != nil { - log.Fatalf("Failed to load server configuration: %s.", err) - os.Exit(1) - } + cmdLogger.Info("consensusModule", consensusConfig.Version, + "applicationManager", managerConfig.Version, + structure.MessageKey, "Modules configured") + + return core.NewCore(do.ChainId, consensusConfig, managerConfig, logger) +} - serverProcess, err := newCore.NewGatewayV0(serverConfig) +// ServeRunner() returns a command runner that prepares the environment and sets +// up the core for Eris-DB to run. After the setup succeeds, it starts the core +// and waits for the core to terminate. +func ServeRunner(do *definitions.Do) func(*cobra.Command, []string) { + return func(cmd *cobra.Command, args []string) { + // load configuration from a single location to avoid a wrong configuration + // file is loaded. + err := do.ReadConfig(do.WorkDir, DefaultConfigBasename, DefaultConfigType) if err != nil { - log.Fatalf("Failed to load servers: %s.", err) - os.Exit(1) + util.Fatalf("Fatal error reading configuration from %s/%s", do.WorkDir, + DefaultConfigFilename) } - err = serverProcess.Start() + + newCore, err := NewCoreFromDo(do) + if err != nil { - log.Fatalf("Failed to start servers: %s.", err) - os.Exit(1) + util.Fatalf("Failed to load core: %s", err) } - _, err = newCore.NewGatewayTendermint(serverConfig) - if err != nil { - log.Fatalf("Failed to start Tendermint gateway") + + if !do.DisableRpc { + serverConfig, err := core.LoadServerConfig(do) + if err != nil { + util.Fatalf("Failed to load server configuration: %s.", err) + } + serverProcess, err := newCore.NewGatewayV0(serverConfig) + if err != nil { + util.Fatalf("Failed to load servers: %s.", err) + } + err = serverProcess.Start() + if err != nil { + util.Fatalf("Failed to start servers: %s.", err) + } + _, err = newCore.NewGatewayTendermint(serverConfig) + if err != nil { + util.Fatalf("Failed to start Tendermint gateway") + } + <-serverProcess.StopEventChannel() + } else { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + fmt.Fprintf(os.Stderr, "Received %s signal. Marmots out.", <-signals) } - <-serverProcess.StopEventChannel() - } else { - signals := make(chan os.Signal, 1) - done := make(chan bool, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - go func() { - signal := <-signals - // TODO: [ben] clean up core; in a manner consistent with enabled rpc - log.Fatalf("Received %s signal. Marmots out.", signal) - done <- true - }() - <-done } } diff --git a/config/config.go b/config/config.go index a37cf96f9dcfd2c13ac278fc22fa48a3869209b1..32dc2c320950ee81bd0780cae6499baabd9c8824 100644 --- a/config/config.go +++ b/config/config.go @@ -174,3 +174,14 @@ func GetConfigurationFileBytes(chainId, moniker, seeds string, chainImageName st return buffer.Bytes(), nil } + +func GetExampleConfigFileBytes() ([]byte, error) { + return GetConfigurationFileBytes( + "simplechain", + "delectable_marmot", + "192.168.168.255", + "db:latest", + true, + "46657", + "eris-db") +} diff --git a/config/config_test.go b/config/config_test.go index 79286f35b4eb6fd79923d8ced725121ea218843d..e01346e846133f931b825009eb627aa6616f4b3b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,13 +17,21 @@ package config import ( + "bytes" "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" ) -func TestConfigurationFileBytes(t *testing.T) { - // TODO: [ben] parse written bytes for comparison with expected parameters - if _, err := GetConfigurationFileBytes("simplechain", "marmot", "noseeds", "db:latest", - true, "", "eris-db"); err != nil { - t.Errorf("Error writing configuration file bytes: %s", err) - } +// Since the logic for generating configuration files (in eris-cm) is split from +// the logic for consuming them +func TestGeneratedConfigIsUsable(t *testing.T) { + bs, err := GetExampleConfigFileBytes() + assert.NoError(t, err, "Should be able to create example config") + buf := bytes.NewBuffer(bs) + conf := viper.New() + viper.SetConfigType("toml") + err = conf.ReadConfig(buf) + assert.NoError(t, err, "Should be able to read example config into Viper") } diff --git a/config/dump_config_test.go b/config/dump_config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ed71746c3ad2ff2aafe9132c572d8b6de97da530 --- /dev/null +++ b/config/dump_config_test.go @@ -0,0 +1,19 @@ +// +build dumpconfig + +// Space above matters +package config + +import ( + "io/ioutil" + "testing" + "github.com/stretchr/testify/assert" +) + +// This is a little convenience for getting a config file dump. Just run: +// go test -tags dumpconfig ./config +// This pseudo test won't run unless the dumpconfig tag is +func TestDumpConfig(t *testing.T) { + bs, err := GetExampleConfigFileBytes() + assert.NoError(t, err, "Should be able to create example config") + ioutil.WriteFile("config_dump.toml", bs, 0644) +} diff --git a/config/viper.go b/config/viper.go new file mode 100644 index 0000000000000000000000000000000000000000..fd56d89e67a20fa55cf1bcfd1e2487071acb5318 --- /dev/null +++ b/config/viper.go @@ -0,0 +1,29 @@ +package config + +import ( + "fmt" + "github.com/spf13/viper" +) + +// Safely get the subtree from a viper config, returning an error if it could not +// be obtained for any reason. +func ViperSubConfig(conf *viper.Viper, configSubtreePath string) (subConfig *viper.Viper, err error) { + // Viper internally panics if `moduleName` contains an unallowed + // character (eg, a dash). + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("Viper panicked trying to read config subtree: %s", + configSubtreePath) + } + }() + if !conf.IsSet(configSubtreePath) { + return nil, fmt.Errorf("Failed to read config subtree: %s", + configSubtreePath) + } + subConfig = conf.Sub(configSubtreePath) + if subConfig == nil { + return nil, fmt.Errorf("Failed to read config subtree: %s", + configSubtreePath) + } + return subConfig, err +} diff --git a/consensus/consensus.go b/consensus/consensus.go index f0795a5dde853f3ec6ee1aa046241aba5c25d250..03296eed028a0151a8a5ae3d696d58107caa96c9 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -28,13 +28,12 @@ func LoadConsensusEngineInPipe(moduleConfig *config.ModuleConfig, pipe definitions.Pipe) error { switch moduleConfig.Name { case "tendermint": - tendermint, err := tendermint.NewTendermint(moduleConfig, - pipe.GetApplication()) + tmint, err := tendermint.NewTendermint(moduleConfig, pipe.GetApplication()) if err != nil { return fmt.Errorf("Failed to load Tendermint node: %v", err) } - err = pipe.SetConsensusEngine(tendermint) + err = pipe.SetConsensusEngine(tmint) if err != nil { return fmt.Errorf("Failed to load Tendermint in pipe as "+ "ConsensusEngine: %v", err) @@ -42,7 +41,7 @@ func LoadConsensusEngineInPipe(moduleConfig *config.ModuleConfig, // For Tendermint we have a coupled Blockchain and ConsensusEngine // implementation, so load it at the same time as ConsensusEngine - err = pipe.SetBlockchain(tendermint) + err = pipe.SetBlockchain(tmint) if err != nil { return fmt.Errorf("Failed to load Tendermint in pipe as "+ "Blockchain: %v", err) diff --git a/consensus/tendermint/config.go b/consensus/tendermint/config.go index 1bbb74bd6aa35be73c28c7bb75adea040cfddfbb..094d7979e429ae16ec2bd3ff39e59e926895a77d 100644 --- a/consensus/tendermint/config.go +++ b/consensus/tendermint/config.go @@ -23,10 +23,10 @@ import ( "path" "time" - viper "github.com/spf13/viper" + "github.com/spf13/viper" tendermintConfig "github.com/tendermint/go-config" - config "github.com/eris-ltd/eris-db/config" + "github.com/eris-ltd/eris-db/config" ) // NOTE [ben] Compiler check to ensure TendermintConfig successfully implements @@ -147,7 +147,8 @@ func (tmintConfig *TendermintConfig) GetMapString(key string) map[string]string func (tmintConfig *TendermintConfig) GetConfig(key string) tendermintConfig.Config { // TODO: [ben] log out a warning as this indicates a potentially breaking code // change from Tendermints side - if !tmintConfig.subTree.IsSet(key) { + subTree, _ := config.ViperSubConfig(tmintConfig.subTree, key) + if subTree == nil { return &TendermintConfig{ subTree: viper.New(), } diff --git a/consensus/tendermint/tendermint.go b/consensus/tendermint/tendermint.go index 237fb4a0bd9b3da558fa73a7014b0f14546db661..87553697880fcd144c14c0bf70ff3eab3cdcf0c2 100644 --- a/consensus/tendermint/tendermint.go +++ b/consensus/tendermint/tendermint.go @@ -71,10 +71,10 @@ func NewTendermint(moduleConfig *config.ModuleConfig, if !moduleConfig.Config.IsSet("configuration") { return nil, fmt.Errorf("Failed to extract Tendermint configuration subtree.") } - tendermintConfigViper := moduleConfig.Config.Sub("configuration") + tendermintConfigViper, err := config.ViperSubConfig(moduleConfig.Config, "configuration") if tendermintConfigViper == nil { return nil, - fmt.Errorf("Failed to extract Tendermint configuration subtree.") + fmt.Errorf("Failed to extract Tendermint configuration subtree: %s", err) } // wrap a copy of the viper config in a tendermint/go-config interface tmintConfig := GetTendermintConfig(tendermintConfigViper) diff --git a/core/config.go b/core/config.go index a2a82dbd1e07e649962f8ee37e2f61b48ceb25aa..cb8239ffee07b25c1cc5039cc32a87a7670c8975 100644 --- a/core/config.go +++ b/core/config.go @@ -24,13 +24,14 @@ import ( "os" "path" - config "github.com/eris-ltd/eris-db/config" - consensus "github.com/eris-ltd/eris-db/consensus" - definitions "github.com/eris-ltd/eris-db/definitions" - manager "github.com/eris-ltd/eris-db/manager" - server "github.com/eris-ltd/eris-db/server" - util "github.com/eris-ltd/eris-db/util" - version "github.com/eris-ltd/eris-db/version" + "github.com/eris-ltd/eris-db/config" + "github.com/eris-ltd/eris-db/consensus" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/manager" + "github.com/eris-ltd/eris-db/server" + "github.com/eris-ltd/eris-db/util" + "github.com/eris-ltd/eris-db/version" "github.com/spf13/viper" ) @@ -75,17 +76,14 @@ func LoadModuleConfig(conf *viper.Viper, rootWorkDir, rootDataDir, fmt.Errorf("Failed to create module data directory %s.", dataDir) } // load configuration subtree for module - // TODO: [ben] Viper internally panics if `moduleName` contains an unallowed - // character (eg, a dash). Either this needs to be wrapped in a go-routine - // and recovered from or a PR to viper is needed to address this bug. if !conf.IsSet(moduleName) { return nil, fmt.Errorf("Failed to read configuration section for %s", moduleName) } - subConfig := conf.Sub(moduleName) + subConfig, err := config.ViperSubConfig(conf, moduleName) if subConfig == nil { - return nil, - fmt.Errorf("Failed to read configuration section for %s.", moduleName) + return nil, fmt.Errorf("Failed to read configuration section for %s: %s", + moduleName, err) } return &config.ModuleConfig{ @@ -104,19 +102,24 @@ func LoadModuleConfig(conf *viper.Viper, rootWorkDir, rootDataDir, // LoadServerModuleConfig wraps specifically for the servers run by core func LoadServerConfig(do *definitions.Do) (*server.ServerConfig, error) { // load configuration subtree for servers - if !do.Config.IsSet("servers") { - return nil, fmt.Errorf("Failed to read configuration section for servers") - } - subConfig := do.Config.Sub("servers") - if subConfig == nil { - return nil, - fmt.Errorf("Failed to read configuration section for servers") + subConfig, err := config.ViperSubConfig(do.Config, "servers") + if err != nil { + return nil, err } serverConfig, err := server.ReadServerConfig(subConfig) + if err != nil { + return nil, err + } serverConfig.ChainId = do.ChainId return serverConfig, err } +func LoadLoggingConfig(do *definitions.Do) (*logging.LoggingConfig, error) { + //subConfig, err := SubConfig(conf, "logging") + loggingConfig := &logging.LoggingConfig{} + return loggingConfig, nil +} + //------------------------------------------------------------------------------ // Helper functions diff --git a/core/core.go b/core/core.go index 03f56b56a47927b09d8b2bdc9e5a2302da1fcd84..c4e08963023b7de5ba4611a35ccf134dd378414d 100644 --- a/core/core.go +++ b/core/core.go @@ -32,6 +32,8 @@ import ( // rpc_v0 is carried over from Eris-DBv0.11 and before on port 1337 rpc_v0 "github.com/eris-ltd/eris-db/rpc/v0" // rpc_tendermint is carried over from Eris-DBv0.11 and before on port 46657 + + "github.com/eris-ltd/eris-db/logging/loggers" rpc_tendermint "github.com/eris-ltd/eris-db/rpc/tendermint/core" server "github.com/eris-ltd/eris-db/server" ) @@ -44,14 +46,16 @@ type Core struct { tendermintPipe definitions.TendermintPipe } -func NewCore(chainId string, consensusConfig *config.ModuleConfig, - managerConfig *config.ModuleConfig) (*Core, error) { +func NewCore(chainId string, + consensusConfig *config.ModuleConfig, + managerConfig *config.ModuleConfig, + logger loggers.InfoTraceLogger) (*Core, error) { // start new event switch, TODO: [ben] replace with eris-db/event evsw := events.NewEventSwitch() evsw.Start() // start a new application pipe that will load an application manager - pipe, err := manager.NewApplicationPipe(managerConfig, evsw, + pipe, err := manager.NewApplicationPipe(managerConfig, evsw, logger, consensusConfig.Version) if err != nil { return nil, fmt.Errorf("Failed to load application pipe: %v", err) @@ -65,12 +69,6 @@ func NewCore(chainId string, consensusConfig *config.ModuleConfig, if err != nil { log.Warn(fmt.Sprintf("Tendermint gateway not supported by %s", managerConfig.Version)) - return &Core{ - chainId: chainId, - evsw: evsw, - pipe: pipe, - tendermintPipe: nil, - }, nil } return &Core{ chainId: chainId, diff --git a/definitions/pipe.go b/definitions/pipe.go index f15c60df5cbe4aceda0a9421ed824c35bb0b3ea3..68b4abc07a59902f5b63b6d350c8247732fef6aa 100644 --- a/definitions/pipe.go +++ b/definitions/pipe.go @@ -33,6 +33,7 @@ import ( event "github.com/eris-ltd/eris-db/event" manager_types "github.com/eris-ltd/eris-db/manager/types" "github.com/eris-ltd/eris-db/txs" + "github.com/eris-ltd/eris-db/logging/loggers" ) type Pipe interface { @@ -43,6 +44,7 @@ type Pipe interface { Transactor() Transactor // Hash of Genesis state GenesisHash() []byte + Logger() loggers.InfoTraceLogger // NOTE: [ben] added to Pipe interface on 0.12 refactor GetApplication() manager_types.Application SetConsensusEngine(consensusEngine consensus_types.ConsensusEngine) error diff --git a/docs/generator.go b/docs/generator.go index fb430fc067be744340d07c486593563e994a75fd..2d34fb2a358ecdb4991e8ce002851d440846d8c1 100644 --- a/docs/generator.go +++ b/docs/generator.go @@ -13,6 +13,7 @@ import ( clientCommands "github.com/eris-ltd/eris-db/client/cmd" "github.com/eris-ltd/eris-db/version" "github.com/spf13/cobra" + "github.com/eris-ltd/eris-db/definitions" ) // Repository maintainers should customize the next two lines. @@ -115,9 +116,9 @@ func AddClientToDB(dbCmd, clientCmd *cobra.Command) error { func main() { // Repository maintainers should populate the top level command object. erisDbCommand := commands.ErisDbCmd - commands.InitErisDbCli() - commands.AddCommands() - commands.AddGlobalFlags() + do := definitions.NewDo() + commands.AddGlobalFlags(do) + commands.AddCommands(do) erisClientCommand := clientCommands.ErisClientCmd clientCommands.InitErisClientInit() diff --git a/glide.lock b/glide.lock index 76c7e0b2c0dc6b668890e5388cc0820b9d4b430c..cfd0d6066dea987150500aa66f1a88ff75ad7e73 100644 --- a/glide.lock +++ b/glide.lock @@ -30,8 +30,6 @@ imports: - go/common - name: github.com/eris-ltd/eris-keys version: 114ebc77443db9a153692233294e48bc7e184215 -- name: github.com/eris-ltd/eris-logger - version: ea48a395d6ecc0eccc67a26da9fc7a6106fabb84 - name: github.com/fsnotify/fsnotify version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8 - name: github.com/gin-gonic/gin @@ -215,4 +213,24 @@ imports: version: ecde8c8f16df93a994dda8936c8f60f0c26c28ab - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 +- name: github.com/go-kit/kit + version: f66b0e13579bfc5a48b9e2a94b1209c107ea1f41 + subpackages: + - log +- name: github.com/eapache/channels + version: 47238d5aae8c0fefd518ef2bee46290909cf8263 +- name: github.com/eapache/queue + version: 44cc805cf13205b55f69e14bcb69867d1ae92f98 +- name: github.com/go-logfmt/logfmt + version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 +- name: github.com/go-stack/stack + version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 +- name: github.com/Sirupsen/logrus + version: d26492970760ca5d33129d2d799e34be5c4782eb +- name: github.com/inconshreveable/log15 + version: 46a701a619de90c65a78c04d1a58bf02585e9701 + subpackages: + - term +- name: github.com/eris-ltd/eris-logger + version: ea48a395d6ecc0eccc67a26da9fc7a6106fabb84 devImports: [] diff --git a/glide.yaml b/glide.yaml index ca8e52b5c438cf55e2cb5a85ca74617f8d53c485..7b6f0398c08f09f4ba7de4b54177d805a4a48583 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,6 +1,5 @@ package: github.com/eris-ltd/eris-db import: -- package: github.com/eris-ltd/eris-logger - package: github.com/eris-ltd/eris-keys - package: github.com/spf13/cobra - package: github.com/spf13/viper @@ -15,5 +14,18 @@ import: - ripemd160 - package: gopkg.in/fatih/set.v0 - package: gopkg.in/tylerb/graceful.v1 -- package: golang.org/x/net/http2 +- package: golang.org/x/net + subpackages: + - http2 - package: github.com/eris-ltd/common +- package: github.com/go-kit/kit + version: ^0.3.0 +- package: github.com/eapache/channels + version: ~1.1.0 +- package: github.com/go-logfmt/logfmt + version: ^0.3.0 +- package: github.com/go-stack/stack + version: ^1.5.2 +- package: github.com/inconshreveable/log15 +- package: github.com/Sirupsen/logrus + version: ^0.11.0 diff --git a/logging/adapters/adapters.go b/logging/adapters/adapters.go new file mode 100644 index 0000000000000000000000000000000000000000..3b0cec85a77743071a9487b730c4d3ad4e77825c --- /dev/null +++ b/logging/adapters/adapters.go @@ -0,0 +1,2 @@ +package adapters + diff --git a/logging/adapters/logrus/logrus.go b/logging/adapters/logrus/logrus.go new file mode 100644 index 0000000000000000000000000000000000000000..708bac4d0cc7680bcd4c4ccf64ca07e21aaa9c3e --- /dev/null +++ b/logging/adapters/logrus/logrus.go @@ -0,0 +1,23 @@ +package adapters + +import ( + "github.com/Sirupsen/logrus" + kitlog "github.com/go-kit/kit/log" +) + +type logrusLogger struct { + logger logrus.Logger +} + +var _ kitlog.Logger = (*logrusLogger)(nil) + +func NewLogrusLogger(logger logrus.Logger) *logrusLogger { + return &logrusLogger{ + logger: logger, + } +} + +func (ll *logrusLogger) Log(keyvals... interface{}) error { + return nil +} + diff --git a/logging/adapters/stdlib/capture.go b/logging/adapters/stdlib/capture.go new file mode 100644 index 0000000000000000000000000000000000000000..dfa0a85f1bd3994744e1a64a909e620644d1a508 --- /dev/null +++ b/logging/adapters/stdlib/capture.go @@ -0,0 +1,26 @@ +package stdlib + +import ( + "io" + "log" + + "github.com/eris-ltd/eris-db/logging/loggers" + kitlog "github.com/go-kit/kit/log" +) + +func Capture(stdLibLogger log.Logger, +logger loggers.InfoTraceLogger) io.Writer { + adapter := newAdapter(logger) + stdLibLogger.SetOutput(adapter) + return adapter +} + +func CaptureRootLogger(logger loggers.InfoTraceLogger) io.Writer { + adapter := newAdapter(logger) + log.SetOutput(adapter) + return adapter +} + +func newAdapter(logger loggers.InfoTraceLogger) io.Writer { + return kitlog.NewStdlibAdapter(logger) +} diff --git a/logging/adapters/tendermint_log15/capture.go b/logging/adapters/tendermint_log15/capture.go new file mode 100644 index 0000000000000000000000000000000000000000..afc5d29eedcc61aacbbe8318775da50e74be0468 --- /dev/null +++ b/logging/adapters/tendermint_log15/capture.go @@ -0,0 +1,47 @@ +package adapters + +import ( + "github.com/eris-ltd/eris-db/logging/loggers" + kitlog "github.com/go-kit/kit/log" + "github.com/tendermint/log15" +) + +type infoTraceLoggerAsLog15Handler struct { + logger loggers.InfoTraceLogger +} + +var _ log15.Handler = (*infoTraceLoggerAsLog15Handler)(nil) + +type log15HandlerAsKitLogger struct { + handler log15.Handler +} + +var _ kitlog.Logger = (*log15HandlerAsKitLogger)(nil) + +func (l *log15HandlerAsKitLogger) Log(keyvals ...interface{}) error { + record := LogLineToRecord(keyvals...) + return l.handler.Log(record) +} + +func (h *infoTraceLoggerAsLog15Handler) Log(record *log15.Record) error { + if record.Lvl < log15.LvlDebug { + // Send to Critical, Warning, Error, and Info to the Info channel + h.logger.Info(RecordToLogLine(record)...) + } else { + // Send to Debug to the Trace channel + h.logger.Trace(RecordToLogLine(record)...) + } + return nil +} + +func Log15HandlerAsKitLogger(handler log15.Handler) kitlog.Logger { + return &log15HandlerAsKitLogger{ + handler: handler, + } +} + +func InfoTraceLoggerAsLog15Handler(logger loggers.InfoTraceLogger) log15.Handler { + return &infoTraceLoggerAsLog15Handler{ + logger: logger, + } +} diff --git a/logging/adapters/tendermint_log15/convert.go b/logging/adapters/tendermint_log15/convert.go new file mode 100644 index 0000000000000000000000000000000000000000..52cda465988566abeda50ac6ab28fad5c8a90838 --- /dev/null +++ b/logging/adapters/tendermint_log15/convert.go @@ -0,0 +1,70 @@ +package adapters + +import ( + "time" + + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/go-stack/stack" + "github.com/tendermint/log15" +) + +// Convert a go-kit log line (i.e. keyvals... interface{}) into a log15 record +// This allows us to use log15 output handlers +func LogLineToRecord(keyvals ...interface{}) *log15.Record { + vals, ctx := structure.ValuesAndContext(keyvals, structure.TimeKey, + structure.MessageKey, structure.CallerKey, structure.LevelKey) + + // Mapping of log line to Record is on a best effort basis + theTime, _ := vals[structure.TimeKey].(time.Time) + call, _ := vals[structure.CallerKey].(stack.Call) + level, _ := vals[structure.LevelKey].(string) + message, _ := vals[structure.MessageKey].(string) + + return &log15.Record{ + Time: theTime, + Lvl: Log15LvlFromString(level), + Msg: message, + Call: call, + Ctx: append(ctx, structure.CallerKey, call), + KeyNames: log15.RecordKeyNames{ + Time: structure.TimeKey, + Msg: structure.MessageKey, + Lvl: structure.LevelKey, + }} +} + +// Convert a log15 record to a go-kit log line (i.e. keyvals... interface{}) +// This allows us to capture output from dependencies using log15 +func RecordToLogLine(record *log15.Record) []interface{} { + return Concat( + Slice( + structure.TimeKey, record.Time, + structure.CallerKey, record.Call, + structure.LevelKey, record.Lvl.String(), + ), + record.Ctx, + Slice( + structure.MessageKey, record.Msg, + )) +} + +// Collapse our weak notion of leveling and log15's into a log15.Lvl +func Log15LvlFromString(level string) log15.Lvl { + if level == "" { + return log15.LvlDebug + } + switch level { + case loggers.InfoLevelName: + return log15.LvlInfo + case loggers.TraceLevelName: + return log15.LvlDebug + default: + lvl, err := log15.LvlFromString(level) + if err == nil { + return lvl + } + return log15.LvlDebug + } +} diff --git a/logging/config.go b/logging/config.go new file mode 100644 index 0000000000000000000000000000000000000000..b43b1b5545ba47586cb3e61a01589bac4de564c0 --- /dev/null +++ b/logging/config.go @@ -0,0 +1,11 @@ +package logging + +type ( + SinkConfig struct { + Channels []string + } + + LoggingConfig struct { + Sinks []SinkConfig + } +) diff --git a/logging/lifecycle/lifecycle.go b/logging/lifecycle/lifecycle.go new file mode 100644 index 0000000000000000000000000000000000000000..fd85a3248a1d5329d66077b4b8d8720d8d7a242a --- /dev/null +++ b/logging/lifecycle/lifecycle.go @@ -0,0 +1,37 @@ +package lifecycle + +// No package in ./logging/... should depend on lifecycle + +import ( + "os" + + "github.com/eris-ltd/eris-db/logging" + tmLog15adapter "github.com/eris-ltd/eris-db/logging/adapters/tendermint_log15" + "github.com/eris-ltd/eris-db/logging/loggers" + kitlog "github.com/go-kit/kit/log" + tmLog15 "github.com/tendermint/log15" + "github.com/eris-ltd/eris-db/logging/structure" +) + +func NewLoggerFromConfig(LoggingConfig logging.LoggingConfig) loggers.InfoTraceLogger { + infoLogger := kitlog.NewLogfmtLogger(os.Stderr) + traceLogger := kitlog.NewLogfmtLogger(os.Stderr) + return logging.WithMetadata(loggers.NewInfoTraceLogger(infoLogger, traceLogger)) +} + +func NewStdErrLogger() loggers.InfoTraceLogger { + logger := tmLog15adapter.Log15HandlerAsKitLogger( + tmLog15.StreamHandler(os.Stderr, tmLog15.TerminalFormat())) + return NewLogger(logger, logger) +} + +func NewLogger(infoLogger, traceLogger kitlog.Logger) loggers.InfoTraceLogger { + infoTraceLogger := loggers.NewInfoTraceLogger(infoLogger, traceLogger) + return logging.WithMetadata(infoTraceLogger) +} + +func CaptureTendermintLog15Output(infoTraceLogger loggers.InfoTraceLogger) { + tmLog15.Root().SetHandler( + tmLog15adapter.InfoTraceLoggerAsLog15Handler(infoTraceLogger. + With(structure.ComponentKey, "tendermint"))) +} diff --git a/logging/loggers/channel_logger.go b/logging/loggers/channel_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..20e824ef804deaf9de64266d94c1ef8338d47344 --- /dev/null +++ b/logging/loggers/channel_logger.go @@ -0,0 +1,73 @@ +package loggers + +import ( + "github.com/eapache/channels" + kitlog "github.com/go-kit/kit/log" +) + +const ( + LoggingRingBufferCap channels.BufferCap = 100 +) + +type ChannelLogger struct { + ch channels.Channel +} + +var _ kitlog.Logger = (*ChannelLogger)(nil) + +// Creates a Logger that uses a uses a non-blocking channel. +// +// We would like calls to Log to never block so we use a channel implementation +// that is non-blocking on writes and is able to be so by using a finite ring +// buffer. +func newChannelLogger() *ChannelLogger { + return &ChannelLogger{ + ch: channels.NewRingChannel(LoggingRingBufferCap), + } +} + +func (cl *ChannelLogger) Log(keyvals ...interface{}) error { + cl.ch.In() <- keyvals + // We don't have a way to pass on any logging errors, but that's okay: Log is + // a maximal interface and the error return type is only there for special + // cases. + return nil +} + +// Read a log line by waiting until one is available and returning it +func (cl *ChannelLogger) WaitReadLogLine() []interface{} { + log := <-cl.ch.Out() + // We are passing slices of interfaces down this channel (go-kit log's Log + // interface type), a panic is the right thing to do if this type assertion + // fails. + return log.([]interface{}) +} + +// Tries to read a log line from the channel buffer or returns nil if none is +// immediately available +func (cl *ChannelLogger) ReadLogLine() []interface{} { + select { + case log := <-cl.ch.Out(): + // See WaitReadLogLine + return log.([]interface{}) + default: + return nil + } +} + +// Enters an infinite loop that will drain any log lines from the passed logger. +// +// Exits if the channel is closed. +func (cl *ChannelLogger) DrainChannelToLogger(logger kitlog.Logger) { + for cl.ch.Out() != nil { + logger.Log(cl.WaitReadLogLine()...) + } +} + +// Wraps an underlying Logger baseLogger to provide a Logger that is +// is non-blocking on calls to Log. +func NonBlockingLogger(logger kitlog.Logger) *ChannelLogger { + cl := newChannelLogger() + go cl.DrainChannelToLogger(logger) + return cl +} diff --git a/logging/loggers/channel_logger_test.go b/logging/loggers/channel_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..087bd5b4146dd82b4624a3f8c17d126bb8739196 --- /dev/null +++ b/logging/loggers/channel_logger_test.go @@ -0,0 +1,32 @@ +package loggers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "fmt" +) + +func TestChannelLogger(t *testing.T) { + cl := newChannelLogger() + + // Push a larger number of log messages than will fit into ring buffer + for i := 0; i < int(LoggingRingBufferCap)+10; i++ { + cl.Log("log line", i) + } + + // Observe that oldest 10 messages are overwritten (so first message is 10) + for i := 0; i < int(LoggingRingBufferCap); i++ { + ll := cl.WaitReadLogLine() + assert.Equal(t, 10+i, ll[1]) + } + + assert.Nil(t, cl.ReadLogLine(), "Since we have drained the buffer there "+ + "should be no more log lines.") +} + +func TestBlether(t *testing.T) { + var bs []byte + ext := append(bs, ) + fmt.Println(ext) +} \ No newline at end of file diff --git a/logging/loggers/info_trace_logger.go b/logging/loggers/info_trace_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..7374477a92a40a80a8d726bf4da136f203e645af --- /dev/null +++ b/logging/loggers/info_trace_logger.go @@ -0,0 +1,126 @@ +package loggers + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +const ( + InfoChannelName = "Info" + TraceChannelName = "Trace" + + InfoLevelName = InfoChannelName + TraceLevelName = TraceChannelName +) + +type infoTraceLogger struct { + infoLogger *kitlog.Context + traceLogger *kitlog.Context +} + +// InfoTraceLogger maintains two independent concurrently-safe channels of +// logging. The idea behind the independence is that you can ignore one channel +// with no performance penalty. For more fine grained filtering or aggregation +// the Info and Trace loggers can be decorated loggers that perform arbitrary +// filtering/routing/aggregation on log messages. +type InfoTraceLogger interface { + // Send a log message to the default channel + kitlog.Logger + + // Send an log message to the Info channel, formed of a sequence of key value + // pairs. Info messages should be operationally interesting to a human who is + // monitoring the logs. But not necessarily a human who is trying to + // understand or debug the system. Any handled errors or warnings should be + // sent to the Info channel (where you may wish to tag them with a suitable + // key-value pair to categorise them as such). + Info(keyvals ...interface{}) + + // Send an log message to the Trace channel, formed of a sequence of key-value + // pairs. Trace messages can be used for any state change in the system that + // may be of interest to a machine consumer or a human who is trying to debug + // the system or trying to understand the system in detail. If the messages + // are very point-like and contain little structure, consider using a metric + // instead. + Trace(keyvals ...interface{}) + + // A logging context (see go-kit log's Context). Takes a sequence key values + // via With or WithPrefix and ensures future calls to log will have those + // contextual values appended to the call to an underlying logger. + // Values can be dynamic by passing an instance of the kitlog.Valuer interface + // This provides an interface version of the kitlog.Context struct to be used + // For implementations that wrap a kitlog.Context. In addition it makes no + // assumption about the name or signature of the logging method(s). + // See InfoTraceLogger + + // Establish a context by appending contextual key-values to any existing + // contextual values + With(keyvals ...interface{}) InfoTraceLogger + + // Establish a context by prepending contextual key-values to any existing + // contextual values + WithPrefix(keyvals ...interface{}) InfoTraceLogger +} + +// Interface assertions +var _ InfoTraceLogger = (*infoTraceLogger)(nil) +var _ kitlog.Logger = (InfoTraceLogger)(nil) + +func NewInfoTraceLogger(infoLogger, traceLogger kitlog.Logger) InfoTraceLogger { + // We will never halt progress a log emitter. If log output takes too long + // will start dropping log lines by using a ring buffer. + // We also guard against any concurrency bugs in underlying loggers by feeding + // them from a single channel + logger := kitlog.NewContext(NonBlockingLogger(MultipleChannelLogger( + map[string]kitlog.Logger{ + InfoChannelName: infoLogger, + TraceChannelName: traceLogger, + }))) + return &infoTraceLogger{ + infoLogger: logger.With( + structure.ChannelKey, InfoChannelName, + structure.LevelKey, InfoLevelName, + ), + traceLogger: logger.With( + structure.ChannelKey, TraceChannelName, + structure.LevelKey, TraceLevelName, + ), + } +} + +func NewNoopInfoTraceLogger() InfoTraceLogger { + noopLogger := kitlog.NewNopLogger() + return NewInfoTraceLogger(noopLogger, noopLogger) +} + +func (l *infoTraceLogger) With(keyvals ...interface{}) InfoTraceLogger { + return &infoTraceLogger{ + infoLogger: l.infoLogger.With(keyvals...), + traceLogger: l.traceLogger.With(keyvals...), + } +} + +func (l *infoTraceLogger) WithPrefix(keyvals ...interface{}) InfoTraceLogger { + return &infoTraceLogger{ + infoLogger: l.infoLogger.WithPrefix(keyvals...), + traceLogger: l.traceLogger.WithPrefix(keyvals...), + } +} + +func (l *infoTraceLogger) Info(keyvals ...interface{}) { + // We send Info and Trace log lines down the same pipe to keep them ordered + l.infoLogger.Log(keyvals...) +} + +func (l *infoTraceLogger) Trace(keyvals ...interface{}) { + l.traceLogger.Log(keyvals...) +} + +// If logged to as a plain kitlog logger presume the message is for Trace +// This favours keeping Info reasonably quiet. Note that an InfoTraceLogger +// aware adapter can make its own choices, but we tend to thing of logs from +// dependencies as less interesting than logs generated by us or specifically +// routed by us. +func (l *infoTraceLogger) Log(keyvals ...interface{}) error { + l.Trace(keyvals...) + return nil +} diff --git a/logging/loggers/info_trace_logger_test.go b/logging/loggers/info_trace_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..505f2606badbeb178f9046982a6d130030225456 --- /dev/null +++ b/logging/loggers/info_trace_logger_test.go @@ -0,0 +1,14 @@ +package loggers + +import ( + "os" + "testing" + + kitlog "github.com/go-kit/kit/log" +) + +func TestLogger(t *testing.T) { + stderrLogger := kitlog.NewLogfmtLogger(os.Stderr) + logger := NewInfoTraceLogger(stderrLogger, stderrLogger) + logger.Trace("hello", "barry") +} diff --git a/logging/loggers/logging_test.go b/logging/loggers/logging_test.go new file mode 100644 index 0000000000000000000000000000000000000000..78166fc91ca74989abeb7e08daf793dce10595a2 --- /dev/null +++ b/logging/loggers/logging_test.go @@ -0,0 +1,21 @@ +package loggers + +import "errors" + +type testLogger struct { + logLines [][]interface{} + err error +} + +func newErrorLogger(errMessage string) *testLogger { + return &testLogger{err: errors.New(errMessage)} +} + +func newTestLogger() *testLogger { + return &testLogger{} +} + +func (tl *testLogger) Log(keyvals ...interface{}) error { + tl.logLines = append(tl.logLines, keyvals) + return tl.err +} diff --git a/logging/loggers/multiple_channel_logger.go b/logging/loggers/multiple_channel_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..9f68b4df84e9957638239cb46861fc72f4b4a3da --- /dev/null +++ b/logging/loggers/multiple_channel_logger.go @@ -0,0 +1,37 @@ +package loggers + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +// This represents a 'SELECT ONE' type logger. When logged to it will search +// for the ChannelKey field, look that up in its map and send the log line there +// Otherwise logging is a noop (but an error will be returned - which is optional) +type MultipleChannelLogger map[string]kitlog.Logger + +var _ kitlog.Logger = MultipleChannelLogger(nil) + +// Like go-kit log's Log method only logs a message to the specified channelName +// which must be a member of this MultipleChannelLogger +func (mcl MultipleChannelLogger) Log(keyvals ...interface{}) error { + channel := structure.Value(keyvals, structure.ChannelKey) + if channel == nil { + return fmt.Errorf("MultipleChannelLogger could not select channel because" + + " '%s' was not set in log message", structure.ChannelKey) + } + channelName, ok := channel.(string) + if !ok { + return fmt.Errorf("MultipleChannelLogger could not select channel because" + + " channel was set to non-string value %v", channel) + } + logger := mcl[channelName] + if logger == nil { + return fmt.Errorf("Could not log to channel '%s', since it is not "+ + "registered with this MultipleChannelLogger (the underlying logger may "+ + "have been nil when passed to NewMultipleChannelLogger)", channelName) + } + return logger.Log(keyvals...) +} diff --git a/logging/loggers/multiple_channel_logger_test.go b/logging/loggers/multiple_channel_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..db8f5013a13c636af96c2791d0ee5e00a38695fc --- /dev/null +++ b/logging/loggers/multiple_channel_logger_test.go @@ -0,0 +1,28 @@ +package loggers + +import ( + "runtime" + "testing" + "time" + + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" + "github.com/stretchr/testify/assert" +) + +func TestMultipleChannelLogger(t *testing.T) { + boringLogger, interestingLogger := newTestLogger(), newTestLogger() + mcl := kitlog.NewContext(MultipleChannelLogger(map[string]kitlog.Logger{ + "Boring": boringLogger, + "Interesting": interestingLogger, + })) + err := mcl.With("time", kitlog.Valuer(func() interface{} { return "aa" })). + Log(structure.ChannelKey, "Boring", "foo", "bar") + assert.NoError(t, err, "Should log without an error") + // Wait for channel to drain + time.Sleep(time.Second) + runtime.Gosched() + assert.Equal(t, []interface{}{"time", "aa", structure.ChannelKey, "Boring", + "foo", "bar"}, + boringLogger.logLines[0]) +} diff --git a/logging/loggers/multiple_output_logger.go b/logging/loggers/multiple_output_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..9f3cb5326aa1a98d0f3fcfad6462051965749064 --- /dev/null +++ b/logging/loggers/multiple_output_logger.go @@ -0,0 +1,50 @@ +package loggers + +import ( + "strings" + + kitlog "github.com/go-kit/kit/log" +) + +// This represents an 'AND' type logger. When logged to it will log to each of +// the loggers in the slice. +type MultipleOutputLogger []kitlog.Logger + +var _ kitlog.Logger = MultipleOutputLogger(nil) + +func (mol MultipleOutputLogger) Log(keyvals ...interface{}) error { + var errs []error + for _, logger := range mol { + err := logger.Log(keyvals...) + if err != nil { + errs = append(errs, err) + } + } + return combineErrors(errs) +} + +// Creates a logger that forks log messages to each of its outputLoggers +func NewMultipleOutputLogger(outputLoggers ...kitlog.Logger) kitlog.Logger { + return MultipleOutputLogger(outputLoggers) +} + +type multipleErrors []error + +func combineErrors(errs []error) error { + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + return multipleErrors(errs) + } +} + +func (errs multipleErrors) Error() string { + var errStrings []string + for _, err := range errs { + errStrings = append(errStrings, err.Error()) + } + return strings.Join(errStrings, ";") +} diff --git a/logging/loggers/multiple_output_logger_test.go b/logging/loggers/multiple_output_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..786c60064947d3475c60275f45140ce39edde199 --- /dev/null +++ b/logging/loggers/multiple_output_logger_test.go @@ -0,0 +1,18 @@ +package loggers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewMultipleOutputLogger(t *testing.T) { + a, b := newErrorLogger("error a"), newErrorLogger("error b") + mol := NewMultipleOutputLogger(a, b) + logLine := []interface{}{"msg", "hello"} + err := mol.Log(logLine...) + expected := [][]interface{}{logLine} + assert.Equal(t, expected, a.logLines) + assert.Equal(t, expected, b.logLines) + assert.IsType(t, multipleErrors{}, err) +} diff --git a/logging/metadata.go b/logging/metadata.go new file mode 100644 index 0000000000000000000000000000000000000000..3fbed2cf4c819a5e28c0788515a3920f45b209ee --- /dev/null +++ b/logging/metadata.go @@ -0,0 +1,27 @@ +package logging + +import ( + "time" + + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +const ( + // To get the Caller information correct on the log, we need to count the + // number of calls from a log call in the code to the time it hits a kitlog + // context: [log call site (5), Info/Trace (4), MultipleChannelLogger.Log (3), + // kitlog.Context.Log (2), kitlog.bindValues (1) (binding occurs), + // kitlog.Caller (0), stack.caller] + infoTraceLoggerCallDepth = 5 +) + +var defaultTimestampUTCValuer kitlog.Valuer = func() interface{} { + return time.Now() +} + +func WithMetadata(infoTraceLogger loggers.InfoTraceLogger) loggers.InfoTraceLogger { + return infoTraceLogger.With(structure.TimeKey, defaultTimestampUTCValuer, + structure.CallerKey, kitlog.Caller(infoTraceLoggerCallDepth)) +} diff --git a/logging/structure/structure.go b/logging/structure/structure.go new file mode 100644 index 0000000000000000000000000000000000000000..9420b20095b4fabab87ce707f082d17968f90ba3 --- /dev/null +++ b/logging/structure/structure.go @@ -0,0 +1,60 @@ +package structure + +import . "github.com/eris-ltd/eris-db/util/slice" + +const ( + // Key for go time.Time object + TimeKey = "time" + // Key for call site for log invocation + CallerKey = "caller" + // Key for String name for level + LevelKey = "level" + // Key to switch on for channel in a multiple channel logging context + ChannelKey = "channel" + // Key for string message + MessageKey = "message" + // Key for module or function or struct that is the subject of the logging + ComponentKey = "component" +) + +// Pull the specified values from a structured log line into a map. +// Assumes keys are single-valued. +// Returns a map of the key-values from the requested keys and +// the unmatched remainder keyvals as context as a slice of key-values. +func ValuesAndContext(keyvals []interface{}, + keys ...interface{}) (map[interface{}]interface{}, []interface{}) { + vals := make(map[interface{}]interface{}, len(keys)) + context := make([]interface{}, len(keyvals)) + copy(context, keyvals) + deletions := 0 + // We can't really do better than a linear scan of both lists here. N is small + // so screw the asymptotics. + // Guard against odd-length list + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + for k := 0; k < len(keys); k++ { + if keyvals[i] == keys[k] { + // Pull the matching key-value pair into vals to return + vals[keys[k]] = keyvals[i+1] + // Delete the key once it's found + keys = DeleteAt(keys, k) + // And remove the key-value pair from context + context = Delete(context, i-deletions, 2) + // Keep a track of how much we've shrunk the context to offset next + // deletion + deletions += 2 + break + } + } + } + return vals, context +} + +// Return a single value corresponding to key in keyvals +func Value(keyvals []interface{}, key interface{}) interface{} { + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + if keyvals[i] == key { + return keyvals[i+1] + } + } + return nil +} \ No newline at end of file diff --git a/logging/structure/structure_test.go b/logging/structure/structure_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2fbdaa3e5e1c525c5ccf836b430ad13ebbb5333a --- /dev/null +++ b/logging/structure/structure_test.go @@ -0,0 +1,15 @@ +package structure + +import ( + "testing" + + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/stretchr/testify/assert" +) + +func TestValuesAndContext(t *testing.T) { + keyvals := Slice("hello", 1, "dog", 2, "fish", 3, "fork", 5) + vals, ctx := ValuesAndContext(keyvals, "hello", "fish") + assert.Equal(t, map[interface{}]interface{}{"hello": 1, "fish": 3}, vals) + assert.Equal(t, Slice("dog", 2, "fork", 5), ctx) +} diff --git a/logging/terminal.go b/logging/terminal.go new file mode 100644 index 0000000000000000000000000000000000000000..e8eea7d8107484f1f5bd79a745f042e77fa49e8f --- /dev/null +++ b/logging/terminal.go @@ -0,0 +1,29 @@ +package logging + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + "github.com/go-kit/kit/log/term" +) + +func Colors(keyvals ...interface{}) term.FgBgColor { + for i := 0; i < len(keyvals)-1; i += 2 { + if keyvals[i] != structure.LevelKey { + continue + } + switch keyvals[i+1] { + case "debug": + return term.FgBgColor{Fg: term.DarkGray} + case "info": + return term.FgBgColor{Fg: term.Gray} + case "warn": + return term.FgBgColor{Fg: term.Yellow} + case "error": + return term.FgBgColor{Fg: term.Red} + case "crit": + return term.FgBgColor{Fg: term.Gray, Bg: term.DarkRed} + default: + return term.FgBgColor{} + } + } + return term.FgBgColor{} +} diff --git a/manager/eris-mint/pipe.go b/manager/eris-mint/pipe.go index dc9b64875319a7fb50c6fb6c269778f2d9272d5a..d9691f27081580b8a7e45d603bbc8bf955744c6f 100644 --- a/manager/eris-mint/pipe.go +++ b/manager/eris-mint/pipe.go @@ -28,8 +28,6 @@ import ( tm_types "github.com/tendermint/tendermint/types" tmsp_types "github.com/tendermint/tmsp/types" - log "github.com/eris-ltd/eris-logger" - "github.com/eris-ltd/eris-db/account" blockchain_types "github.com/eris-ltd/eris-db/blockchain/types" imath "github.com/eris-ltd/eris-db/common/math/integral" @@ -38,12 +36,15 @@ import ( core_types "github.com/eris-ltd/eris-db/core/types" "github.com/eris-ltd/eris-db/definitions" edb_event "github.com/eris-ltd/eris-db/event" + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" vm "github.com/eris-ltd/eris-db/manager/eris-mint/evm" "github.com/eris-ltd/eris-db/manager/eris-mint/state" state_types "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" manager_types "github.com/eris-ltd/eris-db/manager/types" rpc_tm_types "github.com/eris-ltd/eris-db/rpc/tendermint/core/types" "github.com/eris-ltd/eris-db/txs" + log "github.com/eris-ltd/eris-logger" ) type erisMintPipe struct { @@ -59,6 +60,7 @@ type erisMintPipe struct { // Genesis cache genesisDoc *state_types.GenesisDoc genesisState *state.State + logger loggers.InfoTraceLogger } // NOTE [ben] Compiler check to ensure erisMintPipe successfully implements @@ -70,7 +72,8 @@ var _ definitions.Pipe = (*erisMintPipe)(nil) var _ definitions.TendermintPipe = (*erisMintPipe)(nil) func NewErisMintPipe(moduleConfig *config.ModuleConfig, - eventSwitch *go_events.EventSwitch) (*erisMintPipe, error) { + eventSwitch *go_events.EventSwitch, + logger loggers.InfoTraceLogger) (*erisMintPipe, error) { startedState, genesisDoc, err := startState(moduleConfig.DataDir, moduleConfig.Config.GetString("db_backend"), moduleConfig.GenesisFile, @@ -79,11 +82,11 @@ func NewErisMintPipe(moduleConfig *config.ModuleConfig, return nil, fmt.Errorf("Failed to start state: %v", err) } // assert ChainId matches genesis ChainId - log.WithFields(log.Fields{ - "chainId": startedState.ChainID, - "lastBlockHeight": startedState.LastBlockHeight, - "lastBlockHash": startedState.LastBlockHash, - }).Debug("Loaded state") + logger.Info( + "chainId", startedState.ChainID, + "lastBlockHeight", startedState.LastBlockHeight, + "lastBlockHash", startedState.LastBlockHash, + structure.MessageKey, "Loaded state") // start the application erisMint := NewErisMint(startedState, eventSwitch) @@ -108,6 +111,7 @@ func NewErisMintPipe(moduleConfig *config.ModuleConfig, // authority - this is a sort of dependency injection pattern consensusEngine: nil, blockchain: nil, + logger: logger, } // NOTE: [Silas] @@ -183,6 +187,10 @@ func startState(dataDir, backend, genesisFile, chainId string) (*state.State, //------------------------------------------------------------------------------ // Implement definitions.Pipe for erisMintPipe +func (pipe *erisMintPipe) Logger() loggers.InfoTraceLogger { + return pipe.logger +} + func (pipe *erisMintPipe) Accounts() definitions.Accounts { return pipe.accounts } diff --git a/manager/eris-mint/state/state.go b/manager/eris-mint/state/state.go index a36bc8ec2f607eaa505fcf8b1b7ad66c04a44033..20106e3386d7818d681ebebaa8e8a2c24379b1e1 100644 --- a/manager/eris-mint/state/state.go +++ b/manager/eris-mint/state/state.go @@ -12,7 +12,6 @@ import ( ptypes "github.com/eris-ltd/eris-db/permission/types" "github.com/eris-ltd/eris-db/txs" - . "github.com/tendermint/go-common" dbm "github.com/tendermint/go-db" "github.com/tendermint/go-events" "github.com/tendermint/go-merkle" @@ -20,6 +19,7 @@ import ( core_types "github.com/eris-ltd/eris-db/core/types" "github.com/tendermint/tendermint/types" + "github.com/eris-ltd/eris-db/util" ) var ( @@ -77,7 +77,7 @@ func LoadState(db dbm.DB) *State { s.nameReg.Load(nameRegHash) if *err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - Exit(Fmt("Data has been corrupted or its spec has changed: %v\n", *err)) + util.Fatalf("Data has been corrupted or its spec has changed: %v\n", *err) } // TODO: ensure that buf is completely read. } @@ -101,7 +101,10 @@ func (s *State) Save() { //wire.WriteByteSlice(s.validatorInfos.Hash(), buf, n, err) wire.WriteByteSlice(s.nameReg.Hash(), buf, n, err) if *err != nil { - PanicCrisis(*err) + // TODO: [Silas] Do something better than this, really serialising ought to + // be error-free + util.Fatalf("Could not serialise state in order to save the state, " + + "cannot continue, error: %s", *err) } s.DB.Set(stateKey, buf.Bytes()) } @@ -401,7 +404,7 @@ func (s *State) SetFireable(evc events.Fireable) { func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) (*GenesisDoc, *State) { jsonBlob, err := ioutil.ReadFile(genDocFile) if err != nil { - Exit(Fmt("Couldn't read GenesisDoc file: %v", err)) + util.Fatalf("Couldn't read GenesisDoc file: %v", err) } genDoc := GenesisDocFromJSON(jsonBlob) return genDoc, MakeGenesisState(db, genDoc) @@ -409,7 +412,7 @@ func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) (*GenesisDoc, *State func MakeGenesisState(db dbm.DB, genDoc *GenesisDoc) *State { if len(genDoc.Validators) == 0 { - Exit(Fmt("The genesis file has no validators")) + util.Fatalf("The genesis file has no validators") } if genDoc.GenesisTime.IsZero() { diff --git a/manager/manager.go b/manager/manager.go index 406648cd3ef36bc56857a55689c3b233870e633a..276d872f1da331ec8ae83d21729a22135a5d00d3 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -21,12 +21,13 @@ import ( events "github.com/tendermint/go-events" - log "github.com/eris-ltd/eris-logger" - config "github.com/eris-ltd/eris-db/config" definitions "github.com/eris-ltd/eris-db/definitions" erismint "github.com/eris-ltd/eris-db/manager/eris-mint" // types "github.com/eris-ltd/eris-db/manager/types" + + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" ) // NewApplicationPipe returns an initialised Pipe interface @@ -35,18 +36,18 @@ import ( // of an application. It is feasible this will be insufficient to support // different types of applications later down the line. func NewApplicationPipe(moduleConfig *config.ModuleConfig, - evsw *events.EventSwitch, consensusMinorVersion string) (definitions.Pipe, + evsw *events.EventSwitch, logger loggers.InfoTraceLogger, + consensusMinorVersion string) (definitions.Pipe, error) { switch moduleConfig.Name { case "erismint": if err := erismint.AssertCompatibleConsensus(consensusMinorVersion); err != nil { return nil, err } - log.WithFields(log.Fields{ - "compatibleConsensus": consensusMinorVersion, - "erisMintVersion": erismint.GetErisMintVersion().GetVersionString(), - }).Debug("Loading ErisMint") - return erismint.NewErisMintPipe(moduleConfig, evsw) + logger.Info("compatibleConsensus", consensusMinorVersion, + "erisMintVersion", erismint.GetErisMintVersion().GetVersionString(), + structure.MessageKey, "Loading ErisMint") + return erismint.NewErisMintPipe(moduleConfig, evsw, logger) } return nil, fmt.Errorf("Failed to return Pipe for %s", moduleConfig.Name) } diff --git a/rpc/tendermint/client/client_test.go b/rpc/tendermint/client/client_test.go index 2580508d828b88b457cc683f78a21c145aee5e9b..52e0125e7645dd9d87e101c65c68dc60fa9b5faa 100644 --- a/rpc/tendermint/client/client_test.go +++ b/rpc/tendermint/client/client_test.go @@ -30,5 +30,4 @@ func TestMapsAndValues(t *testing.T) { _, _, err = mapAndValues("Foo", 4, 4, "Bar") assert.Error(t, err, "Should be an error to provide non-string keys") - } diff --git a/rpc/tendermint/test/shared.go b/rpc/tendermint/test/shared.go index d211bd5d11c6fd7c078ce12665366f0a48d0a134..8ddce9b340f3aa507ccbffe1fe736408e41cb6b3 100644 --- a/rpc/tendermint/test/shared.go +++ b/rpc/tendermint/test/shared.go @@ -21,6 +21,7 @@ import ( "path" + "github.com/eris-ltd/eris-db/logging/lifecycle" state_types "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" "github.com/spf13/viper" tm_common "github.com/tendermint/go-common" @@ -83,7 +84,12 @@ func initGlobalVariables(ffs *fixtures.FileFixtures) error { // Set up priv_validator.json before we start tendermint (otherwise it will // create its own one. saveNewPriv() - testCore, err = core.NewCore("testCore", consensusConfig, managerConfig) + logger := lifecycle.NewStdErrLogger() + // To spill tendermint logs on the floor: + // lifecycle.CaptureTendermintLog15Output(loggers.NewNoopInfoTraceLogger()) + lifecycle.CaptureTendermintLog15Output(logger) + testCore, err = core.NewCore("testCore", consensusConfig, managerConfig, + logger) if err != nil { return err } diff --git a/test/mock/pipe.go b/test/mock/pipe.go index e279ad18253e2fc755f9948d8ec5108d89588a41..ecd245ec87e2c9934e360ae72869210e134ee820 100644 --- a/test/mock/pipe.go +++ b/test/mock/pipe.go @@ -14,6 +14,7 @@ import ( td "github.com/eris-ltd/eris-db/test/testdata/testdata" "github.com/eris-ltd/eris-db/txs" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/tendermint/go-crypto" "github.com/tendermint/go-p2p" mintTypes "github.com/tendermint/tendermint/types" @@ -29,24 +30,20 @@ type MockPipe struct { events event.EventEmitter namereg definitions.NameReg transactor definitions.Transactor + logger loggers.InfoTraceLogger } // Create a new mock tendermint pipe. func NewMockPipe(td *td.TestData) definitions.Pipe { - accounts := &accounts{td} - blockchain := &blockchain{td} - consensusEngine := &consensusEngine{td} - eventer := &eventer{td} - namereg := &namereg{td} - transactor := &transactor{td} return &MockPipe{ - td, - accounts, - blockchain, - consensusEngine, - eventer, - namereg, - transactor, + testData: td, + accounts: &accounts{td}, + blockchain: &blockchain{td}, + consensusEngine: &consensusEngine{td}, + events: &eventer{td}, + namereg: &namereg{td}, + transactor: &transactor{td}, + logger: loggers.NewNoopInfoTraceLogger(), } } @@ -75,6 +72,10 @@ func (pipe *MockPipe) Transactor() definitions.Transactor { return pipe.transactor } +func (pipe *MockPipe) Logger() loggers.InfoTraceLogger { + return pipe.logger +} + func (pipe *MockPipe) GetApplication() manager_types.Application { // TODO: [ben] mock application return nil diff --git a/txs/log.go b/txs/log.go deleted file mode 100644 index b967a58d0ef7d4701fc15f5ab3dfee1f046b15f5..0000000000000000000000000000000000000000 --- a/txs/log.go +++ /dev/null @@ -1,7 +0,0 @@ -package txs - -import ( - "github.com/tendermint/go-logger" -) - -var log = logger.New("module", "types") diff --git a/util/os.go b/util/os.go new file mode 100644 index 0000000000000000000000000000000000000000..54f41b16b420f9b2b92fdf3dbfd9959cb81b772f --- /dev/null +++ b/util/os.go @@ -0,0 +1,13 @@ +package util + +import ( + "fmt" + "os" +) + +// Prints an error message to stderr and exits with status code 1 +func Fatalf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} + diff --git a/util/slice/slice.go b/util/slice/slice.go new file mode 100644 index 0000000000000000000000000000000000000000..55332d87f5c96aa753f6f0580b306f77385a5134 --- /dev/null +++ b/util/slice/slice.go @@ -0,0 +1,64 @@ +package slice + +func Slice(elements ...interface{}) []interface{} { + return elements +} + +func EmptySlice() []interface{} { + return []interface{}{} +} + +// Like append but on the interface{} type and always to a fresh backing array +// so can be used safely with slices over arrays you did not create. +func CopyAppend(slice []interface{}, elements ...interface{}) []interface{} { + sliceLength := len(slice) + newSlice := make([]interface{}, sliceLength+len(elements)) + for i, e := range slice { + newSlice[i] = e + } + for i, e := range elements { + newSlice[sliceLength+i] = e + } + return newSlice +} + +// Prepend elements to slice in the order they appear +func CopyPrepend(slice []interface{}, elements ...interface{}) []interface{} { + elementsLength := len(elements) + newSlice := make([]interface{}, len(slice)+elementsLength) + for i, e := range elements { + newSlice[i] = e + } + for i, e := range slice { + newSlice[elementsLength+i] = e + } + return newSlice +} + +// Concatenate slices into a single slice +func Concat(slices ...[]interface{}) []interface{} { + offset := 0 + for _, slice := range slices { + offset += len(slice) + } + concat := make([]interface{}, offset) + offset = 0 + for _, slice := range slices { + for i, e := range slice { + concat[offset+i] = e + } + offset += len(slice) + } + return concat +} + +// Deletes n elements starting with the ith from a slice by splicing. +// Beware uses append so the underlying backing array will be modified! +func Delete(slice []interface{}, i int, n int) []interface{} { + return append(slice[:i], slice[i+n:]...) +} + +// +func DeleteAt(slice []interface{}, i int) []interface{} { + return Delete(slice, i, 1) +} diff --git a/util/slice/slice_test.go b/util/slice/slice_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4a1f53e71ad0f5c4518e73c1d7b2eaa34ef8abc8 --- /dev/null +++ b/util/slice/slice_test.go @@ -0,0 +1,36 @@ +package slice + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCopyAppend(t *testing.T) { + assert.Equal(t, Slice(1, "two", "three", 4), + CopyAppend(Slice(1, "two"), "three", 4)) + assert.Equal(t, EmptySlice(), CopyAppend(nil)) + assert.Equal(t, Slice(1), CopyAppend(nil, 1)) + assert.Equal(t, Slice(1), CopyAppend(Slice(1))) +} + +func TestCopyPrepend(t *testing.T) { + assert.Equal(t, Slice("three", 4, 1, "two"), + CopyPrepend(Slice(1, "two"), "three", 4)) + assert.Equal(t, EmptySlice(), CopyPrepend(nil)) + assert.Equal(t, Slice(1), CopyPrepend(nil, 1)) + assert.Equal(t, Slice(1), CopyPrepend(Slice(1))) +} + +func TestConcat(t *testing.T) { + assert.Equal(t, Slice(1,2,3,4,5), Concat(Slice(1,2,3,4,5))) + assert.Equal(t, Slice(1,2,3,4,5), Concat(Slice(1,2,3),Slice(4,5))) + assert.Equal(t, Slice(1,2,3,4,5), Concat(Slice(1),Slice(2,3),Slice(4,5))) + assert.Equal(t, EmptySlice(), Concat(nil)) + assert.Equal(t, Slice(1), Concat(nil, Slice(), Slice(1))) + assert.Equal(t, Slice(1), Concat(Slice(1), Slice(), nil)) +} + +func TestDelete(t *testing.T) { + assert.Equal(t, Slice(1,2,4,5), Delete(Slice(1,2,3,4,5), 2, 1)) +} \ No newline at end of file