From 9b48450ef662b7da568366492e0bc9ba3fcdc1f1 Mon Sep 17 00:00:00 2001 From: Silas Davis <silas@erisindustries.com> Date: Tue, 31 Jan 2017 17:37:44 +0000 Subject: [PATCH] Refinements to ChannelLogger and added filters to config --- core/config.go | 10 +- logging/config/filter.go | 21 +- logging/config/filter_test.go | 83 +++++- logging/config/sinks.go | 269 ++++++++++++++++-- logging/config/sinks_test.go | 133 ++++++--- logging/config/types/types.go | 90 ------ logging/lifecycle/lifecycle.go | 24 +- logging/loggers/capture_logger.go | 65 +++++ logging/loggers/capture_logger_test.go | 45 +++ logging/loggers/channel_logger.go | 107 +++++-- logging/loggers/channel_logger_test.go | 18 ++ logging/loggers/eris_format_logger.go | 4 + logging/loggers/filter_logger_test.go | 4 +- logging/loggers/info_trace_logger.go | 27 +- logging/loggers/logging_test.go | 35 --- logging/loggers/multiple_channel_logger.go | 51 ---- .../loggers/multiple_channel_logger_test.go | 42 --- .../loggers/multiple_output_logger_test.go | 4 +- logging/loggers/shared_test.go | 35 +++ logging/loggers/vector_valued_logger_test.go | 2 +- 20 files changed, 703 insertions(+), 366 deletions(-) delete mode 100644 logging/config/types/types.go create mode 100644 logging/loggers/capture_logger.go create mode 100644 logging/loggers/capture_logger_test.go delete mode 100644 logging/loggers/logging_test.go delete mode 100644 logging/loggers/multiple_channel_logger.go delete mode 100644 logging/loggers/multiple_channel_logger_test.go create mode 100644 logging/loggers/shared_test.go diff --git a/core/config.go b/core/config.go index c6278384..70c20713 100644 --- a/core/config.go +++ b/core/config.go @@ -26,7 +26,7 @@ import ( "github.com/eris-ltd/eris-db/config" "github.com/eris-ltd/eris-db/consensus" "github.com/eris-ltd/eris-db/definitions" - lctypes "github.com/eris-ltd/eris-db/logging/config/types" + lconfig "github.com/eris-ltd/eris-db/logging/config" "github.com/eris-ltd/eris-db/manager" "github.com/eris-ltd/eris-db/server" "github.com/eris-ltd/eris-db/util" @@ -113,14 +113,14 @@ func LoadServerConfig(do *definitions.Do) (*server.ServerConfig, error) { return serverConfig, err } -func LoadLoggingConfigFromDo(do *definitions.Do) (*lctypes.LoggingConfig, error) { +func LoadLoggingConfigFromDo(do *definitions.Do) (*lconfig.LoggingConfig, error) { //subConfig, err := SubConfig(conf, "logging") - loggingConfig := &lctypes.LoggingConfig{} + loggingConfig := &lconfig.LoggingConfig{} return loggingConfig, nil } -func LoadLoggingConfigFromClientDo(do *definitions.ClientDo) (*lctypes.LoggingConfig, error) { - loggingConfig := &lctypes.LoggingConfig{} +func LoadLoggingConfigFromClientDo(do *definitions.ClientDo) (*lconfig.LoggingConfig, error) { + loggingConfig := &lconfig.LoggingConfig{} return loggingConfig, nil } diff --git a/logging/config/filter.go b/logging/config/filter.go index 204a0929..612b2be8 100644 --- a/logging/config/filter.go +++ b/logging/config/filter.go @@ -2,33 +2,26 @@ package config import ( "fmt" - "os" "regexp" - "github.com/eapache/channels" "github.com/eris-ltd/eris-db/common/math/integral" - "github.com/eris-ltd/eris-db/logging/config/types" - "github.com/eris-ltd/eris-db/logging/loggers" - kitlog "github.com/go-kit/kit/log" ) -func BuildFilterPredicate(filterConfig *types.FilterConfig) (func([]interface{}) bool, error) { - includePredicate, err := BuildKeyValuesPredicate(filterConfig.Include, true) - if err != nil { - return nil, err - } - excludePredicate, err := BuildKeyValuesPredicate(filterConfig.Exclude, false) +func BuildFilterPredicate(filterConfig *FilterConfig) (func([]interface{}) bool, error) { + predicate, err := BuildKeyValuesPredicate(filterConfig.Predicates, + filterConfig.FilterMode.MatchAll()) if err != nil { return nil, err } + include := filterConfig.FilterMode.Include() return func(keyvals []interface{}) bool { - // When do we want to exclude a log line - return !includePredicate(keyvals) || excludePredicate(keyvals) + // XOR the predicate with include. If include is true then negate the match. + return predicate(keyvals) != include }, nil } -func BuildKeyValuesPredicate(kvpConfigs []*types.KeyValuePredicateConfig, +func BuildKeyValuesPredicate(kvpConfigs []*KeyValuePredicateConfig, matchAll bool) (func([]interface{}) bool, error) { length := len(kvpConfigs) keyRegexes := make([]*regexp.Regexp, length) diff --git a/logging/config/filter_test.go b/logging/config/filter_test.go index a0bf18ad..ea6d2fdb 100644 --- a/logging/config/filter_test.go +++ b/logging/config/filter_test.go @@ -3,15 +3,14 @@ package config import ( "testing" - "github.com/eris-ltd/eris-db/logging/config/types" . "github.com/eris-ltd/eris-db/util/slice" "github.com/stretchr/testify/assert" ) func TestBuildKeyValuesPredicateMatchAll(t *testing.T) { - conf := []*types.KeyValuePredicateConfig{ + conf := []*KeyValuePredicateConfig{ { - KeyRegex: "Foo", + KeyRegex: "Foo", ValueRegex: "bar", }, } @@ -21,7 +20,7 @@ func TestBuildKeyValuesPredicateMatchAll(t *testing.T) { } func TestBuildKeyValuesPredicateMatchAny(t *testing.T) { - conf := []*types.KeyValuePredicateConfig{ + conf := []*KeyValuePredicateConfig{ { KeyRegex: "Bosh", ValueRegex: "Bish", @@ -32,23 +31,89 @@ func TestBuildKeyValuesPredicateMatchAny(t *testing.T) { assert.True(t, kvp(Slice("Foo", "bar", "Bosh", "Bish"))) } -func TestBuildFilterPredicate(t *testing.T) { - fc := &types.FilterConfig{ - Include: []*types.KeyValuePredicateConfig{ +func TestExcludeAllFilterPredicate(t *testing.T) { + fc := &FilterConfig{ + FilterMode: ExcludeWhenAllMatch, + Predicates: []*KeyValuePredicateConfig{ { - KeyRegex: "^Foo$", + KeyRegex: "Bosh", + ValueRegex: "Bish", + }, + { + KeyRegex: "Bosh", + ValueRegex: "Bash", }, }, - Exclude: []*types.KeyValuePredicateConfig{ + } + fp, err := BuildFilterPredicate(fc) + assert.NoError(t, err) + assert.False(t, fp(Slice("Bosh", "Bash", "Shoes", 42))) + assert.True(t, fp(Slice("Bosh", "Bash", "Foo", "bar", "Shoes", 42, "Bosh", "Bish"))) + assert.False(t, fp(Slice("Food", 0.2, "Shoes", 42))) + +} +func TestExcludeAnyFilterPredicate(t *testing.T) { + fc := &FilterConfig{ + FilterMode: ExcludeWhenAnyMatches, + Predicates: []*KeyValuePredicateConfig{ { KeyRegex: "Bosh", ValueRegex: "Bish", }, + { + KeyRegex: "Bosh", + ValueRegex: "Bash", + }, }, } fp, err := BuildFilterPredicate(fc) assert.NoError(t, err) assert.False(t, fp(Slice("Foo", "bar", "Shoes", 42))) assert.True(t, fp(Slice("Foo", "bar", "Shoes", 42, "Bosh", "Bish"))) + assert.True(t, fp(Slice("Food", 0.2, "Shoes", 42, "Bosh", "Bish"))) + +} + +func TestIncludeAllFilterPredicate(t *testing.T) { + fc := &FilterConfig{ + FilterMode: IncludeWhenAllMatch, + Predicates: []*KeyValuePredicateConfig{ + { + KeyRegex: "Bosh", + ValueRegex: "Bish", + }, + { + KeyRegex: "Planks", + ValueRegex: "^0.2$", + }, + }, + } + fp, err := BuildFilterPredicate(fc) + assert.NoError(t, err) + assert.True(t, fp(Slice("Foo", "bar", "Shoes", 42))) + // Don't filter, it has all the required key values + assert.False(t, fp(Slice("Foo", "bar", "Planks", 0.2, "Shoes", 42, "Bosh", "Bish"))) assert.True(t, fp(Slice("Food", 0.2, "Shoes", 42))) } + +func TestIncludeAnyFilterPredicate(t *testing.T) { + fc := &FilterConfig{ + FilterMode: IncludeWhenAnyMatches, + Predicates: []*KeyValuePredicateConfig{ + { + KeyRegex: "Bosh", + ValueRegex: "Bish", + }, + { + KeyRegex: "^Shoes$", + ValueRegex: "42", + }, + }, + } + fp, err := BuildFilterPredicate(fc) + assert.NoError(t, err) + assert.False(t, fp(Slice("Foo", "bar", "Shoes", 3427))) + assert.False(t, fp(Slice("Foo", "bar", "Shoes", 42, "Bosh", "Bish"))) + assert.False(t, fp(Slice("Food", 0.2, "Shoes", 42))) + +} diff --git a/logging/config/sinks.go b/logging/config/sinks.go index 080f7c42..25fe29ee 100644 --- a/logging/config/sinks.go +++ b/logging/config/sinks.go @@ -4,21 +4,226 @@ import ( "fmt" "os" - "regexp" - "github.com/eapache/channels" - "github.com/eris-ltd/eris-db/common/math/integral" - "github.com/eris-ltd/eris-db/logging/config/types" "github.com/eris-ltd/eris-db/logging/loggers" kitlog "github.com/go-kit/kit/log" ) -func BuildLoggerFromRootSinkConfig(sinkConfig *types.SinkConfig) (kitlog.Logger, map[string]*loggers.ChannelLogger, error) { - return BuildLoggerFromSinkConfig(sinkConfig, make(map[string]*loggers.ChannelLogger)) +type source string +type outputType string +type transformType string +type filterMode string + +const ( + // OutputType + NoOutput outputType = "" + Graylog outputType = "Graylog" + Syslog outputType = "Syslog" + File outputType = "File" + Stdout outputType = "Stdout" + Stderr outputType = "Stderr" + + // TransformType + NoTransform transformType = "" + // Filter log lines + Filter transformType = "Filter" + // Remove key-val pairs from each log line + Prune transformType = "Prune" + Capture transformType = "Capture" + Label transformType = "Label" + + IncludeWhenAllMatch filterMode = "IncludeWhenAllMatch" + IncludeWhenAnyMatches filterMode = "IncludeWhenAnyMatches" + ExcludeWhenAllMatch filterMode = "ExcludeWhenAllMatch" + ExcludeWhenAnyMatches filterMode = "ExcludeWhenAnyMatches" +) + +// Only include log lines matching the filter so negate the predicate in filter +func (mode filterMode) Include() bool { + switch mode { + case IncludeWhenAllMatch, IncludeWhenAnyMatches: + return true + default: + return false + } +} + +// The predicate should only evaluate true if all the key value predicates match +func (mode filterMode) MatchAll() bool { + switch mode { + case IncludeWhenAllMatch, ExcludeWhenAllMatch: + return true + default: + return false + } +} + +// Exclude log lines that match the predicate +func (mode filterMode) Exclude() bool { + return !mode.Include() } -func BuildLoggerFromSinkConfig(sinkConfig *types.SinkConfig, - captures map[string]*loggers.ChannelLogger) (kitlog.Logger, map[string]*loggers.ChannelLogger, error) { +// The predicate should evaluate true if at least one of the key value predicates matches +func (mode filterMode) MatchAny() bool { + return !mode.MatchAny() +} + +// Sink configuration types +type ( + // Outputs + GraylogConfig struct { + } + + SyslogConfig struct { + } + + FileConfig struct { + Path string + } + + OutputConfig struct { + OutputType outputType + *GraylogConfig + *FileConfig + *SyslogConfig + } + + // Transforms + LabelConfig struct { + Labels map[string]string + Prefix bool + } + + CaptureConfig struct { + Name string + BufferCap int + Passthrough bool + } + + // Generates true if KeyRegex matches a log line key and ValueRegex matches that key's value. + // If ValueRegex is empty then returns true if any key matches + // If KeyRegex is empty then returns true if any value matches + KeyValuePredicateConfig struct { + KeyRegex string + ValueRegex string + } + + // Filter types + + FilterConfig struct { + FilterMode filterMode + // Predicates to match a log line against using FilterMode + Predicates []*KeyValuePredicateConfig + } + + TransformConfig struct { + TransformType transformType + *LabelConfig + *CaptureConfig + *FilterConfig + } + + // Sink + // A Sink describes a logger that logs to zero or one output and logs to zero or more child sinks. + // before transmitting its log it applies zero or one transforms to the stream of log lines. + // by chaining together many Sinks arbitrary transforms to and multi + SinkConfig struct { + Transform *TransformConfig + Sinks []*SinkConfig + Output *OutputConfig + } + + LoggingConfig struct { + InfoSink *SinkConfig + InfoAndTraceSink *SinkConfig + } +) + +// Builders + +func Sink() *SinkConfig { + return &SinkConfig{} +} + +func (sinkConfig *SinkConfig) AddSinks(sinks ...*SinkConfig) *SinkConfig { + sinkConfig.Sinks = append(sinkConfig.Sinks, sinks...) + return sinkConfig +} + +func (sinkConfig *SinkConfig) SetTransform(transform *TransformConfig) *SinkConfig { + sinkConfig.Transform = transform + return sinkConfig +} + +func (sinkConfig *SinkConfig) SetOutput(output *OutputConfig) *SinkConfig { + sinkConfig.Output = output + return sinkConfig +} + +func StdoutOutput() *OutputConfig { + return &OutputConfig{ + OutputType: Stdout, + } +} +func StderrOutput() *OutputConfig { + return &OutputConfig{ + OutputType: Stderr, + } +} + +func CaptureTransform(name string, bufferCap int, passthrough bool) *TransformConfig { + return &TransformConfig{ + TransformType: Capture, + CaptureConfig: &CaptureConfig{ + Name: name, + BufferCap: bufferCap, + Passthrough: passthrough, + }, + } +} + +func LabelTransform(prefix bool, labelKeyvals ...string) *TransformConfig { + length := len(labelKeyvals) / 2 + labels := make(map[string]string, length) + for i := 0; i < 2*length; i += 2 { + labels[labelKeyvals[i]] = labelKeyvals[i+1] + } + return &TransformConfig{ + TransformType: Label, + LabelConfig: &LabelConfig{ + Prefix: prefix, + Labels: labels, + }, + } +} + +func FilterTransform(fmode filterMode, keyvalueRegexes ...string) *TransformConfig { + length := len(keyvalueRegexes) / 2 + predicates := make([]*KeyValuePredicateConfig, length) + for i := 0; i < length; i++ { + kv := i * 2 + predicates[i] = &KeyValuePredicateConfig{ + KeyRegex: keyvalueRegexes[kv], + ValueRegex: keyvalueRegexes[kv+1], + } + } + return &TransformConfig{ + TransformType: Filter, + FilterConfig: &FilterConfig{ + FilterMode: fmode, + Predicates: predicates, + }, + } +} + +func (sinkConfig *SinkConfig) BuildLogger() (kitlog.Logger, map[string]*loggers.CaptureLogger, error) { + return BuildLoggerFromSinkConfig(sinkConfig, make(map[string]*loggers.CaptureLogger)) +} + +// Logger formation + +func BuildLoggerFromSinkConfig(sinkConfig *SinkConfig, + captures map[string]*loggers.CaptureLogger) (kitlog.Logger, map[string]*loggers.CaptureLogger, error) { if sinkConfig == nil { return kitlog.NewNopLogger(), captures, nil } @@ -32,7 +237,7 @@ func BuildLoggerFromSinkConfig(sinkConfig *types.SinkConfig, outputLoggers[i] = l } - if sinkConfig.Output != nil && sinkConfig.Output.OutputType != types.NoOutput { + if sinkConfig.Output != nil && sinkConfig.Output.OutputType != NoOutput { l, err := BuildOutputLogger(sinkConfig.Output) if err != nil { return nil, captures, err @@ -42,35 +247,37 @@ func BuildLoggerFromSinkConfig(sinkConfig *types.SinkConfig, outputLogger := loggers.NewMultipleOutputLogger(outputLoggers...) - if sinkConfig.Transform != nil && sinkConfig.Transform.TransformType != types.NoTransform { + if sinkConfig.Transform != nil && sinkConfig.Transform.TransformType != NoTransform { return BuildTransformLogger(sinkConfig.Transform, captures, outputLogger) } return outputLogger, captures, nil } -func BuildOutputLogger(outputConfig *types.OutputConfig) (kitlog.Logger, error) { +func BuildOutputLogger(outputConfig *OutputConfig) (kitlog.Logger, error) { switch outputConfig.OutputType { - case types.NoOutput: + case NoOutput: return kitlog.NewNopLogger(), nil - //case types.Graylog: - //case types.Syslog: - case types.Stdout: + //case Graylog: + //case Syslog: + case Stdout: return loggers.NewStreamLogger(os.Stdout), nil - case types.Stderr: + case Stderr: return loggers.NewStreamLogger(os.Stderr), nil - case types.File: + case File: return loggers.NewFileLogger(outputConfig.FileConfig.Path) default: - return nil, fmt.Errorf("Could not build logger for output: '%s'", outputConfig.OutputType) + return nil, fmt.Errorf("Could not build logger for output: '%s'", + outputConfig.OutputType) } } -func BuildTransformLogger(transformConfig *types.TransformConfig, captures map[string]*loggers.ChannelLogger, - outputLogger kitlog.Logger) (kitlog.Logger, map[string]*loggers.ChannelLogger, error) { +func BuildTransformLogger(transformConfig *TransformConfig, + captures map[string]*loggers.CaptureLogger, + outputLogger kitlog.Logger) (kitlog.Logger, map[string]*loggers.CaptureLogger, error) { switch transformConfig.TransformType { - case types.NoTransform: + case NoTransform: return outputLogger, captures, nil - case types.Label: + case Label: keyvals := make([]interface{}, 0, len(transformConfig.Labels)*2) for k, v := range transformConfig.LabelConfig.Labels { keyvals = append(keyvals, k, v) @@ -80,18 +287,22 @@ func BuildTransformLogger(transformConfig *types.TransformConfig, captures map[s } else { return kitlog.NewContext(outputLogger).With(keyvals...), captures, nil } - case types.Capture: + case Capture: name := transformConfig.CaptureConfig.Name if _, ok := captures[name]; ok { return nil, captures, fmt.Errorf("Could not register new logging capture since name '%s' already "+ "registered", name) } - // Create a buffered channel logger to capture logs from upstream - cl := loggers.NewChannelLogger(channels.BufferCap(transformConfig.CaptureConfig.BufferCap)) - captures[name] = cl - // Return a logger that tees intput logs to this ChannelLogger and the passed in output logger - return loggers.NewMultipleOutputLogger(cl, outputLogger), captures, nil - case types.Filter: + // Create a capture logger according to configuration (it may tee the output) + // or capture it to be flushed later + captureLogger := loggers.NewCaptureLogger(outputLogger, + channels.BufferCap(transformConfig.CaptureConfig.BufferCap), + transformConfig.CaptureConfig.Passthrough) + // Register the capture + captures[name] = captureLogger + // Pass it upstream to be logged to + return captureLogger, captures, nil + case Filter: predicate, err := BuildFilterPredicate(transformConfig.FilterConfig) if err != nil { return nil, captures, fmt.Errorf("Could not build filter predicate: '%s'", err) diff --git a/logging/config/sinks_test.go b/logging/config/sinks_test.go index d300901a..8191369b 100644 --- a/logging/config/sinks_test.go +++ b/logging/config/sinks_test.go @@ -3,54 +3,99 @@ package config import ( "testing" - "github.com/eris-ltd/eris-db/logging/config/types" - . "github.com/eris-ltd/eris-db/util/slice" "github.com/stretchr/testify/assert" ) func TestBuildLoggerFromSinkConfig(t *testing.T) { - sinkConfig := &types.SinkConfig{ - Transform: &types.TransformConfig{ - TransformType: types.NoTransform, - }, - Sinks: []*types.SinkConfig{ - { - Transform: &types.TransformConfig{ - TransformType: types.NoTransform, - }, - Sinks: []*types.SinkConfig{ - { - Transform: &types.TransformConfig{ - TransformType: types.Capture, - CaptureConfig: &types.CaptureConfig{ - Name: "cap", - BufferCap: 100, - }, - }, - Output: &types.OutputConfig{ - OutputType: types.Stderr, - }, - Sinks: []*types.SinkConfig{ - { - Transform: &types.TransformConfig{ - TransformType: types.Label, - LabelConfig: &types.LabelConfig{ - Prefix: true, - Labels: map[string]string{"Label": "A Label!"}, - }, - }, - Output: &types.OutputConfig{ - OutputType: types.Stdout, - }, - }, - }, - }, - }, - }, - }, - } - logger, captures, err := BuildLoggerFromRootSinkConfig(sinkConfig) + sinkConfig := Sink(). + AddSinks( + Sink(). + AddSinks( + Sink(). + AddSinks( + Sink(). + SetTransform(CaptureTransform("cap", 100, true)). + SetOutput(StderrOutput()). + AddSinks( + Sink(). + SetTransform(LabelTransform(true, "Label", "A Label!")). + SetOutput(StdoutOutput()))))) + + logger, captures, err := sinkConfig.BuildLogger() logger.Log("Foo", "Bar") assert.NoError(t, err) - assert.Equal(t, []interface{}{"Foo", "Bar"}, captures["cap"].WaitReadLogLine()) + assert.Equal(t, logLines("Foo", "Bar"), + captures["cap"].BufferLogger().FlushLogLines()) +} + +func TestFilterSinks(t *testing.T) { + sinkConfig := Sink(). + SetOutput(StderrOutput()). + AddSinks( + Sink(). + SetTransform(FilterTransform(IncludeWhenAnyMatches, + "Foo", "Bar", + "Rough", "Trade", + )). + AddSinks( + Sink(). + SetTransform(CaptureTransform("Included", 100, true)). + AddSinks( + Sink(). + SetTransform(FilterTransform(ExcludeWhenAllMatch, + "Foo", "Baz", + "Index", "00$")). + AddSinks( + Sink(). + SetTransform(CaptureTransform("Excluded", 100, false)), + ), + ), + ), + ) + + logger, captures, err := sinkConfig.BuildLogger() + assert.NoError(t, err, "Should be able to build filter logger") + included := captures["Included"] + excluded := captures["Excluded"] + + // Included by both filters + ll := logLines("Foo", "Bar") + logger.Log(ll[0]...) + assert.Equal(t, logLines("Foo", "Bar"), + included.BufferLogger().FlushLogLines()) + assert.Equal(t, logLines("Foo", "Bar"), + excluded.BufferLogger().FlushLogLines()) + + // Included by first filter and excluded by second + ll = logLines("Foo", "Bar", "Foo", "Baz", "Index", "1000") + logger.Log(ll[0]...) + assert.Equal(t, ll, included.BufferLogger().FlushLogLines()) + assert.Equal(t, logLines(), excluded.BufferLogger().FlushLogLines()) + + // Included by first filter and not excluded by second despite matching one + // predicate + ll = logLines("Rough", "Trade", "Index", "1000") + logger.Log(ll[0]...) + assert.Equal(t, ll, included.BufferLogger().FlushLogLines()) + assert.Equal(t, ll, excluded.BufferLogger().FlushLogLines()) + +} + +// Takes a variadic argument of log lines as a list of key value pairs delimited +// by the empty string +func logLines(keyvals ...string) [][]interface{} { + llines := make([][]interface{}, 0) + line := make([]interface{}, 0) + for _, kv := range keyvals { + if kv == "" { + llines = append(llines, line) + line = make([]interface{}, 0) + } else { + line = append(line, kv) + } + } + if len(line) > 0 { + llines = append(llines, line) + } + return llines } diff --git a/logging/config/types/types.go b/logging/config/types/types.go deleted file mode 100644 index ec72db6f..00000000 --- a/logging/config/types/types.go +++ /dev/null @@ -1,90 +0,0 @@ -package types - -type Source string -type OutputType string -type TransformType string - -const ( - NoOutput OutputType = "" - Graylog OutputType = "Graylog" - Syslog OutputType = "Syslog" - File OutputType = "File" - Stdout OutputType = "Stdout" - Stderr OutputType = "Stderr" - - NoTransform TransformType = "" - // Filter log lines - Filter TransformType = "Filter" - // Remove key-val pairs from each log line - Prune TransformType = "Prune" - Capture TransformType = "Capture" - Label TransformType = "Label" -) - -type ( - // Outputs - GraylogConfig struct { - } - - SyslogConfig struct { - } - - FileConfig struct { - Path string - } - - OutputConfig struct { - OutputType OutputType - *GraylogConfig - *FileConfig - *SyslogConfig - } - - // Transforms - LabelConfig struct { - Labels map[string]string - Prefix bool - } - - CaptureConfig struct { - Name string - BufferCap int - } - - // Generates true if KeyRegex matches a log line key and ValueRegex matches that key's value. - // If ValueRegex is empty then returns true if any key matches - // If KeyRegex is empty then returns true if any value matches - KeyValuePredicateConfig struct { - KeyRegex string - ValueRegex string - } - - FilterConfig struct { - // Only include log lines if they match ALL predicates in Include or include all log lines if empty - Include []*KeyValuePredicateConfig - // Of those log lines included by Include, exclude log lines matching ANY predicate in Exclude or include all log lines if empty - Exclude []*KeyValuePredicateConfig - } - - TransformConfig struct { - TransformType TransformType - *LabelConfig - *CaptureConfig - *FilterConfig - } - - // Sink - // A Sink describes a logger that logs to zero or one output and logs to zero or more child sinks. - // before transmitting its log it applies zero or one transforms to the stream of log lines. - // by chaining together many Sinks arbitrary transforms to and multi - SinkConfig struct { - Transform *TransformConfig - Sinks []*SinkConfig - Output *OutputConfig - } - - LoggingConfig struct { - InfoSink *SinkConfig - InfoAndTraceSink *SinkConfig - } -) diff --git a/logging/lifecycle/lifecycle.go b/logging/lifecycle/lifecycle.go index c04ad6cc..4bbbf31c 100644 --- a/logging/lifecycle/lifecycle.go +++ b/logging/lifecycle/lifecycle.go @@ -27,7 +27,6 @@ import ( "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/logging/structure" - lctypes "github.com/eris-ltd/eris-db/logging/config/types" "github.com/eris-ltd/eris-db/logging/types" kitlog "github.com/go-kit/kit/log" "github.com/streadway/simpleuuid" @@ -38,7 +37,7 @@ import ( // to set up their root logger and capture any other logging output. // Obtain a logger from a LoggingConfig -func NewLoggerFromLoggingConfig(loggingConfig *lctypes.LoggingConfig) (types.InfoTraceLogger, error) { +func NewLoggerFromLoggingConfig(loggingConfig *config.LoggingConfig) (types.InfoTraceLogger, error) { if loggingConfig == nil { return NewStdErrLogger(), nil } @@ -49,8 +48,10 @@ func NewLoggerFromLoggingConfig(loggingConfig *lctypes.LoggingConfig) (types.Inf return NewLogger(infoOnlyLogger, infoAndTraceLogger), nil } -// Hot swap logging config by replacing output loggers of passed InfoTraceLogger with those built from loggingConfig -func SwapOutputLoggersFromLoggingConfig(logger types.InfoTraceLogger, loggingConfig *lctypes.LoggingConfig) error { +// Hot swap logging config by replacing output loggers of passed InfoTraceLogger +// with those built from loggingConfig +func SwapOutputLoggersFromLoggingConfig(logger types.InfoTraceLogger, + loggingConfig *config.LoggingConfig) error { infoOnlyLogger, infoAndTraceLogger, err := infoTraceLoggersLoggingConfig(loggingConfig) if err != nil { return err @@ -65,12 +66,10 @@ func NewStdErrLogger() types.InfoTraceLogger { return NewLogger(nil, logger) } -// Provided a standard eris logger that outputs to the supplied underlying info and trace -// loggers +// Provided a standard eris logger that outputs to the supplied underlying info +// and trace loggers func NewLogger(infoOnlyLogger, infoAndTraceLogger kitlog.Logger) types.InfoTraceLogger { - infoTraceLogger := loggers.NewInfoTraceLogger( - loggers.ErisFormatLogger(infoOnlyLogger), - loggers.ErisFormatLogger(infoAndTraceLogger)) + infoTraceLogger := loggers.NewInfoTraceLogger(infoOnlyLogger, infoAndTraceLogger) // Create a random ID based on start time uuid, _ := simpleuuid.NewTime(time.Now()) var runId string @@ -92,13 +91,12 @@ func CaptureStdlibLogOutput(infoTraceLogger types.InfoTraceLogger) { } // Helpers - -func infoTraceLoggersLoggingConfig(loggingConfig *lctypes.LoggingConfig) (kitlog.Logger, kitlog.Logger, error) { - infoOnlyLogger, err := config.BuildLoggerFromSinkConfig(loggingConfig.InfoSink) +func infoTraceLoggersLoggingConfig(loggingConfig *config.LoggingConfig) (kitlog.Logger, kitlog.Logger, error) { + infoOnlyLogger, _, err := loggingConfig.InfoSink.BuildLogger() if err != nil { return nil, nil, err } - infoAndTraceLogger, err := config.BuildLoggerFromSinkConfig(loggingConfig.InfoAndTraceSink) + infoAndTraceLogger, _, err := loggingConfig.InfoAndTraceSink.BuildLogger() if err != nil { return nil, nil, err } diff --git a/logging/loggers/capture_logger.go b/logging/loggers/capture_logger.go new file mode 100644 index 00000000..e95be661 --- /dev/null +++ b/logging/loggers/capture_logger.go @@ -0,0 +1,65 @@ +package loggers + +import ( + "github.com/eapache/channels" + kitlog "github.com/go-kit/kit/log" + "sync" +) + +type CaptureLogger struct { + bufferLogger *ChannelLogger + outputLogger kitlog.Logger + passthrough bool + sync.RWMutex +} + +var _ kitlog.Logger = (*CaptureLogger)(nil) + +// Capture logger captures output set to it into a buffer logger and retains +// a reference to an output logger (the logger whose input it is capturing). +// It can optionally passthrough logs to the output logger. +// Because it holds a refereence to its output it can also be used to coordinate +// Flushing of the buffer to the output logger only in exceptional circumstances +func NewCaptureLogger(outputLogger kitlog.Logger, bufferCap channels.BufferCap, + passthrough bool) *CaptureLogger { + return &CaptureLogger{ + bufferLogger: NewChannelLogger(bufferCap), + outputLogger: outputLogger, + passthrough: passthrough, + } +} + +func (cl *CaptureLogger) Log(keyvals ...interface{}) error { + err := cl.bufferLogger.Log(keyvals...) + if cl.passthrough { + err = cl.outputLogger.Log(keyvals...) + } + return err +} + +func (cl *CaptureLogger) SetPassthrough(passthrough bool) { + cl.RWMutex.Lock() + cl.passthrough = passthrough + cl.RWMutex.Unlock() +} + +func (cl *CaptureLogger) Passthrough() bool { + cl.RWMutex.RLock() + passthrough := cl.passthrough + cl.RWMutex.RUnlock() + return passthrough +} + +// Flushes every log line available in the buffer at the time of calling +// to the OutputLogger and returns. Does not block indefinitely. +func (cl *CaptureLogger) Flush() { + cl.bufferLogger.Flush(cl.outputLogger) +} + +func (cl *CaptureLogger) OutputLogger() kitlog.Logger { + return cl.outputLogger +} + +func (cl *CaptureLogger) BufferLogger() *ChannelLogger { + return cl.bufferLogger +} diff --git a/logging/loggers/capture_logger_test.go b/logging/loggers/capture_logger_test.go new file mode 100644 index 00000000..26741acc --- /dev/null +++ b/logging/loggers/capture_logger_test.go @@ -0,0 +1,45 @@ +package loggers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlushCaptureLogger(t *testing.T) { + outputLogger := newTestLogger() + cl := NewCaptureLogger(outputLogger, 100, false) + buffered := 50 + for i := 0; i < buffered; i++ { + cl.Log("Foo", "Bar", "Index", i) + } + assert.True(t, outputLogger.empty()) + + // Flush the ones we bufferred + cl.Flush() + ll := outputLogger.logLines() + assert.Equal(t, buffered, len(ll)) +} + +func TestTeeCaptureLogger(t *testing.T) { + outputLogger := newTestLogger() + cl := NewCaptureLogger(outputLogger, 100, true) + buffered := 50 + for i := 0; i < buffered; i++ { + cl.Log("Foo", "Bar", "Index", i) + } + // Check passthrough to output + ll := outputLogger.logLines() + assert.Equal(t, buffered, len(ll)) + assert.Equal(t, ll, cl.BufferLogger().FlushLogLines()) + + cl.SetPassthrough(false) + buffered = 110 + for i := 0; i < buffered; i++ { + cl.Log("Foo", "Bar", "Index", i) + } + assert.True(t, outputLogger.empty()) + + cl.Flush() + assert.Equal(t, 100, len(outputLogger.logLines())) +} \ No newline at end of file diff --git a/logging/loggers/channel_logger.go b/logging/loggers/channel_logger.go index 5b6405d5..0e978020 100644 --- a/logging/loggers/channel_logger.go +++ b/logging/loggers/channel_logger.go @@ -15,6 +15,8 @@ package loggers import ( + "sync" + "github.com/eapache/channels" kitlog "github.com/go-kit/kit/log" ) @@ -25,15 +27,15 @@ const ( type ChannelLogger struct { ch channels.Channel + sync.RWMutex } 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. +// Creates a Logger that uses a uses a non-blocking ring buffered channel. +// This logger provides a common abstraction for both a buffered, flushable +// logging cache. And a non-blocking conduit to transmit logs via +// DrainForever (or NonBlockingLogger). func NewChannelLogger(loggingRingBufferCap channels.BufferCap) *ChannelLogger { return &ChannelLogger{ ch: channels.NewRingChannel(loggingRingBufferCap), @@ -41,47 +43,118 @@ func NewChannelLogger(loggingRingBufferCap channels.BufferCap) *ChannelLogger { } func (cl *ChannelLogger) Log(keyvals ...interface{}) error { + // In case channel is being reset + cl.RWMutex.RLock() cl.ch.In() <- keyvals + cl.RWMutex.RUnlock() // 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 } +// Get the current occupancy level of the ring buffer +func (cl *ChannelLogger) BufferLength() int { + return cl.ch.Len() +} + +// Get the cap off the internal ring buffer +func (cl *ChannelLogger) BufferCap() channels.BufferCap { + return cl.ch.Cap() +} + // 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{}) + logLine, ok := <-cl.ch.Out() + return readLogLine(logLine, ok) } // 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{}) + case logLine, ok := <-cl.ch.Out(): + return readLogLine(logLine, ok) default: return nil } } +func readLogLine(logLine interface{}, ok bool) []interface{} { + if !ok { + // Channel closed + return nil + } + // 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 logLine.([]interface{}) +} + // 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()...) +func (cl *ChannelLogger) DrainForever(logger kitlog.Logger) { + logLine := cl.WaitReadLogLine() + // logLine could be nil if channel was closed while waiting for next line + if logLine != nil { + logger.Log(logLine...) + } +} + +// Drains everything that is available at the time of calling +func (cl *ChannelLogger) Flush(logger kitlog.Logger) { + bufferSize := cl.ch.Len() + for i := 0; i < bufferSize; i++ { + logLine := cl.WaitReadLogLine() + if logLine != nil { + logger.Log(logLine...) + } } } +// Drains the next contiguous segment of loglines up to the buffer cap waiting +// for at least one line +func (cl *ChannelLogger) FlushLogLines() [][]interface{} { + logLines := make([][]interface{}, 0, cl.ch.Len()) + cl.Flush(kitlog.LoggerFunc(func(keyvals... interface{}) error { + logLines = append(logLines, keyvals) + return nil + })) + return logLines +} + + +// Close the existing channel halting goroutines that are draining the channel +// and create a new channel to buffer into. Should not cause any log lines +// arriving concurrently to be lost, but any that have not been drained from +// old channel may be. +func (cl *ChannelLogger) Reset() { + cl.RWMutex.Lock() + cl.ch.Close() + cl.ch = channels.NewRingChannel(cl.ch.Cap()) + cl.RWMutex.Unlock() +} + +func (cl *ChannelLogger) WaitLogLines() [][]interface{} { + logLines := make([][]interface{}, 0, cl.ch.Len()) + // Wait for first line + logLines = append(logLines,cl.WaitReadLogLine()) + cl.Flush(kitlog.LoggerFunc(func(keyvals... interface{}) error { + logLines = append(logLines, keyvals) + return nil + })) + return logLines +} + // 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(DefaultLoggingRingBufferCap) - go cl.DrainChannelToLogger(logger) + go cl.DrainForever(logger) return cl } + +func lessThanCap(i int, cap channels.BufferCap) bool { + return cap == channels.Infinity || i < int(cap) +} diff --git a/logging/loggers/channel_logger_test.go b/logging/loggers/channel_logger_test.go index 42cfd4ac..b54f906a 100644 --- a/logging/loggers/channel_logger_test.go +++ b/logging/loggers/channel_logger_test.go @@ -39,3 +39,21 @@ func TestChannelLogger(t *testing.T) { assert.Nil(t, cl.ReadLogLine(), "Since we have drained the buffer there "+ "should be no more log lines.") } + +func TestChannelLogger_Reset(t *testing.T) { + loggingRingBufferCap := channels.BufferCap(5) + cl := NewChannelLogger(loggingRingBufferCap) + for i := 0; i < int(loggingRingBufferCap); i++ { + cl.Log("log line", i) + } + cl.Reset() + for i := 0; i < int(loggingRingBufferCap); i++ { + cl.Log("log line", i) + } + for i := 0; i < int(loggingRingBufferCap); i++ { + ll := cl.WaitReadLogLine() + assert.Equal(t, i, ll[1]) + } + assert.Nil(t, cl.ReadLogLine(), "Since we have drained the buffer there "+ + "should be no more log lines.") +} \ No newline at end of file diff --git a/logging/loggers/eris_format_logger.go b/logging/loggers/eris_format_logger.go index 651396dd..c980b31f 100644 --- a/logging/loggers/eris_format_logger.go +++ b/logging/loggers/eris_format_logger.go @@ -37,6 +37,10 @@ func (efl *erisFormatLogger) Log(keyvals ...interface{}) error { if efl.logger == nil { return nil } + if len(keyvals) % 2 != 0 { + return fmt.Errorf("Log line contains an odd number of elements so " + + "was dropped: %v", keyvals) + } return efl.logger.Log(structure.MapKeyValues(keyvals, erisFormatKeyValueMapper)...) } diff --git a/logging/loggers/filter_logger_test.go b/logging/loggers/filter_logger_test.go index dd900dba..79b837c3 100644 --- a/logging/loggers/filter_logger_test.go +++ b/logging/loggers/filter_logger_test.go @@ -7,11 +7,11 @@ import ( ) func TestFilterLogger(t *testing.T) { - testLogger := newTestLogger() + testLogger := NewChannelLogger(100) filterLogger := NewFilterLogger(testLogger, func(keyvals []interface{}) bool { return len(keyvals) > 0 && keyvals[0] == "Spoon" }) filterLogger.Log("Fish", "Present") filterLogger.Log("Spoon", "Present") - assert.Equal(t, [][]interface{}{Slice("Fish", "Present")}, testLogger.logLines) + assert.Equal(t, [][]interface{}{Slice("Fish", "Present")}, testLogger.FlushLogLines()) } diff --git a/logging/loggers/info_trace_logger.go b/logging/loggers/info_trace_logger.go index 58596f30..1cb81fc9 100644 --- a/logging/loggers/info_trace_logger.go +++ b/logging/loggers/info_trace_logger.go @@ -16,8 +16,8 @@ package loggers import ( "github.com/eris-ltd/eris-db/logging/structure" - kitlog "github.com/go-kit/kit/log" "github.com/eris-ltd/eris-db/logging/types" + kitlog "github.com/go-kit/kit/log" ) type infoTraceLogger struct { @@ -36,17 +36,17 @@ var _ kitlog.Logger = (types.InfoTraceLogger)(nil) func NewInfoTraceLogger(infoOnlyLogger, infoAndTraceLogger kitlog.Logger) types.InfoTraceLogger { // 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. - var infoOnlyOutputLogger, infoTraceOutputLogger kitlog.SwapLogger + var infoOnlyOutputLogger, infoAndTraceOutputLogger kitlog.SwapLogger infoOnlyOutputLogger.Swap(infoOnlyLogger) - infoTraceOutputLogger.Swap(infoAndTraceLogger) - return &infoTraceLogger { - infoOnlyOutputLogger: &infoOnlyOutputLogger, - infoAndTraceOutputLogger: &infoTraceOutputLogger, - infoOnly: wrapLogger(&infoOnlyOutputLogger).With( + infoAndTraceOutputLogger.Swap(infoAndTraceLogger) + return &infoTraceLogger{ + infoOnlyOutputLogger: &infoOnlyOutputLogger, + infoAndTraceOutputLogger: &infoAndTraceOutputLogger, + infoOnly: wrapOutputLogger(&infoOnlyOutputLogger).With( structure.ChannelKey, types.InfoChannelName, structure.LevelKey, types.InfoLevelName, ), - infoAndTrace: wrapLogger(&infoTraceOutputLogger).With( + infoAndTrace: wrapOutputLogger(&infoAndTraceOutputLogger).With( structure.ChannelKey, types.TraceChannelName, structure.LevelKey, types.TraceLevelName, ), @@ -59,14 +59,14 @@ func NewNoopInfoTraceLogger() types.InfoTraceLogger { func (l *infoTraceLogger) With(keyvals ...interface{}) types.InfoTraceLogger { return &infoTraceLogger{ - infoOnly: l.infoOnly.With(keyvals...), + infoOnly: l.infoOnly.With(keyvals...), infoAndTrace: l.infoAndTrace.With(keyvals...), } } func (l *infoTraceLogger) WithPrefix(keyvals ...interface{}) types.InfoTraceLogger { return &infoTraceLogger{ - infoOnly: l.infoOnly.WithPrefix(keyvals...), + infoOnly: l.infoOnly.WithPrefix(keyvals...), infoAndTrace: l.infoAndTrace.WithPrefix(keyvals...), } } @@ -102,6 +102,9 @@ func (l *infoTraceLogger) Log(keyvals ...interface{}) error { return nil } -func wrapLogger(logger kitlog.Logger) *kitlog.Context { - return kitlog.NewContext(NonBlockingLogger(VectorValuedLogger(logger))) +// Wrap the output loggers with a a set of standard transforms, a non-blocking +// ChannelLogger and an outer context +func wrapOutputLogger(outputLogger kitlog.Logger) *kitlog.Context { + return kitlog.NewContext(NonBlockingLogger(ErisFormatLogger( + VectorValuedLogger(outputLogger)))) } diff --git a/logging/loggers/logging_test.go b/logging/loggers/logging_test.go deleted file mode 100644 index 103d8ddf..00000000 --- a/logging/loggers/logging_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2017 Monax Industries Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -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 deleted file mode 100644 index 2e2e344f..00000000 --- a/logging/loggers/multiple_channel_logger.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2017 Monax Industries Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -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 deleted file mode 100644 index a1404204..00000000 --- a/logging/loggers/multiple_channel_logger_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2017 Monax Industries Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -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_test.go b/logging/loggers/multiple_output_logger_test.go index 4f606376..d282a90d 100644 --- a/logging/loggers/multiple_output_logger_test.go +++ b/logging/loggers/multiple_output_logger_test.go @@ -26,7 +26,7 @@ func TestNewMultipleOutputLogger(t *testing.T) { logLine := []interface{}{"msg", "hello"} err := mol.Log(logLine...) expected := [][]interface{}{logLine} - assert.Equal(t, expected, a.logLines) - assert.Equal(t, expected, b.logLines) + assert.Equal(t, expected, a.logLines()) + assert.Equal(t, expected, b.logLines()) assert.IsType(t, multipleErrors{}, err) } diff --git a/logging/loggers/shared_test.go b/logging/loggers/shared_test.go new file mode 100644 index 00000000..0ea6ae40 --- /dev/null +++ b/logging/loggers/shared_test.go @@ -0,0 +1,35 @@ +package loggers + +import "errors" + +type testLogger struct { + cl *ChannelLogger + err error +} + +func (el *testLogger) empty() bool { + return el.cl.BufferLength() == 0 +} + +func (el *testLogger) logLines() [][]interface{} { + return el.cl.WaitLogLines() +} + +func (el *testLogger) Log(keyvals ...interface{}) error { + el.cl.Log(keyvals...) + return el.err +} + +func newErrorLogger(errMessage string) *testLogger { + return &testLogger{ + cl: NewChannelLogger(100), + err: errors.New(errMessage), + } +} + +func newTestLogger() *testLogger { + return &testLogger{ + cl: NewChannelLogger(100), + err: nil, + } +} diff --git a/logging/loggers/vector_valued_logger_test.go b/logging/loggers/vector_valued_logger_test.go index 157f5bc0..33a0c5d8 100644 --- a/logging/loggers/vector_valued_logger_test.go +++ b/logging/loggers/vector_valued_logger_test.go @@ -27,5 +27,5 @@ func TestVectorValuedLogger(t *testing.T) { vvl.Log("foo", "bar", "seen", 1, "seen", 3, "seen", 2) assert.Equal(t, Slice("foo", "bar", "seen", Slice(1, 3, 2)), - logger.logLines[0]) + logger.logLines()[0]) } -- GitLab