Merge pull request #62 from mohae/setloglevel
added support for setting log level using strings: SetLogLevel()
diff --git a/README.md b/README.md
index 44df9ee..46dd102 100644
--- a/README.md
+++ b/README.md
@@ -218,7 +218,7 @@
Send errors to an exception tracking service compatible with the Airbrake API.
Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes.
-* [`github.com/Sirupsen/logrus/hooks/papertrail`](https://github.com/Sirupsen/logrus/blob/master/hooks/airbrake/airbrake.go)
+* [`github.com/Sirupsen/logrus/hooks/papertrail`](https://github.com/Sirupsen/logrus/blob/master/hooks/papertrail/papertrail.go)
Send errors to the Papertrail hosted logging service via UDP.
* [`github.com/Sirupsen/logrus/hooks/syslog`](https://github.com/Sirupsen/logrus/blob/master/hooks/syslog/syslog.go)
diff --git a/entry.go b/entry.go
index 58eb263..dc2b0a7 100644
--- a/entry.go
+++ b/entry.go
@@ -70,18 +70,18 @@
return &Entry{Logger: entry.Logger, Data: data}
}
-func (entry *Entry) log(level Level, msg string) string {
+func (entry *Entry) log(level Level, msg string) {
entry.Time = time.Now()
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, entry); err != nil {
- fmt.Fprintf(os.Stderr, "Failed to fire hook", err)
+ fmt.Fprintf(os.Stderr, "Failed to fire hook\n", err)
}
reader, err := entry.Reader()
if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err)
+ fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
}
entry.Logger.mu.Lock()
@@ -89,10 +89,15 @@
_, err = io.Copy(entry.Logger.Out, reader)
if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to write to log, %v", err)
+ fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
- return reader.String()
+ // To avoid Entry#log() returning a value that only would make sense for
+ // panic() to use in Entry#Panic(), we avoid the allocation by checking
+ // directly here.
+ if level <= PanicLevel {
+ panic(reader.String())
+ }
}
func (entry *Entry) Debug(args ...interface{}) {
@@ -132,8 +137,7 @@
func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
- msg := entry.log(PanicLevel, fmt.Sprint(args...))
- panic(msg)
+ entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...))
}
diff --git a/formatter.go b/formatter.go
index fc0ebd7..cccf1c2 100644
--- a/formatter.go
+++ b/formatter.go
@@ -1,9 +1,5 @@
package logrus
-import (
- "time"
-)
-
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
@@ -36,19 +32,13 @@
entry.Data["fields.time"] = entry.Data["time"]
}
- entry.Data["time"] = entry.Time.Format(time.RFC3339)
-
_, ok = entry.Data["msg"]
if ok {
entry.Data["fields.msg"] = entry.Data["msg"]
}
- entry.Data["msg"] = entry.Message
-
_, ok = entry.Data["level"]
if ok {
entry.Data["fields.level"] = entry.Data["level"]
}
-
- entry.Data["level"] = entry.Level.String()
}
diff --git a/formatter_bench_test.go b/formatter_bench_test.go
new file mode 100644
index 0000000..77989da
--- /dev/null
+++ b/formatter_bench_test.go
@@ -0,0 +1,88 @@
+package logrus
+
+import (
+ "testing"
+ "time"
+)
+
+// smallFields is a small size data set for benchmarking
+var smallFields = Fields{
+ "foo": "bar",
+ "baz": "qux",
+ "one": "two",
+ "three": "four",
+}
+
+// largeFields is a large size data set for benchmarking
+var largeFields = Fields{
+ "foo": "bar",
+ "baz": "qux",
+ "one": "two",
+ "three": "four",
+ "five": "six",
+ "seven": "eight",
+ "nine": "ten",
+ "eleven": "twelve",
+ "thirteen": "fourteen",
+ "fifteen": "sixteen",
+ "seventeen": "eighteen",
+ "nineteen": "twenty",
+ "a": "b",
+ "c": "d",
+ "e": "f",
+ "g": "h",
+ "i": "j",
+ "k": "l",
+ "m": "n",
+ "o": "p",
+ "q": "r",
+ "s": "t",
+ "u": "v",
+ "w": "x",
+ "y": "z",
+ "this": "will",
+ "make": "thirty",
+ "entries": "yeah",
+}
+
+func BenchmarkSmallTextFormatter(b *testing.B) {
+ doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields)
+}
+
+func BenchmarkLargeTextFormatter(b *testing.B) {
+ doBenchmark(b, &TextFormatter{DisableColors: true}, largeFields)
+}
+
+func BenchmarkSmallColoredTextFormatter(b *testing.B) {
+ doBenchmark(b, &TextFormatter{ForceColors: true}, smallFields)
+}
+
+func BenchmarkLargeColoredTextFormatter(b *testing.B) {
+ doBenchmark(b, &TextFormatter{ForceColors: true}, largeFields)
+}
+
+func BenchmarkSmallJSONFormatter(b *testing.B) {
+ doBenchmark(b, &JSONFormatter{}, smallFields)
+}
+
+func BenchmarkLargeJSONFormatter(b *testing.B) {
+ doBenchmark(b, &JSONFormatter{}, largeFields)
+}
+
+func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
+ entry := &Entry{
+ Time: time.Time{},
+ Level: InfoLevel,
+ Message: "message",
+ Data: fields,
+ }
+ var d []byte
+ var err error
+ for i := 0; i < b.N; i++ {
+ d, err = formatter.Format(entry)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(int64(len(d)))
+ }
+}
diff --git a/json_formatter.go b/json_formatter.go
index c0e2d18..9d11b64 100644
--- a/json_formatter.go
+++ b/json_formatter.go
@@ -3,13 +3,16 @@
import (
"encoding/json"
"fmt"
+ "time"
)
-type JSONFormatter struct {
-}
+type JSONFormatter struct{}
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
prefixFieldClashes(entry)
+ entry.Data["time"] = entry.Time.Format(time.RFC3339)
+ entry.Data["msg"] = entry.Message
+ entry.Data["level"] = entry.Level.String()
serialized, err := json.Marshal(entry.Data)
if err != nil {
diff --git a/logrus_test.go b/logrus_test.go
index e476838..bf5dd6e 100644
--- a/logrus_test.go
+++ b/logrus_test.go
@@ -3,6 +3,8 @@
import (
"bytes"
"encoding/json"
+ "strconv"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -24,6 +26,31 @@
assertions(fields)
}
+func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) {
+ var buffer bytes.Buffer
+
+ logger := New()
+ logger.Out = &buffer
+ logger.Formatter = &TextFormatter{
+ DisableColors: true,
+ }
+
+ log(logger)
+
+ fields := make(map[string]string)
+ for _, kv := range strings.Split(buffer.String(), " ") {
+ if !strings.Contains(kv, "=") {
+ continue
+ }
+ kvArr := strings.Split(kv, "=")
+ key := strings.TrimSpace(kvArr[0])
+ val, err := strconv.Unquote(kvArr[1])
+ assert.NoError(t, err)
+ fields[key] = val
+ }
+ assertions(fields)
+}
+
func TestPrint(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Print("test")
@@ -163,6 +190,20 @@
})
}
+func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
+ LogAndAssertText(t, func(log *Logger) {
+ ll := log.WithField("herp", "derp")
+ ll.Info("hello")
+ ll.Info("bye")
+ }, func(fields map[string]string) {
+ for _, fieldName := range []string{"fields.level", "fields.time", "fields.msg"} {
+ if _, ok := fields[fieldName]; ok {
+ t.Fatalf("should not have prefixed %q: %v", fieldName, fields)
+ }
+ }
+ })
+}
+
func TestConvertLevelToString(t *testing.T) {
assert.Equal(t, "debug", DebugLevel.String())
assert.Equal(t, "info", InfoLevel.String())
diff --git a/text_formatter.go b/text_formatter.go
index 0f38c45..2ab0139 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -38,45 +38,26 @@
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
+ var keys []string
+ for k := range entry.Data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
b := &bytes.Buffer{}
prefixFieldClashes(entry)
- if (f.ForceColors || isTerminal) && !f.DisableColors {
- levelText := strings.ToUpper(entry.Data["level"].(string))[0:4]
+ isColored := (f.ForceColors || isTerminal) && !f.DisableColors
- levelColor := blue
-
- if entry.Data["level"] == "warning" {
- levelColor = yellow
- } else if entry.Data["level"] == "error" ||
- entry.Data["level"] == "fatal" ||
- entry.Data["level"] == "panic" {
- levelColor = red
- }
-
- fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Data["msg"])
-
- var keys []string
- for k := range entry.Data {
- if k != "level" && k != "time" && k != "msg" {
- keys = append(keys, k)
- }
- }
- sort.Strings(keys)
- for _, k := range keys {
- v := entry.Data[k]
- fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
- }
+ if isColored {
+ printColored(b, entry, keys)
} else {
- f.AppendKeyValue(b, "time", entry.Data["time"].(string))
- f.AppendKeyValue(b, "level", entry.Data["level"].(string))
- f.AppendKeyValue(b, "msg", entry.Data["msg"].(string))
-
- for key, value := range entry.Data {
- if key != "time" && key != "level" && key != "msg" {
- f.AppendKeyValue(b, key, value)
- }
+ f.AppendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
+ f.AppendKeyValue(b, "level", entry.Level.String())
+ f.AppendKeyValue(b, "msg", entry.Message)
+ for _, key := range keys {
+ f.AppendKeyValue(b, key, entry.Data[key])
}
}
@@ -84,6 +65,26 @@
return b.Bytes(), nil
}
+func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
+ var levelColor int
+ switch entry.Level {
+ case WarnLevel:
+ levelColor = yellow
+ case ErrorLevel, FatalLevel, PanicLevel:
+ levelColor = red
+ default:
+ levelColor = blue
+ }
+
+ levelText := strings.ToUpper(entry.Level.String())[0:4]
+
+ fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
+ for _, k := range keys {
+ v := entry.Data[k]
+ fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
+ }
+}
+
func (f *TextFormatter) AppendKeyValue(b *bytes.Buffer, key, value interface{}) {
if _, ok := value.(string); ok {
fmt.Fprintf(b, "%v=%q ", key, value)