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