diff --git a/client/client.go b/client/client.go index 24eeb0338629fdbaae0aa28367a074e436e1d415..34275c8b2deacc6a10d18d18a496297dd3b44430 100644 --- a/client/client.go +++ b/client/client.go @@ -80,7 +80,7 @@ func NewErisNodeClient(rpcString string) *ErisNodeClient { // it needs to be initialised before go-rpc, hence it's placement here. func init() { h := tendermint_log.LvlFilterHandler(tendermint_log.LvlWarn, tendermint_log.StdoutHandler) - tendermint_log.Root().SetHandler(h) + tendermint_log.Root().SetHandler(h) } //------------------------------------------------------------------------------------ diff --git a/cmd/serve.go b/cmd/serve.go index 8d23e4b3b585108040e78671be510d281d2d60f3..f8b99c08c4fa3ee178b7c967af063ba89fe8aa05 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -115,6 +115,8 @@ func NewCoreFromDo(do *definitions.Do) (*core.Core, error) { // Capture all logging from tendermint/tendermint and tendermint/go-* // dependencies lifecycle.CaptureTendermintLog15Output(logger) + // And from stdlib go log + lifecycle.CaptureStdlibLogOutput(logger) cmdLogger := logger.With("command", "serve") diff --git a/consensus/consensus.go b/consensus/consensus.go index 03296eed028a0151a8a5ae3d696d58107caa96c9..01fc2d12761e56399c2c543359f4c26905ff4d4a 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -28,7 +28,8 @@ func LoadConsensusEngineInPipe(moduleConfig *config.ModuleConfig, pipe definitions.Pipe) error { switch moduleConfig.Name { case "tendermint": - tmint, err := tendermint.NewTendermint(moduleConfig, pipe.GetApplication()) + tmint, err := tendermint.NewTendermint(moduleConfig, pipe.GetApplication(), + pipe.Logger().With()) if err != nil { return fmt.Errorf("Failed to load Tendermint node: %v", err) } diff --git a/consensus/tendermint/tendermint.go b/consensus/tendermint/tendermint.go index 87553697880fcd144c14c0bf70ff3eab3cdcf0c2..545e4c1acb46200f3e5e24d3f0b58bc273dc151a 100644 --- a/consensus/tendermint/tendermint.go +++ b/consensus/tendermint/tendermint.go @@ -41,6 +41,7 @@ import ( // files "github.com/eris-ltd/eris-db/files" blockchain_types "github.com/eris-ltd/eris-db/blockchain/types" consensus_types "github.com/eris-ltd/eris-db/consensus/types" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/txs" "github.com/tendermint/go-wire" ) @@ -57,7 +58,8 @@ var _ consensus_types.ConsensusEngine = (*Tendermint)(nil) var _ blockchain_types.Blockchain = (*Tendermint)(nil) func NewTendermint(moduleConfig *config.ModuleConfig, - application manager_types.Application) (*Tendermint, error) { + application manager_types.Application, + logger loggers.InfoTraceLogger) (*Tendermint, error) { // re-assert proper configuration for module if moduleConfig.Version != GetTendermintVersion().GetMinorVersionString() { return nil, fmt.Errorf("Version string %s did not match %s", @@ -74,7 +76,7 @@ func NewTendermint(moduleConfig *config.ModuleConfig, tendermintConfigViper, err := config.ViperSubConfig(moduleConfig.Config, "configuration") if tendermintConfigViper == nil { return nil, - fmt.Errorf("Failed to extract Tendermint configuration subtree: %s", err) + 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/core.go b/core/core.go index c4e08963023b7de5ba4611a35ccf134dd378414d..3bd32fdcde0c8ec909ab244c06a1479c2447caae 100644 --- a/core/core.go +++ b/core/core.go @@ -22,20 +22,19 @@ import ( // TODO: [ben] swap out go-events with eris-db/event (currently unused) events "github.com/tendermint/go-events" - log "github.com/eris-ltd/eris-logger" - - config "github.com/eris-ltd/eris-db/config" - consensus "github.com/eris-ltd/eris-db/consensus" - definitions "github.com/eris-ltd/eris-db/definitions" - event "github.com/eris-ltd/eris-db/event" - manager "github.com/eris-ltd/eris-db/manager" + "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/event" + "github.com/eris-ltd/eris-db/manager" // 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" "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" + "github.com/eris-ltd/eris-db/server" ) // Core is the high-level structure @@ -60,15 +59,15 @@ func NewCore(chainId string, if err != nil { return nil, fmt.Errorf("Failed to load application pipe: %v", err) } - log.Debug("Loaded pipe with application manager") + logging.TraceMsg(logger, "Loaded pipe with application manager") // pass the consensus engine into the pipe if e := consensus.LoadConsensusEngineInPipe(consensusConfig, pipe); e != nil { return nil, fmt.Errorf("Failed to load consensus engine in pipe: %v", e) } tendermintPipe, err := pipe.GetTendermintPipe() if err != nil { - log.Warn(fmt.Sprintf("Tendermint gateway not supported by %s", - managerConfig.Version)) + logging.TraceMsg(logger, "Tendermint gateway not supported by manager", + "manager-version", managerConfig.Version) } return &Core{ chainId: chainId, diff --git a/glide.lock b/glide.lock index cfd0d6066dea987150500aa66f1a88ff75ad7e73..34cea95404a3b7a0d4ff200facba76ad4cd14890 100644 --- a/glide.lock +++ b/glide.lock @@ -233,4 +233,6 @@ imports: - term - name: github.com/eris-ltd/eris-logger version: ea48a395d6ecc0eccc67a26da9fc7a6106fabb84 +- name: github.com/streadway/simpleuuid + version: 6617b501e485b77e61b98cd533aefff9e258b5a7 devImports: [] diff --git a/glide.yaml b/glide.yaml index 7b6f0398c08f09f4ba7de4b54177d805a4a48583..9ad7b4d4edc758ee21952e15cf611acc9d47fc12 100644 --- a/glide.yaml +++ b/glide.yaml @@ -29,3 +29,4 @@ import: - package: github.com/inconshreveable/log15 - package: github.com/Sirupsen/logrus version: ^0.11.0 +- package: github.com/streadway/simpleuuid diff --git a/logging/adapters/tendermint_log15/convert.go b/logging/adapters/tendermint_log15/convert.go index 52cda465988566abeda50ac6ab28fad5c8a90838..f7be8420866a67a987afb39f84b2e93cea849018 100644 --- a/logging/adapters/tendermint_log15/convert.go +++ b/logging/adapters/tendermint_log15/convert.go @@ -27,7 +27,7 @@ func LogLineToRecord(keyvals ...interface{}) *log15.Record { Lvl: Log15LvlFromString(level), Msg: message, Call: call, - Ctx: append(ctx, structure.CallerKey, call), + Ctx: ctx, KeyNames: log15.RecordKeyNames{ Time: structure.TimeKey, Msg: structure.MessageKey, diff --git a/logging/convention.go b/logging/convention.go new file mode 100644 index 0000000000000000000000000000000000000000..3c75270ac36e65e35f7cc9af14080e282ac873da --- /dev/null +++ b/logging/convention.go @@ -0,0 +1,49 @@ +package logging + +import ( + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + "github.com/eris-ltd/eris-db/util/slice" + kitlog "github.com/go-kit/kit/log" +) + +// Helper functions for InfoTraceLoggers, sort of extension methods to loggers +// to centralise and establish logging conventions on top of in with the base +// logging interface + +// Record structured Info log line with a message and conventional keys +func InfoMsgVals(logger loggers.InfoTraceLogger, message string, vals ...interface{}) { + MsgVals(kitlog.LoggerFunc(logger.Info), message, vals...) +} + +// Record structured Trace log line with a message and conventional keys +func TraceMsgVals(logger loggers.InfoTraceLogger, message string, vals ...interface{}) { + MsgVals(kitlog.LoggerFunc(logger.Trace), message, vals...) +} + +// Record structured Info log line with a message +func InfoMsg(logger loggers.InfoTraceLogger, message string, keyvals ...interface{}) { + Msg(kitlog.LoggerFunc(logger.Info), message, keyvals...) +} + +// Record structured Trace log line with a message +func TraceMsg(logger loggers.InfoTraceLogger, message string, keyvals ...interface{}) { + Msg(kitlog.LoggerFunc(logger.Trace), message, keyvals...) +} + +// Record a structured log line with a message +func Msg(logger kitlog.Logger, message string, keyvals ...interface{}) error { + prepended := slice.CopyPrepend(keyvals, structure.MessageKey, message) + return logger.Log(prepended...) +} + +// Record a structured log line with a message and conventional keys +func MsgVals(logger kitlog.Logger, message string, vals ...interface{}) error { + keyvals := make([]interface{}, len(vals)*2) + for i := 0; i < len(vals); i++ { + kv := i * 2 + keyvals[kv] = structure.KeyFromValue(vals[i]) + keyvals[kv+1] = vals[i] + } + return Msg(logger, message, keyvals) +} diff --git a/logging/lifecycle/lifecycle.go b/logging/lifecycle/lifecycle.go index fd85a3248a1d5329d66077b4b8d8720d8d7a242a..4468e54b4baf2f319cf9ff77481400f35357dece 100644 --- a/logging/lifecycle/lifecycle.go +++ b/logging/lifecycle/lifecycle.go @@ -6,11 +6,14 @@ import ( "os" "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/adapters/stdlib" tmLog15adapter "github.com/eris-ltd/eris-db/logging/adapters/tendermint_log15" "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" kitlog "github.com/go-kit/kit/log" tmLog15 "github.com/tendermint/log15" - "github.com/eris-ltd/eris-db/logging/structure" + "github.com/streadway/simpleuuid" + "time" ) func NewLoggerFromConfig(LoggingConfig logging.LoggingConfig) loggers.InfoTraceLogger { @@ -27,11 +30,22 @@ func NewStdErrLogger() loggers.InfoTraceLogger { func NewLogger(infoLogger, traceLogger kitlog.Logger) loggers.InfoTraceLogger { infoTraceLogger := loggers.NewInfoTraceLogger(infoLogger, traceLogger) - return logging.WithMetadata(infoTraceLogger) + // Create a random ID based on start time + uuid, _ := simpleuuid.NewTime(time.Now()) + var runId string + if uuid != nil { + runId = uuid.String() + } + return logging.WithMetadata(infoTraceLogger.With(structure.RunId, runId)) } func CaptureTendermintLog15Output(infoTraceLogger loggers.InfoTraceLogger) { tmLog15.Root().SetHandler( tmLog15adapter.InfoTraceLoggerAsLog15Handler(infoTraceLogger. - With(structure.ComponentKey, "tendermint"))) + With(structure.ComponentKey, "tendermint_log15"))) +} + +func CaptureStdlibLogOutput(infoTraceLogger loggers.InfoTraceLogger) { + stdlib.CaptureRootLogger(infoTraceLogger. + With(structure.ComponentKey, "stdlib_log")) } diff --git a/logging/loggers/info_trace_logger.go b/logging/loggers/info_trace_logger.go index 7374477a92a40a80a8d726bf4da136f203e645af..6cbdcee6bd9f0aeb2d7965465835f304226eb42a 100644 --- a/logging/loggers/info_trace_logger.go +++ b/logging/loggers/info_trace_logger.go @@ -33,7 +33,7 @@ type InfoTraceLogger interface { // 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{}) + Info(keyvals ...interface{}) error // 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 @@ -41,7 +41,7 @@ type InfoTraceLogger interface { // 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{}) + Trace(keyvals ...interface{}) error // 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 @@ -66,15 +66,16 @@ 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 will never halt the progress of 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, - }))) + logger := kitlog.NewContext(NonBlockingLogger(VectorValuedLogger( + MultipleChannelLogger( + map[string]kitlog.Logger{ + InfoChannelName: infoLogger, + TraceChannelName: traceLogger, + })))) return &infoTraceLogger{ infoLogger: logger.With( structure.ChannelKey, InfoChannelName, @@ -106,13 +107,13 @@ func (l *infoTraceLogger) WithPrefix(keyvals ...interface{}) InfoTraceLogger { } } -func (l *infoTraceLogger) Info(keyvals ...interface{}) { +func (l *infoTraceLogger) Info(keyvals ...interface{}) error { // We send Info and Trace log lines down the same pipe to keep them ordered - l.infoLogger.Log(keyvals...) + return l.infoLogger.Log(keyvals...) } -func (l *infoTraceLogger) Trace(keyvals ...interface{}) { - l.traceLogger.Log(keyvals...) +func (l *infoTraceLogger) Trace(keyvals ...interface{}) error { + return l.traceLogger.Log(keyvals...) } // If logged to as a plain kitlog logger presume the message is for Trace diff --git a/logging/loggers/vector_valued_logger.go b/logging/loggers/vector_valued_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..b8963db13f61a9e02fdf5273010d9f4910d51009 --- /dev/null +++ b/logging/loggers/vector_valued_logger.go @@ -0,0 +1,21 @@ +package loggers + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +// Treat duplicate key-values as consecutive entries in a vector-valued lookup +type vectorValuedLogger struct { + logger kitlog.Logger +} + +var _ kitlog.Logger = &vectorValuedLogger{} + +func (vvl *vectorValuedLogger) Log(keyvals ...interface{}) error { + return vvl.logger.Log(structure.Vectorise(keyvals)...) +} + +func VectorValuedLogger(logger kitlog.Logger) *vectorValuedLogger { + return &vectorValuedLogger{logger: logger} +} diff --git a/logging/loggers/vector_valued_logger_test.go b/logging/loggers/vector_valued_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d6bc54ebdcfb76d0a7b3f20606828d458e6a5d13 --- /dev/null +++ b/logging/loggers/vector_valued_logger_test.go @@ -0,0 +1,17 @@ +package loggers + +import ( + "testing" + + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/stretchr/testify/assert" +) + +func TestVectorValuedLogger(t *testing.T) { + logger := newTestLogger() + vvl := VectorValuedLogger(logger) + vvl.Log("foo", "bar", "seen", 1, "seen", 3, "seen", 2) + + assert.Equal(t, Slice("foo", "bar", "seen", Slice(1, 3, 2)), + logger.logLines[0]) +} diff --git a/logging/metadata.go b/logging/metadata.go index 3fbed2cf4c819a5e28c0788515a3920f45b209ee..d645699902e9a6b9a334390a8824c774aac5a4e6 100644 --- a/logging/metadata.go +++ b/logging/metadata.go @@ -6,6 +6,7 @@ import ( "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/logging/structure" kitlog "github.com/go-kit/kit/log" + "github.com/go-stack/stack" ) const ( @@ -23,5 +24,10 @@ var defaultTimestampUTCValuer kitlog.Valuer = func() interface{} { func WithMetadata(infoTraceLogger loggers.InfoTraceLogger) loggers.InfoTraceLogger { return infoTraceLogger.With(structure.TimeKey, defaultTimestampUTCValuer, - structure.CallerKey, kitlog.Caller(infoTraceLoggerCallDepth)) + structure.CallerKey, kitlog.Caller(infoTraceLoggerCallDepth), + "trace", TraceValuer()) +} + +func TraceValuer() kitlog.Valuer { + return func() interface{} { return stack.Trace() } } diff --git a/logging/structure/structure.go b/logging/structure/structure.go index 9420b20095b4fabab87ce707f082d17968f90ba3..976332be645eb9ea02442819d80f9c250f7001b2 100644 --- a/logging/structure/structure.go +++ b/logging/structure/structure.go @@ -1,20 +1,29 @@ package structure -import . "github.com/eris-ltd/eris-db/util/slice" +import ( + "reflect" + + . "github.com/eris-ltd/eris-db/util/slice" +) const ( - // Key for go time.Time object + // Log time (time.Time) TimeKey = "time" - // Key for call site for log invocation + // Call site for log invocation (go-stack.Call) CallerKey = "caller" - // Key for String name for level - LevelKey = "level" - // Key to switch on for channel in a multiple channel logging context + // Level name (string) + LevelKey = "level" + // Channel name in a vector channel logging context ChannelKey = "channel" - // Key for string message + // Log message (string) MessageKey = "message" - // Key for module or function or struct that is the subject of the logging + // Top-level component (choose one) name ComponentKey = "component" + // Vector-valued scope + ScopeKey = "scope" + // Globally unique identifier persisting while a single instance (root process) + // of this program/service is running + RunId = "run_id" ) // Pull the specified values from a structured log line into a map. @@ -49,6 +58,56 @@ func ValuesAndContext(keyvals []interface{}, return vals, context } +// Stateful index that tracks the location of a possible vector value +type vectorValueindex struct { + // Location of the value belonging to a key in output slice + valueIndex int + // Whether or not the value is currently a vector + vector bool +} + +// 'Vectorises' values associated with repeated string keys member by collapsing many values into a single vector value. +// The result is a copy of keyvals where the first occurrence of each matching key and its first value are replaced by +// that key and all of its values in a single slice. +func Vectorise(keyvals []interface{}, vectorKeys ...string) []interface{} { + outputKeyvals := make([]interface{}, 0, len(keyvals)) + // Track the location and vector status of the values in the output + valueIndices := make(map[string]*vectorValueindex, len(vectorKeys)) + elided := 0 + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + key := keyvals[i] + val := keyvals[i+1] + + // Only attempt to vectorise string keys + if k, ok := key.(string); ok { + if valueIndices[k] == nil { + // Record that this key has been seen once + valueIndices[k] = &vectorValueindex{ + valueIndex: i + 1 - elided, + } + // Copy the key-value to output with the single value + outputKeyvals = append(outputKeyvals, key, val) + } else { + // We have seen this key before + vi := valueIndices[k] + if !vi.vector { + // This must be the only second occurrence of the key so now vectorise the value + outputKeyvals[vi.valueIndex] = []interface{}{outputKeyvals[vi.valueIndex]} + vi.vector = true + } + // Grow the vector value + outputKeyvals[vi.valueIndex] = append(outputKeyvals[vi.valueIndex].([]interface{}), val) + // We are now running two more elements behind the input keyvals because we have absorbed this key-value pair + elided += 2 + } + } else { + // Just copy the key-value to the output for non-string keys + outputKeyvals = append(outputKeyvals, key, val) + } + } + return outputKeyvals +} + // 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 { @@ -57,4 +116,16 @@ func Value(keyvals []interface{}, key interface{}) interface{} { } } return nil -} \ No newline at end of file +} + +// Obtain a canonical key from a value. Useful for structured logging where the +// type of value alone may be sufficient to determine its key. Providing this +// function centralises any convention over type names +func KeyFromValue(val interface{}) string { + switch val.(type) { + case string: + return "text" + default: + return reflect.TypeOf(val).Name() + } +} diff --git a/logging/structure/structure_test.go b/logging/structure/structure_test.go index 2fbdaa3e5e1c525c5ccf836b430ad13ebbb5333a..fde15ff4ae247a02166d91fe7850328c01417e33 100644 --- a/logging/structure/structure_test.go +++ b/logging/structure/structure_test.go @@ -13,3 +13,24 @@ func TestValuesAndContext(t *testing.T) { assert.Equal(t, map[interface{}]interface{}{"hello": 1, "fish": 3}, vals) assert.Equal(t, Slice("dog", 2, "fork", 5), ctx) } + +func TestVectorise(t *testing.T) { + kvs := Slice( + "scope", "lawnmower", + "hub", "budub", + "occupation", "fish brewer", + "scope", "hose pipe", + "flub", "dub", + "scope", "rake", + "flub", "brub", + ) + + kvsVector := Vectorise(kvs, "occupation", "scope") + assert.Equal(t, Slice( + "scope", Slice("lawnmower", "hose pipe", "rake"), + "hub", "budub", + "occupation", "fish brewer", + "flub", Slice("dub", "brub"), + ), + kvsVector) +} diff --git a/rpc/tendermint/test/common.go b/rpc/tendermint/test/common.go index 76909182df98801eab3e08db5b5c4bbfd03617cf..a060833c5091b00156e855c70a276753115d925f 100644 --- a/rpc/tendermint/test/common.go +++ b/rpc/tendermint/test/common.go @@ -6,6 +6,7 @@ package test import ( "fmt" + vm "github.com/eris-ltd/eris-db/manager/eris-mint/evm" rpc_core "github.com/eris-ltd/eris-db/rpc/tendermint/core" "github.com/eris-ltd/eris-db/test/fixtures" ) @@ -17,6 +18,7 @@ func TestWrapper(runner func() int) int { defer ffs.RemoveAll() + vm.SetDebug(true) err := initGlobalVariables(ffs) if err != nil { diff --git a/rpc/tendermint/test/shared.go b/rpc/tendermint/test/shared.go index 8ddce9b340f3aa507ccbffe1fe736408e41cb6b3..8c01b6a2f024d402cbea6dc1e7cea5f54dbf7f7e 100644 --- a/rpc/tendermint/test/shared.go +++ b/rpc/tendermint/test/shared.go @@ -88,6 +88,8 @@ func initGlobalVariables(ffs *fixtures.FileFixtures) error { // To spill tendermint logs on the floor: // lifecycle.CaptureTendermintLog15Output(loggers.NewNoopInfoTraceLogger()) lifecycle.CaptureTendermintLog15Output(logger) + lifecycle.CaptureStdlibLogOutput(logger) + testCore, err = core.NewCore("testCore", consensusConfig, managerConfig, logger) if err != nil { diff --git a/util/slice/slice.go b/util/slice/slice.go index 55332d87f5c96aa753f6f0580b306f77385a5134..2c51efd21f89c3478a185d623e54dcc9d4ea3064 100644 --- a/util/slice/slice.go +++ b/util/slice/slice.go @@ -58,7 +58,34 @@ func Delete(slice []interface{}, i int, n int) []interface{} { return append(slice[:i], slice[i+n:]...) } -// +// Delete an element at a specific index and return the contracted list func DeleteAt(slice []interface{}, i int) []interface{} { return Delete(slice, i, 1) } + +// Flatten a slice by a list by splicing any elements of the list that are +// themselves lists into the slice elements to the list in place of slice itself +func Flatten(slice []interface{}) []interface{} { + return DeepFlatten(slice, 1) +} + +// Recursively flattens a list by splicing any sub-lists into their parent until +// depth is reached. If a negative number is passed for depth then it continues +// until no elements of the returned list are lists +func DeepFlatten(slice []interface{}, depth int) []interface{} { + if depth == 0 { + return slice + } + returnSlice := []interface{}{} + + for _, element := range slice { + if s, ok := element.([]interface{}); ok { + returnSlice = append(returnSlice, DeepFlatten(s, depth-1)...) + } else { + returnSlice = append(returnSlice, element) + } + + } + + return returnSlice +} diff --git a/util/slice/slice_test.go b/util/slice/slice_test.go index 4a1f53e71ad0f5c4518e73c1d7b2eaa34ef8abc8..b3d1c9b6a530b1fe050427b3ba41ae8f9f23c388 100644 --- a/util/slice/slice_test.go +++ b/util/slice/slice_test.go @@ -23,14 +23,21 @@ func TestCopyPrepend(t *testing.T) { } 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, 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 + assert.Equal(t, Slice(1, 2, 4, 5), Delete(Slice(1, 2, 3, 4, 5), 2, 1)) +} + +func TestDeepFlatten(t *testing.T) { + assert.Equal(t, Flatten(Slice(Slice(1, 2), 3, 4)), Slice(1, 2, 3, 4)) + nestedSlice := Slice(Slice(1, Slice(Slice(2))), Slice(3, 4)) + assert.Equal(t, DeepFlatten(nestedSlice, -1), Slice(1, 2, 3, 4)) + assert.Equal(t, DeepFlatten(nestedSlice, 2), Slice(1, Slice(2), 3, 4)) +}