Add log counters The use case in Hugo is to exit with an error code when *any* ERROR logging has occured. See https://github.com/spf13/hugo/issues/740
diff --git a/jww_test.go b/jww_test.go index b6d118a..c405185 100644 --- a/jww_test.go +++ b/jww_test.go
@@ -1,4 +1,4 @@ -// Copyright © 2014 Steve Francia <spf@spf13.com>. +// Copyright © 2016 Steve Francia <spf@spf13.com>. // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -6,51 +6,91 @@ package jwalterweatherman import ( - "bytes" - "github.com/stretchr/testify/assert" - "testing" + "bytes" + "github.com/stretchr/testify/assert" + "sync" + "testing" ) func TestLevels(t *testing.T) { - SetStdoutThreshold(LevelError) - assert.Equal(t, StdoutThreshold(), LevelError) - SetLogThreshold(LevelCritical) - assert.Equal(t, LogThreshold(), LevelCritical) - assert.NotEqual(t, StdoutThreshold(), LevelCritical) - SetStdoutThreshold(LevelWarn) - assert.Equal(t, StdoutThreshold(), LevelWarn) + SetStdoutThreshold(LevelError) + assert.Equal(t, StdoutThreshold(), LevelError) + SetLogThreshold(LevelCritical) + assert.Equal(t, LogThreshold(), LevelCritical) + assert.NotEqual(t, StdoutThreshold(), LevelCritical) + SetStdoutThreshold(LevelWarn) + assert.Equal(t, StdoutThreshold(), LevelWarn) } func TestDefaultLogging(t *testing.T) { - outputBuf := new(bytes.Buffer) - logBuf := new(bytes.Buffer) - LogHandle = logBuf - OutHandle = outputBuf + outputBuf := new(bytes.Buffer) + logBuf := new(bytes.Buffer) + LogHandle = logBuf + OutHandle = outputBuf - SetLogThreshold(LevelWarn) - SetStdoutThreshold(LevelError) + SetLogThreshold(LevelWarn) + SetStdoutThreshold(LevelError) - FATAL.Println("fatal err") - CRITICAL.Println("critical err") - ERROR.Println("an error") - WARN.Println("a warning") - INFO.Println("information") - DEBUG.Println("debugging info") - TRACE.Println("trace") + FATAL.Println("fatal err") + CRITICAL.Println("critical err") + ERROR.Println("an error") + WARN.Println("a warning") + INFO.Println("information") + DEBUG.Println("debugging info") + TRACE.Println("trace") - assert.Contains(t, logBuf.String(), "fatal err") - assert.Contains(t, logBuf.String(), "critical err") - assert.Contains(t, logBuf.String(), "an error") - assert.Contains(t, logBuf.String(), "a warning") - assert.NotContains(t, logBuf.String(), "information") - assert.NotContains(t, logBuf.String(), "debugging info") - assert.NotContains(t, logBuf.String(), "trace") + assert.Contains(t, logBuf.String(), "fatal err") + assert.Contains(t, logBuf.String(), "critical err") + assert.Contains(t, logBuf.String(), "an error") + assert.Contains(t, logBuf.String(), "a warning") + assert.NotContains(t, logBuf.String(), "information") + assert.NotContains(t, logBuf.String(), "debugging info") + assert.NotContains(t, logBuf.String(), "trace") - assert.Contains(t, outputBuf.String(), "fatal err") - assert.Contains(t, outputBuf.String(), "critical err") - assert.Contains(t, outputBuf.String(), "an error") - assert.NotContains(t, outputBuf.String(), "a warning") - assert.NotContains(t, outputBuf.String(), "information") - assert.NotContains(t, outputBuf.String(), "debugging info") - assert.NotContains(t, outputBuf.String(), "trace") + assert.Contains(t, outputBuf.String(), "fatal err") + assert.Contains(t, outputBuf.String(), "critical err") + assert.Contains(t, outputBuf.String(), "an error") + assert.NotContains(t, outputBuf.String(), "a warning") + assert.NotContains(t, outputBuf.String(), "information") + assert.NotContains(t, outputBuf.String(), "debugging info") + assert.NotContains(t, outputBuf.String(), "trace") +} + +func TestLogCounter(t *testing.T) { + ResetLogCounters() + + FATAL.Println("fatal err") + CRITICAL.Println("critical err") + WARN.Println("a warning") + WARN.Println("another warning") + INFO.Println("information") + DEBUG.Println("debugging info") + TRACE.Println("trace") + + wg := &sync.WaitGroup{} + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 10; j++ { + ERROR.Println("error", j) + // check for data races + assert.True(t, LogCountForLevel(LevelError) > uint64(j)) + assert.True(t, LogCountForLevelsGreaterThanorEqualTo(LevelError) > uint64(j)) + } + }() + + } + + wg.Wait() + + assert.Equal(t, uint64(1), LogCountForLevel(LevelFatal)) + assert.Equal(t, uint64(1), LogCountForLevel(LevelCritical)) + assert.Equal(t, uint64(2), LogCountForLevel(LevelWarn)) + assert.Equal(t, uint64(1), LogCountForLevel(LevelInfo)) + assert.Equal(t, uint64(1), LogCountForLevel(LevelDebug)) + assert.Equal(t, uint64(1), LogCountForLevel(LevelTrace)) + assert.Equal(t, uint64(100), LogCountForLevel(LevelError)) + assert.Equal(t, uint64(102), LogCountForLevelsGreaterThanorEqualTo(LevelError)) }
diff --git a/thatswhyyoualwaysleaveanote.go b/thatswhyyoualwaysleaveanote.go index 30f6d54..df5970c 100644 --- a/thatswhyyoualwaysleaveanote.go +++ b/thatswhyyoualwaysleaveanote.go
@@ -1,4 +1,4 @@ -// Copyright © 2014 Steve Francia <spf@spf13.com>. +// Copyright © 2016 Steve Francia <spf@spf13.com>. // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -11,6 +11,7 @@ "io/ioutil" "log" "os" + "sync/atomic" ) // Level describes the chosen log level between @@ -18,10 +19,33 @@ type Level int type NotePad struct { - Handle io.Writer - Level Level - Prefix string - Logger **log.Logger + Handle io.Writer + Level Level + Prefix string + Logger **log.Logger + counter uint64 +} + +func (n *NotePad) incr() { + atomic.AddUint64(&n.counter, 1) +} + +func (n *NotePad) resetCounter() { + atomic.StoreUint64(&n.counter, 0) +} + +func (n *NotePad) getCount() uint64 { + return atomic.LoadUint64(&n.counter) +} + +type countingWriter struct { + incrFunc func() +} + +func (cw *countingWriter) Write(p []byte) (n int, err error) { + cw.incrFunc() + + return 0, nil } // Feedback is special. It writes plainly to the output while @@ -101,6 +125,7 @@ } for _, n := range NotePads { + n.Handle = io.MultiWriter(n.Handle, &countingWriter{n.incr}) *n.Logger = log.New(n.Handle, n.Prefix, logFlags) } @@ -176,6 +201,35 @@ initialize() } +// LogCountForLevel returns the number of log invocations for a given level. +func LogCountForLevel(l Level) uint64 { + for _, np := range NotePads { + if np.Level == l { + return np.getCount() + } + } + return 0 +} + +// LogCountForLevelsGreaterThanorEqualTo returns the number of log invocations +// greater than or equal to a given level threshold. +func LogCountForLevelsGreaterThanorEqualTo(threshold Level) uint64 { + var cnt uint64 + for _, np := range NotePads { + if np.Level >= threshold { + cnt += np.getCount() + } + } + return cnt +} + +// ResetLogCounters resets the invocation counters for all levels. +func ResetLogCounters() { + for _, np := range NotePads { + np.resetCounter() + } +} + // Disables logging for the entire JWW system func DiscardLogging() { LogHandle = ioutil.Discard