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