├── screenshot.png ├── internal └── term │ ├── term_openbsd.go │ ├── term_freebsd.go │ ├── term_darwin.go │ ├── term_linux.go │ ├── term.go │ ├── term_appengine.go │ ├── term_notwindows.go │ ├── term_windows.go │ └── LICENSE ├── .travis.yml ├── std_example_test.go ├── xlog_bench_test.go ├── nop_test.go ├── xlog_examples_test.go ├── LICENSE ├── nop.go ├── util.go ├── output_syslog.go ├── util_test.go ├── handler_examples_test.go ├── levels_test.go ├── levels.go ├── output_examples_test.go ├── std.go ├── std_test.go ├── handler_test.go ├── handler_pre17_test.go ├── handler_pre17.go ├── handler.go ├── README.md ├── xlog_test.go ├── output.go ├── output_test.go └── xlog.go /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rs/xlog/HEAD/screenshot.png -------------------------------------------------------------------------------- /internal/term/term_openbsd.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import "syscall" 4 | 5 | const ioctlReadTermios = syscall.TIOCGETA 6 | -------------------------------------------------------------------------------- /internal/term/term_freebsd.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | const ioctlReadTermios = syscall.TIOCGETA 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - 1.8 5 | - tip 6 | matrix: 7 | allow_failures: 8 | - go: tip 9 | script: 10 | go test -v -race -cpu=1,2,4 ./... 11 | -------------------------------------------------------------------------------- /std_example_test.go: -------------------------------------------------------------------------------- 1 | package xlog_test 2 | 3 | import "github.com/rs/xlog" 4 | 5 | func ExampleSetLogger() { 6 | xlog.SetLogger(xlog.New(xlog.Config{ 7 | Level: xlog.LevelInfo, 8 | Output: xlog.NewConsoleOutput(), 9 | Fields: xlog.F{ 10 | "role": "my-service", 11 | }, 12 | })) 13 | } 14 | -------------------------------------------------------------------------------- /xlog_bench_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import "testing" 4 | 5 | func BenchmarkSend(b *testing.B) { 6 | l := New(Config{Output: Discard, Fields: F{"a": "b"}}).(*logger) 7 | b.ResetTimer() 8 | b.ReportAllocs() 9 | for i := 0; i < b.N; i++ { 10 | l.send(0, 0, "test", F{"foo": "bar", "bar": "baz"}) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/term/term_darwin.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2013 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package term 7 | 8 | import "syscall" 9 | 10 | const ioctlReadTermios = syscall.TIOCGETA 11 | -------------------------------------------------------------------------------- /internal/term/term_linux.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2013 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // +build !appengine 7 | 8 | package term 9 | 10 | import "syscall" 11 | 12 | const ioctlReadTermios = syscall.TCGETS 13 | -------------------------------------------------------------------------------- /internal/term/term.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2011 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // +build linux,!appengine darwin freebsd openbsd 7 | 8 | package term 9 | 10 | type fder interface { 11 | Fd() uintptr 12 | } 13 | -------------------------------------------------------------------------------- /internal/term/term_appengine.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2013 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // +build appengine 7 | 8 | package term 9 | 10 | import "io" 11 | 12 | // IsTerminal always returns false on AppEngine. 13 | func IsTerminal(w io.Writer) bool { 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /nop_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import "testing" 4 | 5 | func TestNopLogger(t *testing.T) { 6 | // cheap cover score upper 7 | NopLogger.SetField("name", "value") 8 | NopLogger.OutputF(LevelInfo, 0, "", nil) 9 | NopLogger.Debug() 10 | NopLogger.Debugf("format") 11 | NopLogger.Info() 12 | NopLogger.Infof("format") 13 | NopLogger.Warn() 14 | NopLogger.Warnf("format") 15 | NopLogger.Error() 16 | NopLogger.Errorf("format") 17 | exit1 = func() {} 18 | NopLogger.Fatal() 19 | NopLogger.Fatalf("format") 20 | NopLogger.Write([]byte{}) 21 | NopLogger.Output(0, "") 22 | } 23 | -------------------------------------------------------------------------------- /internal/term/term_notwindows.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2011 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // +build linux,!appengine darwin freebsd openbsd 7 | 8 | package term 9 | 10 | import ( 11 | "io" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // IsTerminal returns true if w writes to a terminal. 17 | func IsTerminal(w io.Writer) bool { 18 | fw, ok := w.(fder) 19 | if !ok { 20 | return false 21 | } 22 | var termios syscall.Termios 23 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fw.Fd(), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 24 | return err == 0 25 | } 26 | -------------------------------------------------------------------------------- /internal/term/term_windows.go: -------------------------------------------------------------------------------- 1 | // Based on ssh/terminal: 2 | // Copyright 2011 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // +build windows 7 | 8 | package term 9 | 10 | import ( 11 | "io" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 17 | 18 | var ( 19 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") 20 | ) 21 | 22 | // IsTerminal returns true if w writes to a terminal. 23 | func IsTerminal(w io.Writer) bool { 24 | fw, ok := w.(interface { 25 | Fd() uintptr 26 | }) 27 | if !ok { 28 | return false 29 | } 30 | var st uint32 31 | r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fw.Fd(), uintptr(unsafe.Pointer(&st)), 0) 32 | return r != 0 && e == 0 33 | } 34 | -------------------------------------------------------------------------------- /xlog_examples_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xlog_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "log" 9 | 10 | "github.com/rs/xlog" 11 | ) 12 | 13 | func Example_log() { 14 | ctx := context.TODO() 15 | l := xlog.FromContext(ctx) 16 | 17 | // Log a simple message 18 | l.Debug("message") 19 | 20 | if err := errors.New("some error"); err != nil { 21 | l.Errorf("Some error happened: %v", err) 22 | } 23 | 24 | // With optional fields 25 | l.Debugf("foo %s", "bar", xlog.F{ 26 | "field": "value", 27 | }) 28 | } 29 | 30 | func Example_stdlog() { 31 | // Define logger conf 32 | conf := xlog.Config{ 33 | Output: xlog.NewConsoleOutput(), 34 | } 35 | 36 | // Remove timestamp and other decorations of the std logger 37 | log.SetFlags(0) 38 | 39 | // Plug a xlog instance to Go's std logger 40 | log.SetOutput(xlog.New(conf)) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /nop.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | type nop struct{} 4 | 5 | // NopLogger is an no-op implementation of xlog.Logger 6 | var NopLogger = &nop{} 7 | 8 | func (n nop) SetField(name string, value interface{}) {} 9 | 10 | func (n nop) GetFields() F { return map[string]interface{}{} } 11 | 12 | func (n nop) OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) {} 13 | 14 | func (n nop) Debug(v ...interface{}) {} 15 | 16 | func (n nop) Debugf(format string, v ...interface{}) {} 17 | 18 | func (n nop) Info(v ...interface{}) {} 19 | 20 | func (n nop) Infof(format string, v ...interface{}) {} 21 | 22 | func (n nop) Warn(v ...interface{}) {} 23 | 24 | func (n nop) Warnf(format string, v ...interface{}) {} 25 | 26 | func (n nop) Error(v ...interface{}) {} 27 | 28 | func (n nop) Errorf(format string, v ...interface{}) {} 29 | 30 | func (n nop) Fatal(v ...interface{}) { 31 | exit1() 32 | } 33 | 34 | func (n nop) Fatalf(format string, v ...interface{}) { 35 | exit1() 36 | } 37 | 38 | func (n nop) Write(p []byte) (int, error) { return len(p), nil } 39 | 40 | func (n nop) Output(calldepth int, s string) error { 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/term/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Eskildsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | type color int 11 | 12 | const ( 13 | red color = 31 14 | green color = 32 15 | yellow color = 33 16 | blue color = 34 17 | gray color = 37 18 | ) 19 | 20 | func colorPrint(w io.Writer, s string, c color) { 21 | w.Write([]byte{0x1b, '[', byte('0' + c/10), byte('0' + c%10), 'm'}) 22 | w.Write([]byte(s)) 23 | w.Write([]byte("\x1b[0m")) 24 | } 25 | 26 | func needsQuotedValueRune(r rune) bool { 27 | return r <= ' ' || r == '=' || r == '"' 28 | } 29 | 30 | // writeValue writes a value on the writer in a logfmt compatible way 31 | func writeValue(w io.Writer, v interface{}) (err error) { 32 | switch v := v.(type) { 33 | case nil: 34 | _, err = w.Write([]byte("null")) 35 | case string: 36 | if strings.IndexFunc(v, needsQuotedValueRune) != -1 { 37 | var b []byte 38 | b, err = json.Marshal(v) 39 | if err == nil { 40 | w.Write(b) 41 | } 42 | } else { 43 | _, err = w.Write([]byte(v)) 44 | } 45 | case error: 46 | s := v.Error() 47 | err = writeValue(w, s) 48 | default: 49 | s := fmt.Sprint(v) 50 | err = writeValue(w, s) 51 | } 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /output_syslog.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package xlog 4 | 5 | import ( 6 | "io" 7 | "log/syslog" 8 | ) 9 | 10 | // NewSyslogOutput returns JSONOutputs in a LevelOutput with writers set to syslog 11 | // with the proper priority added to a LOG_USER facility. 12 | // If network and address are empty, Dial will connect to the local syslog server. 13 | func NewSyslogOutput(network, address, tag string) Output { 14 | return NewSyslogOutputFacility(network, address, tag, syslog.LOG_USER) 15 | } 16 | 17 | // NewSyslogOutputFacility returns JSONOutputs in a LevelOutput with writers set to syslog 18 | // with the proper priority added to the passed facility. 19 | // If network and address are empty, Dial will connect to the local syslog server. 20 | func NewSyslogOutputFacility(network, address, tag string, facility syslog.Priority) Output { 21 | o := LevelOutput{ 22 | Debug: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_DEBUG, tag)), 23 | Info: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_INFO, tag)), 24 | Warn: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_WARNING, tag)), 25 | Error: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_ERR, tag)), 26 | } 27 | return o 28 | } 29 | 30 | // NewSyslogWriter returns a writer ready to be used with output modules. 31 | // If network and address are empty, Dial will connect to the local syslog server. 32 | func NewSyslogWriter(network, address string, prio syslog.Priority, tag string) io.Writer { 33 | s, err := syslog.Dial(network, address, prio, tag) 34 | if err != nil { 35 | m := "syslog dial error: " + err.Error() 36 | critialLogger.Print(m) 37 | panic(m) 38 | } 39 | return s 40 | } 41 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestColorPrint(t *testing.T) { 13 | buf := &bytes.Buffer{} 14 | colorPrint(buf, "test", red) 15 | assert.Equal(t, "\x1b[31mtest\x1b[0m", buf.String()) 16 | buf.Reset() 17 | colorPrint(buf, "test", green) 18 | assert.Equal(t, "\x1b[32mtest\x1b[0m", buf.String()) 19 | buf.Reset() 20 | colorPrint(buf, "test", yellow) 21 | assert.Equal(t, "\x1b[33mtest\x1b[0m", buf.String()) 22 | buf.Reset() 23 | colorPrint(buf, "test", blue) 24 | assert.Equal(t, "\x1b[34mtest\x1b[0m", buf.String()) 25 | buf.Reset() 26 | colorPrint(buf, "test", gray) 27 | assert.Equal(t, "\x1b[37mtest\x1b[0m", buf.String()) 28 | } 29 | 30 | func TestNeedsQuotedValueRune(t *testing.T) { 31 | assert.True(t, needsQuotedValueRune('=')) 32 | assert.True(t, needsQuotedValueRune('"')) 33 | assert.True(t, needsQuotedValueRune(' ')) 34 | assert.False(t, needsQuotedValueRune('a')) 35 | assert.False(t, needsQuotedValueRune('\'')) 36 | } 37 | 38 | func TestWriteValue(t *testing.T) { 39 | buf := &bytes.Buffer{} 40 | write := func(v interface{}) string { 41 | buf.Reset() 42 | err := writeValue(buf, v) 43 | if err == nil { 44 | return buf.String() 45 | } 46 | return "" 47 | } 48 | assert.Equal(t, `foobar`, write(`foobar`)) 49 | assert.Equal(t, `"foo=bar"`, write(`foo=bar`)) 50 | assert.Equal(t, `"foo bar"`, write(`foo bar`)) 51 | assert.Equal(t, `"foo\"bar"`, write(`foo"bar`)) 52 | assert.Equal(t, `"foo\nbar"`, write("foo\nbar")) 53 | assert.Equal(t, `null`, write(nil)) 54 | assert.Equal(t, `"2000-01-02 03:04:05 +0000 UTC"`, write(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC))) 55 | assert.Equal(t, `"error \"with quote\""`, write(errors.New(`error "with quote"`))) 56 | } 57 | -------------------------------------------------------------------------------- /handler_examples_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xlog_test 4 | 5 | import ( 6 | "errors" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/justinas/alice" 12 | "github.com/rs/xlog" 13 | ) 14 | 15 | func Example_handler() { 16 | c := alice.New() 17 | 18 | host, _ := os.Hostname() 19 | conf := xlog.Config{ 20 | // Set some global env fields 21 | Fields: xlog.F{ 22 | "role": "my-service", 23 | "host": host, 24 | }, 25 | } 26 | 27 | // Install the logger handler with default output on the console 28 | c = c.Append(xlog.NewHandler(conf)) 29 | 30 | // Plug the xlog handler's input to Go's default logger 31 | log.SetFlags(0) 32 | log.SetOutput(xlog.New(conf)) 33 | 34 | // Install some provided extra handler to set some request's context fields. 35 | // Thanks to those handler, all our logs will come with some pre-populated fields. 36 | c = c.Append(xlog.RemoteAddrHandler("ip")) 37 | c = c.Append(xlog.UserAgentHandler("user_agent")) 38 | c = c.Append(xlog.RefererHandler("referer")) 39 | c = c.Append(xlog.RequestIDHandler("req_id", "Request-Id")) 40 | 41 | // Here is your final handler 42 | h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | // Get the logger from the request's context. You can safely assume it 44 | // will be always there: if the handler is removed, xlog.FromContext 45 | // will return a NopLogger 46 | l := xlog.FromRequest(r) 47 | 48 | // Then log some errors 49 | if err := errors.New("some error from elsewhere"); err != nil { 50 | l.Errorf("Here is an error: %v", err) 51 | } 52 | 53 | // Or some info with fields 54 | l.Info("Something happend", xlog.F{ 55 | "user": "current user id", 56 | "status": "ok", 57 | }) 58 | })) 59 | http.Handle("/", h) 60 | 61 | if err := http.ListenAndServe(":8080", nil); err != nil { 62 | log.SetOutput(os.Stderr) // make sure we print to console 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /levels_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLevelFromString(t *testing.T) { 10 | l, err := LevelFromString("debug") 11 | assert.NoError(t, err) 12 | assert.Equal(t, LevelDebug, l) 13 | l, err = LevelFromString("info") 14 | assert.NoError(t, err) 15 | assert.Equal(t, LevelInfo, l) 16 | l, err = LevelFromString("warn") 17 | assert.NoError(t, err) 18 | assert.Equal(t, LevelWarn, l) 19 | l, err = LevelFromString("error") 20 | assert.NoError(t, err) 21 | assert.Equal(t, LevelError, l) 22 | l, err = LevelFromString("fatal") 23 | assert.NoError(t, err) 24 | assert.Equal(t, LevelFatal, l) 25 | _, err = LevelFromString("foo") 26 | assert.Error(t, err, "") 27 | } 28 | 29 | func TestLevelUnmarshalerText(t *testing.T) { 30 | l := Level(-1) 31 | err := l.UnmarshalText([]byte("debug")) 32 | assert.NoError(t, err) 33 | assert.Equal(t, LevelDebug, l) 34 | err = l.UnmarshalText([]byte("info")) 35 | assert.NoError(t, err) 36 | assert.Equal(t, LevelInfo, l) 37 | err = l.UnmarshalText([]byte("warn")) 38 | assert.NoError(t, err) 39 | assert.Equal(t, LevelWarn, l) 40 | err = l.UnmarshalText([]byte("error")) 41 | assert.NoError(t, err) 42 | assert.Equal(t, LevelError, l) 43 | err = l.UnmarshalText([]byte("fatal")) 44 | assert.NoError(t, err) 45 | assert.Equal(t, LevelFatal, l) 46 | assert.Error(t, l.UnmarshalText([]byte("invalid"))) 47 | } 48 | 49 | func TestLevelString(t *testing.T) { 50 | assert.Equal(t, "debug", LevelDebug.String()) 51 | assert.Equal(t, "info", LevelInfo.String()) 52 | assert.Equal(t, "warn", LevelWarn.String()) 53 | assert.Equal(t, "error", LevelError.String()) 54 | assert.Equal(t, "fatal", LevelFatal.String()) 55 | assert.Equal(t, "10", Level(10).String()) 56 | } 57 | 58 | func TestLevelMarshalerText(t *testing.T) { 59 | b, err := LevelDebug.MarshalText() 60 | assert.NoError(t, err) 61 | assert.Equal(t, string(levelBytesDebug), string(b)) 62 | b, err = LevelInfo.MarshalText() 63 | assert.NoError(t, err) 64 | assert.Equal(t, string(levelBytesInfo), string(b)) 65 | b, err = LevelWarn.MarshalText() 66 | assert.NoError(t, err) 67 | assert.Equal(t, string(levelBytesWarn), string(b)) 68 | b, err = LevelError.MarshalText() 69 | assert.NoError(t, err) 70 | assert.Equal(t, string(levelBytesError), string(b)) 71 | b, err = LevelFatal.MarshalText() 72 | assert.NoError(t, err) 73 | assert.Equal(t, string(levelBytesFatal), string(b)) 74 | b, err = Level(10).MarshalText() 75 | assert.NoError(t, err) 76 | assert.Equal(t, "10", string(b)) 77 | } 78 | -------------------------------------------------------------------------------- /levels.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Level defines log levels 10 | type Level int 11 | 12 | // Log levels 13 | const ( 14 | LevelDebug Level = iota 15 | LevelInfo 16 | LevelWarn 17 | LevelError 18 | LevelFatal 19 | ) 20 | 21 | // Log level strings 22 | var ( 23 | levelDebug = "debug" 24 | levelInfo = "info" 25 | levelWarn = "warn" 26 | levelError = "error" 27 | levelFatal = "fatal" 28 | 29 | levelBytesDebug = []byte(levelDebug) 30 | levelBytesInfo = []byte(levelInfo) 31 | levelBytesWarn = []byte(levelWarn) 32 | levelBytesError = []byte(levelError) 33 | levelBytesFatal = []byte(levelFatal) 34 | ) 35 | 36 | // LevelFromString returns the level based on its string representation 37 | func LevelFromString(t string) (Level, error) { 38 | l := Level(0) 39 | err := (&l).UnmarshalText([]byte(t)) 40 | return l, err 41 | } 42 | 43 | // UnmarshalText lets Level implements the TextUnmarshaler interface used by encoding packages 44 | func (l *Level) UnmarshalText(text []byte) (err error) { 45 | if bytes.Equal(text, levelBytesDebug) { 46 | *l = LevelDebug 47 | } else if bytes.Equal(text, levelBytesInfo) { 48 | *l = LevelInfo 49 | } else if bytes.Equal(text, levelBytesWarn) { 50 | *l = LevelWarn 51 | } else if bytes.Equal(text, levelBytesError) { 52 | *l = LevelError 53 | } else if bytes.Equal(text, levelBytesFatal) { 54 | *l = LevelFatal 55 | } else { 56 | err = fmt.Errorf("Uknown level %v", string(text)) 57 | } 58 | return 59 | } 60 | 61 | // String returns the string representation of the level. 62 | func (l Level) String() string { 63 | var t string 64 | switch l { 65 | case LevelDebug: 66 | t = levelDebug 67 | case LevelInfo: 68 | t = levelInfo 69 | case LevelWarn: 70 | t = levelWarn 71 | case LevelError: 72 | t = levelError 73 | case LevelFatal: 74 | t = levelFatal 75 | default: 76 | t = strconv.FormatInt(int64(l), 10) 77 | } 78 | return t 79 | } 80 | 81 | // MarshalText lets Level implements the TextMarshaler interface used by encoding packages 82 | func (l Level) MarshalText() ([]byte, error) { 83 | var t []byte 84 | switch l { 85 | case LevelDebug: 86 | t = levelBytesDebug 87 | case LevelInfo: 88 | t = levelBytesInfo 89 | case LevelWarn: 90 | t = levelBytesWarn 91 | case LevelError: 92 | t = levelBytesError 93 | case LevelFatal: 94 | t = levelBytesFatal 95 | default: 96 | t = []byte(strconv.FormatInt(int64(l), 10)) 97 | } 98 | return t, nil 99 | } 100 | -------------------------------------------------------------------------------- /output_examples_test.go: -------------------------------------------------------------------------------- 1 | package xlog_test 2 | 3 | import ( 4 | "log/syslog" 5 | 6 | "github.com/rs/xlog" 7 | ) 8 | 9 | func Example_combinedOutputs() { 10 | conf := xlog.Config{ 11 | Output: xlog.NewOutputChannel(xlog.MultiOutput{ 12 | // Output interesting messages to console 13 | 0: xlog.FilterOutput{ 14 | Cond: func(fields map[string]interface{}) bool { 15 | val, found := fields["type"] 16 | return found && val == "interesting" 17 | }, 18 | Output: xlog.NewConsoleOutput(), 19 | }, 20 | // Also setup by-level loggers 21 | 1: xlog.LevelOutput{ 22 | // Send debug messages to console if they match type 23 | Debug: xlog.FilterOutput{ 24 | Cond: func(fields map[string]interface{}) bool { 25 | val, found := fields["type"] 26 | return found && val == "interesting" 27 | }, 28 | Output: xlog.NewConsoleOutput(), 29 | }, 30 | }, 31 | // Also send everything over syslog 32 | 2: xlog.NewSyslogOutput("", "", ""), 33 | }), 34 | } 35 | 36 | lh := xlog.NewHandler(conf) 37 | _ = lh 38 | } 39 | 40 | func ExampleMultiOutput() { 41 | conf := xlog.Config{ 42 | Output: xlog.NewOutputChannel(xlog.MultiOutput{ 43 | // Output everything to console 44 | 0: xlog.NewConsoleOutput(), 45 | // and also to local syslog 46 | 1: xlog.NewSyslogOutput("", "", ""), 47 | }), 48 | } 49 | lh := xlog.NewHandler(conf) 50 | _ = lh 51 | } 52 | 53 | func ExampleFilterOutput() { 54 | conf := xlog.Config{ 55 | Output: xlog.NewOutputChannel(xlog.FilterOutput{ 56 | // Match messages containing a field type = interesting 57 | Cond: func(fields map[string]interface{}) bool { 58 | val, found := fields["type"] 59 | return found && val == "interesting" 60 | }, 61 | // Output matching messages to the console 62 | Output: xlog.NewConsoleOutput(), 63 | }), 64 | } 65 | 66 | lh := xlog.NewHandler(conf) 67 | _ = lh 68 | } 69 | 70 | func ExampleLevelOutput() { 71 | conf := xlog.Config{ 72 | Output: xlog.NewOutputChannel(xlog.LevelOutput{ 73 | // Send debug message to console 74 | Debug: xlog.NewConsoleOutput(), 75 | // and error messages to syslog 76 | Error: xlog.NewSyslogOutput("", "", ""), 77 | // other levels are discarded 78 | }), 79 | } 80 | 81 | lh := xlog.NewHandler(conf) 82 | _ = lh 83 | } 84 | 85 | func ExampleNewSyslogWriter() { 86 | conf := xlog.Config{ 87 | Output: xlog.NewOutputChannel(xlog.LevelOutput{ 88 | Debug: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_DEBUG, "")), 89 | Info: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_INFO, "")), 90 | Warn: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_WARNING, "")), 91 | Error: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_ERR, "")), 92 | }), 93 | } 94 | 95 | lh := xlog.NewHandler(conf) 96 | _ = lh 97 | } 98 | -------------------------------------------------------------------------------- /std.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import "fmt" 4 | 5 | var std = New(Config{ 6 | Output: NewConsoleOutput(), 7 | }) 8 | 9 | // SetLogger changes the global logger instance 10 | func SetLogger(logger Logger) { 11 | std = logger 12 | } 13 | 14 | // Debug calls the Debug() method on the default logger 15 | func Debug(v ...interface{}) { 16 | f := extractFields(&v) 17 | std.OutputF(LevelDebug, 2, fmt.Sprint(v...), f) 18 | } 19 | 20 | // Debugf calls the Debugf() method on the default logger 21 | func Debugf(format string, v ...interface{}) { 22 | f := extractFields(&v) 23 | std.OutputF(LevelDebug, 2, fmt.Sprintf(format, v...), f) 24 | } 25 | 26 | // Info calls the Info() method on the default logger 27 | func Info(v ...interface{}) { 28 | f := extractFields(&v) 29 | std.OutputF(LevelInfo, 2, fmt.Sprint(v...), f) 30 | } 31 | 32 | // Infof calls the Infof() method on the default logger 33 | func Infof(format string, v ...interface{}) { 34 | f := extractFields(&v) 35 | std.OutputF(LevelInfo, 2, fmt.Sprintf(format, v...), f) 36 | } 37 | 38 | // Warn calls the Warn() method on the default logger 39 | func Warn(v ...interface{}) { 40 | f := extractFields(&v) 41 | std.OutputF(LevelWarn, 2, fmt.Sprint(v...), f) 42 | } 43 | 44 | // Warnf calls the Warnf() method on the default logger 45 | func Warnf(format string, v ...interface{}) { 46 | f := extractFields(&v) 47 | std.OutputF(LevelWarn, 2, fmt.Sprintf(format, v...), f) 48 | } 49 | 50 | // Error calls the Error() method on the default logger 51 | func Error(v ...interface{}) { 52 | f := extractFields(&v) 53 | std.OutputF(LevelError, 2, fmt.Sprint(v...), f) 54 | } 55 | 56 | // Errorf calls the Errorf() method on the default logger 57 | // 58 | // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last 59 | // argument to workaround go vet false alarm. 60 | func Errorf(format string, v ...interface{}) { 61 | f := extractFields(&v) 62 | if f != nil { 63 | // Let user add a %v at the end of the message when fields are passed to satisfy go vet 64 | l := len(format) 65 | if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { 66 | format = format[0 : l-2] 67 | } 68 | } 69 | std.OutputF(LevelError, 2, fmt.Sprintf(format, v...), f) 70 | } 71 | 72 | // Fatal calls the Fatal() method on the default logger 73 | func Fatal(v ...interface{}) { 74 | f := extractFields(&v) 75 | std.OutputF(LevelFatal, 2, fmt.Sprint(v...), f) 76 | if l, ok := std.(*logger); ok { 77 | if o, ok := l.output.(*OutputChannel); ok { 78 | o.Close() 79 | } 80 | } 81 | exit1() 82 | } 83 | 84 | // Fatalf calls the Fatalf() method on the default logger 85 | // 86 | // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last 87 | // argument to workaround go vet false alarm. 88 | func Fatalf(format string, v ...interface{}) { 89 | f := extractFields(&v) 90 | if f != nil { 91 | // Let user add a %v at the end of the message when fields are passed to satisfy go vet 92 | l := len(format) 93 | if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { 94 | format = format[0 : l-2] 95 | } 96 | } 97 | std.OutputF(LevelFatal, 2, fmt.Sprintf(format, v...), f) 98 | if l, ok := std.(*logger); ok { 99 | if o, ok := l.output.(*OutputChannel); ok { 100 | o.Close() 101 | } 102 | } 103 | exit1() 104 | } 105 | -------------------------------------------------------------------------------- /std_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGlobalLogger(t *testing.T) { 10 | o := newTestOutput() 11 | oldStd := std 12 | defer func() { std = oldStd }() 13 | SetLogger(New(Config{Output: o})) 14 | Debug("test") 15 | last := o.get() 16 | assert.Equal(t, "test", last["message"]) 17 | assert.Equal(t, "debug", last["level"]) 18 | o.reset() 19 | Debugf("test") 20 | last = o.get() 21 | assert.Equal(t, "test", last["message"]) 22 | assert.Equal(t, "debug", last["level"]) 23 | o.reset() 24 | Info("test") 25 | last = o.get() 26 | assert.Equal(t, "test", last["message"]) 27 | assert.Equal(t, "info", last["level"]) 28 | o.reset() 29 | Infof("test") 30 | last = o.get() 31 | assert.Equal(t, "test", last["message"]) 32 | assert.Equal(t, "info", last["level"]) 33 | o.reset() 34 | Warn("test") 35 | last = o.get() 36 | assert.Equal(t, "test", last["message"]) 37 | assert.Equal(t, "warn", last["level"]) 38 | o.reset() 39 | Warnf("test") 40 | last = o.get() 41 | assert.Equal(t, "test", last["message"]) 42 | assert.Equal(t, "warn", last["level"]) 43 | o.reset() 44 | Error("test") 45 | last = o.get() 46 | assert.Equal(t, "test", last["message"]) 47 | assert.Equal(t, "error", last["level"]) 48 | o.reset() 49 | Errorf("test") 50 | last = o.get() 51 | assert.Equal(t, "test", last["message"]) 52 | assert.Equal(t, "error", last["level"]) 53 | o.reset() 54 | oldExit := exit1 55 | exit1 = func() {} 56 | defer func() { exit1 = oldExit }() 57 | Fatal("test") 58 | last = o.get() 59 | assert.Equal(t, "test", last["message"]) 60 | assert.Equal(t, "fatal", last["level"]) 61 | o.reset() 62 | Fatalf("test") 63 | last = o.get() 64 | assert.Equal(t, "test", last["message"]) 65 | assert.Equal(t, "fatal", last["level"]) 66 | o.reset() 67 | } 68 | 69 | func TestStdError(t *testing.T) { 70 | o := newTestOutput() 71 | oldStd := std 72 | defer func() { std = oldStd }() 73 | SetLogger(New(Config{Output: o})) 74 | Error("test", F{"foo": "bar"}) 75 | last := <-o.w 76 | assert.Contains(t, last["file"], "std_test.go:") 77 | delete(last, "file") 78 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test", "foo": "bar"}, last) 79 | } 80 | 81 | func TestStdErrorf(t *testing.T) { 82 | o := newTestOutput() 83 | oldStd := std 84 | defer func() { std = oldStd }() 85 | SetLogger(New(Config{Output: o})) 86 | Errorf("test %d%v", 1, F{"foo": "bar"}) 87 | last := <-o.w 88 | assert.Contains(t, last["file"], "std_test.go:") 89 | delete(last, "file") 90 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test 1", "foo": "bar"}, last) 91 | } 92 | 93 | func TestStdFatal(t *testing.T) { 94 | e := exit1 95 | exited := 0 96 | exit1 = func() { exited++ } 97 | defer func() { exit1 = e }() 98 | o := newTestOutput() 99 | oldStd := std 100 | defer func() { std = oldStd }() 101 | SetLogger(New(Config{Output: o})) 102 | Fatal("test", F{"foo": "bar"}) 103 | last := <-o.w 104 | assert.Contains(t, last["file"], "std_test.go:") 105 | delete(last, "file") 106 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test", "foo": "bar"}, last) 107 | assert.Equal(t, 1, exited) 108 | } 109 | 110 | func TestStdFatalf(t *testing.T) { 111 | e := exit1 112 | exited := 0 113 | exit1 = func() { exited++ } 114 | defer func() { exit1 = e }() 115 | o := newTestOutput() 116 | oldStd := std 117 | defer func() { std = oldStd }() 118 | SetLogger(New(Config{Output: o})) 119 | Fatalf("test %d%v", 1, F{"foo": "bar"}) 120 | last := <-o.w 121 | assert.Contains(t, last["file"], "std_test.go:") 122 | delete(last, "file") 123 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test 1", "foo": "bar"}, last) 124 | assert.Equal(t, 1, exited) 125 | } 126 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xlog 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestFromContext(t *testing.T) { 16 | assert.Equal(t, NopLogger, FromContext(nil)) 17 | assert.Equal(t, NopLogger, FromContext(context.Background())) 18 | l := &logger{} 19 | ctx := NewContext(context.Background(), l) 20 | assert.Equal(t, l, FromContext(ctx)) 21 | } 22 | 23 | func TestNewHandler(t *testing.T) { 24 | c := Config{ 25 | Level: LevelInfo, 26 | Fields: F{"foo": "bar"}, 27 | Output: NewOutputChannel(&testOutput{}), 28 | } 29 | lh := NewHandler(c) 30 | h := lh(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | l := FromRequest(r) 32 | assert.NotNil(t, l) 33 | assert.NotEqual(t, NopLogger, l) 34 | if l, ok := l.(*logger); assert.True(t, ok) { 35 | assert.Equal(t, LevelInfo, l.level) 36 | assert.Equal(t, c.Output, l.output) 37 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 38 | } 39 | })) 40 | h.ServeHTTP(nil, &http.Request{}) 41 | } 42 | 43 | func TestURLHandler(t *testing.T) { 44 | r := &http.Request{ 45 | URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, 46 | } 47 | h := URLHandler("url")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | l := FromRequest(r).(*logger) 49 | assert.Equal(t, F{"url": "/path?foo=bar"}, F(l.fields)) 50 | })) 51 | h = NewHandler(Config{})(h) 52 | h.ServeHTTP(nil, r) 53 | } 54 | 55 | func TestMethodHandler(t *testing.T) { 56 | r := &http.Request{ 57 | Method: "POST", 58 | } 59 | h := MethodHandler("method")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 | l := FromRequest(r).(*logger) 61 | assert.Equal(t, F{"method": "POST"}, F(l.fields)) 62 | })) 63 | h = NewHandler(Config{})(h) 64 | h.ServeHTTP(nil, r) 65 | } 66 | 67 | func TestRequestHandler(t *testing.T) { 68 | r := &http.Request{ 69 | Method: "POST", 70 | URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, 71 | } 72 | h := RequestHandler("request")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | l := FromRequest(r).(*logger) 74 | assert.Equal(t, F{"request": "POST /path?foo=bar"}, F(l.fields)) 75 | })) 76 | h = NewHandler(Config{})(h) 77 | h.ServeHTTP(nil, r) 78 | } 79 | 80 | func TestRemoteAddrHandler(t *testing.T) { 81 | r := &http.Request{ 82 | RemoteAddr: "1.2.3.4:1234", 83 | } 84 | h := RemoteAddrHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 85 | l := FromRequest(r).(*logger) 86 | assert.Equal(t, F{"ip": "1.2.3.4"}, F(l.fields)) 87 | })) 88 | h = NewHandler(Config{})(h) 89 | h.ServeHTTP(nil, r) 90 | } 91 | 92 | func TestRemoteAddrHandlerIPv6(t *testing.T) { 93 | r := &http.Request{ 94 | RemoteAddr: "[2001:db8:a0b:12f0::1]:1234", 95 | } 96 | h := RemoteAddrHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | l := FromRequest(r).(*logger) 98 | assert.Equal(t, F{"ip": "2001:db8:a0b:12f0::1"}, F(l.fields)) 99 | })) 100 | h = NewHandler(Config{})(h) 101 | h.ServeHTTP(nil, r) 102 | } 103 | 104 | func TestUserAgentHandler(t *testing.T) { 105 | r := &http.Request{ 106 | Header: http.Header{ 107 | "User-Agent": []string{"some user agent string"}, 108 | }, 109 | } 110 | h := UserAgentHandler("ua")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 111 | l := FromRequest(r).(*logger) 112 | assert.Equal(t, F{"ua": "some user agent string"}, F(l.fields)) 113 | })) 114 | h = NewHandler(Config{})(h) 115 | h.ServeHTTP(nil, r) 116 | } 117 | 118 | func TestRefererHandler(t *testing.T) { 119 | r := &http.Request{ 120 | Header: http.Header{ 121 | "Referer": []string{"http://foo.com/bar"}, 122 | }, 123 | } 124 | h := RefererHandler("ua")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 125 | l := FromRequest(r).(*logger) 126 | assert.Equal(t, F{"ua": "http://foo.com/bar"}, F(l.fields)) 127 | })) 128 | h = NewHandler(Config{})(h) 129 | h.ServeHTTP(nil, r) 130 | } 131 | 132 | func TestRequestIDHandler(t *testing.T) { 133 | r := &http.Request{} 134 | h := RequestIDHandler("id", "Request-Id")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 | l := FromRequest(r).(*logger) 136 | if id, ok := IDFromRequest(r); assert.True(t, ok) { 137 | assert.Equal(t, l.fields["id"], id) 138 | assert.Len(t, id.String(), 20) 139 | assert.Equal(t, id.String(), w.Header().Get("Request-Id")) 140 | } 141 | assert.Len(t, l.fields["id"], 12) 142 | })) 143 | h = NewHandler(Config{})(h) 144 | w := httptest.NewRecorder() 145 | h.ServeHTTP(w, r) 146 | } 147 | -------------------------------------------------------------------------------- /handler_pre17_test.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package xlog 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/rs/xhandler" 12 | "github.com/stretchr/testify/assert" 13 | "golang.org/x/net/context" 14 | ) 15 | 16 | func TestFromContext(t *testing.T) { 17 | assert.Equal(t, NopLogger, FromContext(nil)) 18 | assert.Equal(t, NopLogger, FromContext(context.Background())) 19 | l := &logger{} 20 | ctx := NewContext(context.Background(), l) 21 | assert.Equal(t, l, FromContext(ctx)) 22 | } 23 | 24 | func TestNewHandler(t *testing.T) { 25 | c := Config{ 26 | Level: LevelInfo, 27 | Fields: F{"foo": "bar"}, 28 | Output: NewOutputChannel(&testOutput{}), 29 | } 30 | lh := NewHandler(c) 31 | h := lh(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 32 | l := FromContext(ctx) 33 | assert.NotNil(t, l) 34 | assert.NotEqual(t, NopLogger, l) 35 | if l, ok := l.(*logger); assert.True(t, ok) { 36 | assert.Equal(t, LevelInfo, l.level) 37 | assert.Equal(t, c.Output, l.output) 38 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 39 | } 40 | })) 41 | h.ServeHTTPC(context.Background(), nil, nil) 42 | } 43 | 44 | func TestURLHandler(t *testing.T) { 45 | r := &http.Request{ 46 | URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, 47 | } 48 | h := URLHandler("url")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 49 | l := FromContext(ctx).(*logger) 50 | assert.Equal(t, F{"url": "/path?foo=bar"}, F(l.fields)) 51 | })) 52 | h = NewHandler(Config{})(h) 53 | h.ServeHTTPC(context.Background(), nil, r) 54 | } 55 | 56 | func TestMethodHandler(t *testing.T) { 57 | r := &http.Request{ 58 | Method: "POST", 59 | } 60 | h := MethodHandler("method")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 61 | l := FromContext(ctx).(*logger) 62 | assert.Equal(t, F{"method": "POST"}, F(l.fields)) 63 | })) 64 | h = NewHandler(Config{})(h) 65 | h.ServeHTTPC(context.Background(), nil, r) 66 | } 67 | 68 | func TestRequestHandler(t *testing.T) { 69 | r := &http.Request{ 70 | Method: "POST", 71 | URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, 72 | } 73 | h := RequestHandler("request")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 74 | l := FromContext(ctx).(*logger) 75 | assert.Equal(t, F{"request": "POST /path?foo=bar"}, F(l.fields)) 76 | })) 77 | h = NewHandler(Config{})(h) 78 | h.ServeHTTPC(context.Background(), nil, r) 79 | } 80 | 81 | func TestRemoteAddrHandler(t *testing.T) { 82 | r := &http.Request{ 83 | RemoteAddr: "1.2.3.4:1234", 84 | } 85 | h := RemoteAddrHandler("ip")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 86 | l := FromContext(ctx).(*logger) 87 | assert.Equal(t, F{"ip": "1.2.3.4"}, F(l.fields)) 88 | })) 89 | h = NewHandler(Config{})(h) 90 | h.ServeHTTPC(context.Background(), nil, r) 91 | } 92 | 93 | func TestRemoteAddrHandlerIPv6(t *testing.T) { 94 | r := &http.Request{ 95 | RemoteAddr: "[2001:db8:a0b:12f0::1]:1234", 96 | } 97 | h := RemoteAddrHandler("ip")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 98 | l := FromContext(ctx).(*logger) 99 | assert.Equal(t, F{"ip": "2001:db8:a0b:12f0::1"}, F(l.fields)) 100 | })) 101 | h = NewHandler(Config{})(h) 102 | h.ServeHTTPC(context.Background(), nil, r) 103 | } 104 | 105 | func TestUserAgentHandler(t *testing.T) { 106 | r := &http.Request{ 107 | Header: http.Header{ 108 | "User-Agent": []string{"some user agent string"}, 109 | }, 110 | } 111 | h := UserAgentHandler("ua")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 112 | l := FromContext(ctx).(*logger) 113 | assert.Equal(t, F{"ua": "some user agent string"}, F(l.fields)) 114 | })) 115 | h = NewHandler(Config{})(h) 116 | h.ServeHTTPC(context.Background(), nil, r) 117 | } 118 | 119 | func TestRefererHandler(t *testing.T) { 120 | r := &http.Request{ 121 | Header: http.Header{ 122 | "Referer": []string{"http://foo.com/bar"}, 123 | }, 124 | } 125 | h := RefererHandler("ua")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 126 | l := FromContext(ctx).(*logger) 127 | assert.Equal(t, F{"ua": "http://foo.com/bar"}, F(l.fields)) 128 | })) 129 | h = NewHandler(Config{})(h) 130 | h.ServeHTTPC(context.Background(), nil, r) 131 | } 132 | 133 | func TestRequestIDHandler(t *testing.T) { 134 | r := &http.Request{} 135 | h := RequestIDHandler("id", "Request-Id")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 136 | l := FromContext(ctx).(*logger) 137 | if id, ok := IDFromContext(ctx); assert.True(t, ok) { 138 | assert.Equal(t, l.fields["id"], id) 139 | assert.Len(t, id.String(), 20) 140 | assert.Equal(t, id.String(), w.Header().Get("Request-Id")) 141 | } 142 | assert.Len(t, l.fields["id"], 12) 143 | })) 144 | h = NewHandler(Config{})(h) 145 | w := httptest.NewRecorder() 146 | h.ServeHTTPC(context.Background(), w, r) 147 | } 148 | -------------------------------------------------------------------------------- /handler_pre17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package xlog 4 | 5 | import ( 6 | "net" 7 | "net/http" 8 | 9 | "github.com/rs/xhandler" 10 | "github.com/rs/xid" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | type key int 15 | 16 | const ( 17 | logKey key = iota 18 | idKey 19 | ) 20 | 21 | // IDFromContext returns the unique id associated to the request if any. 22 | func IDFromContext(ctx context.Context) (xid.ID, bool) { 23 | id, ok := ctx.Value(idKey).(xid.ID) 24 | return id, ok 25 | } 26 | 27 | // FromContext gets the logger out of the context. 28 | // If not logger is stored in the context, a NopLogger is returned. 29 | func FromContext(ctx context.Context) Logger { 30 | if ctx == nil { 31 | return NopLogger 32 | } 33 | l, ok := ctx.Value(logKey).(Logger) 34 | if !ok { 35 | return NopLogger 36 | } 37 | return l 38 | } 39 | 40 | // NewContext returns a copy of the parent context and associates it with the provided logger. 41 | func NewContext(ctx context.Context, l Logger) context.Context { 42 | return context.WithValue(ctx, logKey, l) 43 | } 44 | 45 | // NewHandler instanciates a new xlog HTTP handler. 46 | // 47 | // If not configured, the output is set to NewConsoleOutput() by default. 48 | func NewHandler(c Config) func(xhandler.HandlerC) xhandler.HandlerC { 49 | if c.Output == nil { 50 | c.Output = NewOutputChannel(NewConsoleOutput()) 51 | } 52 | return func(next xhandler.HandlerC) xhandler.HandlerC { 53 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 54 | l := New(c) 55 | ctx = NewContext(ctx, l) 56 | next.ServeHTTPC(ctx, w, r) 57 | if l, ok := l.(*logger); ok { 58 | l.close() 59 | } 60 | }) 61 | } 62 | } 63 | 64 | // URLHandler returns a handler setting the request's URL as a field 65 | // to the current context's logger using the passed name as field name. 66 | func URLHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 67 | return func(next xhandler.HandlerC) xhandler.HandlerC { 68 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 69 | FromContext(ctx).SetField(name, r.URL.String()) 70 | next.ServeHTTPC(ctx, w, r) 71 | }) 72 | } 73 | } 74 | 75 | // MethodHandler returns a handler setting the request's method as a field 76 | // to the current context's logger using the passed name as field name. 77 | func MethodHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 78 | return func(next xhandler.HandlerC) xhandler.HandlerC { 79 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 80 | FromContext(ctx).SetField(name, r.Method) 81 | next.ServeHTTPC(ctx, w, r) 82 | }) 83 | } 84 | } 85 | 86 | // RequestHandler returns a handler setting the request's method and URL as a field 87 | // to the current context's logger using the passed name as field name. 88 | func RequestHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 89 | return func(next xhandler.HandlerC) xhandler.HandlerC { 90 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 91 | FromContext(ctx).SetField(name, r.Method+" "+r.URL.String()) 92 | next.ServeHTTPC(ctx, w, r) 93 | }) 94 | } 95 | } 96 | 97 | // RemoteAddrHandler returns a handler setting the request's remote address as a field 98 | // to the current context's logger using the passed name as field name. 99 | func RemoteAddrHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 100 | return func(next xhandler.HandlerC) xhandler.HandlerC { 101 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 102 | if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { 103 | FromContext(ctx).SetField(name, host) 104 | } 105 | next.ServeHTTPC(ctx, w, r) 106 | }) 107 | } 108 | } 109 | 110 | // UserAgentHandler returns a handler setting the request's client's user-agent as 111 | // a field to the current context's logger using the passed name as field name. 112 | func UserAgentHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 113 | return func(next xhandler.HandlerC) xhandler.HandlerC { 114 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 115 | if ua := r.Header.Get("User-Agent"); ua != "" { 116 | FromContext(ctx).SetField(name, ua) 117 | } 118 | next.ServeHTTPC(ctx, w, r) 119 | }) 120 | } 121 | } 122 | 123 | // RefererHandler returns a handler setting the request's referer header as 124 | // a field to the current context's logger using the passed name as field name. 125 | func RefererHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { 126 | return func(next xhandler.HandlerC) xhandler.HandlerC { 127 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 128 | if ref := r.Header.Get("Referer"); ref != "" { 129 | FromContext(ctx).SetField(name, ref) 130 | } 131 | next.ServeHTTPC(ctx, w, r) 132 | }) 133 | } 134 | } 135 | 136 | // RequestIDHandler returns a handler setting a unique id to the request which can 137 | // be gathered using IDFromContext(ctx). This generated id is added as a field to the 138 | // logger using the passed name as field name. The id is also added as a response 139 | // header if the headerName is not empty. 140 | // 141 | // The generated id is a URL safe base64 encoded mongo object-id-like unique id. 142 | // Mongo unique id generation algorithm has been selected as a trade-off between 143 | // size and ease of use: UUID is less space efficient and snowflake requires machine 144 | // configuration. 145 | func RequestIDHandler(name, headerName string) func(next xhandler.HandlerC) xhandler.HandlerC { 146 | return func(next xhandler.HandlerC) xhandler.HandlerC { 147 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 148 | id, ok := IDFromContext(ctx) 149 | if !ok { 150 | id = xid.New() 151 | ctx = context.WithValue(ctx, idKey, id) 152 | } 153 | if name != "" { 154 | FromContext(ctx).SetField(name, id) 155 | } 156 | if headerName != "" { 157 | w.Header().Set(headerName, id.String()) 158 | } 159 | next.ServeHTTPC(ctx, w, r) 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xlog 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/rs/xid" 11 | ) 12 | 13 | type key int 14 | 15 | const ( 16 | logKey key = iota 17 | idKey 18 | ) 19 | 20 | // IDFromContext returns the unique id associated to the request if any. 21 | func IDFromContext(ctx context.Context) (xid.ID, bool) { 22 | id, ok := ctx.Value(idKey).(xid.ID) 23 | return id, ok 24 | } 25 | 26 | // IDFromRequest returns the unique id accociated to the request if any. 27 | func IDFromRequest(r *http.Request) (xid.ID, bool) { 28 | if r == nil { 29 | return xid.ID{}, false 30 | } 31 | return IDFromContext(r.Context()) 32 | } 33 | 34 | // FromContext gets the logger out of the context. 35 | // If not logger is stored in the context, a NopLogger is returned. 36 | func FromContext(ctx context.Context) Logger { 37 | if ctx == nil { 38 | return NopLogger 39 | } 40 | l, ok := ctx.Value(logKey).(Logger) 41 | if !ok { 42 | return NopLogger 43 | } 44 | return l 45 | } 46 | 47 | // FromRequest gets the logger in the request's context. 48 | // This is a shortcut for xlog.FromContext(r.Context()) 49 | func FromRequest(r *http.Request) Logger { 50 | if r == nil { 51 | return NopLogger 52 | } 53 | return FromContext(r.Context()) 54 | } 55 | 56 | // NewContext returns a copy of the parent context and associates it with the provided logger. 57 | func NewContext(ctx context.Context, l Logger) context.Context { 58 | return context.WithValue(ctx, logKey, l) 59 | } 60 | 61 | // NewHandler instanciates a new xlog HTTP handler. 62 | // 63 | // If not configured, the output is set to NewConsoleOutput() by default. 64 | func NewHandler(c Config) func(http.Handler) http.Handler { 65 | if c.Output == nil { 66 | c.Output = NewOutputChannel(NewConsoleOutput()) 67 | } 68 | return func(next http.Handler) http.Handler { 69 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | var l Logger 71 | if r != nil { 72 | l = New(c) 73 | r = r.WithContext(NewContext(r.Context(), l)) 74 | } 75 | next.ServeHTTP(w, r) 76 | if l, ok := l.(*logger); ok { 77 | l.close() 78 | } 79 | }) 80 | } 81 | } 82 | 83 | // URLHandler returns a handler setting the request's URL as a field 84 | // to the current context's logger using the passed name as field name. 85 | func URLHandler(name string) func(next http.Handler) http.Handler { 86 | return func(next http.Handler) http.Handler { 87 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 88 | l := FromContext(r.Context()) 89 | l.SetField(name, r.URL.String()) 90 | next.ServeHTTP(w, r) 91 | }) 92 | } 93 | } 94 | 95 | // MethodHandler returns a handler setting the request's method as a field 96 | // to the current context's logger using the passed name as field name. 97 | func MethodHandler(name string) func(next http.Handler) http.Handler { 98 | return func(next http.Handler) http.Handler { 99 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 100 | l := FromContext(r.Context()) 101 | l.SetField(name, r.Method) 102 | next.ServeHTTP(w, r) 103 | }) 104 | } 105 | } 106 | 107 | // RequestHandler returns a handler setting the request's method and URL as a field 108 | // to the current context's logger using the passed name as field name. 109 | func RequestHandler(name string) func(next http.Handler) http.Handler { 110 | return func(next http.Handler) http.Handler { 111 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | l := FromContext(r.Context()) 113 | l.SetField(name, r.Method+" "+r.URL.String()) 114 | next.ServeHTTP(w, r) 115 | }) 116 | } 117 | } 118 | 119 | // RemoteAddrHandler returns a handler setting the request's remote address as a field 120 | // to the current context's logger using the passed name as field name. 121 | func RemoteAddrHandler(name string) func(next http.Handler) http.Handler { 122 | return func(next http.Handler) http.Handler { 123 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { 125 | l := FromContext(r.Context()) 126 | l.SetField(name, host) 127 | } 128 | next.ServeHTTP(w, r) 129 | }) 130 | } 131 | } 132 | 133 | // UserAgentHandler returns a handler setting the request's client's user-agent as 134 | // a field to the current context's logger using the passed name as field name. 135 | func UserAgentHandler(name string) func(next http.Handler) http.Handler { 136 | return func(next http.Handler) http.Handler { 137 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | if ua := r.Header.Get("User-Agent"); ua != "" { 139 | l := FromContext(r.Context()) 140 | l.SetField(name, ua) 141 | } 142 | next.ServeHTTP(w, r) 143 | }) 144 | } 145 | } 146 | 147 | // RefererHandler returns a handler setting the request's referer header as 148 | // a field to the current context's logger using the passed name as field name. 149 | func RefererHandler(name string) func(next http.Handler) http.Handler { 150 | return func(next http.Handler) http.Handler { 151 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 | if ref := r.Header.Get("Referer"); ref != "" { 153 | l := FromContext(r.Context()) 154 | l.SetField(name, ref) 155 | } 156 | next.ServeHTTP(w, r) 157 | }) 158 | } 159 | } 160 | 161 | // RequestIDHandler returns a handler setting a unique id to the request which can 162 | // be gathered using IDFromContext(ctx). This generated id is added as a field to the 163 | // logger using the passed name as field name. The id is also added as a response 164 | // header if the headerName is not empty. 165 | // 166 | // The generated id is a URL safe base64 encoded mongo object-id-like unique id. 167 | // Mongo unique id generation algorithm has been selected as a trade-off between 168 | // size and ease of use: UUID is less space efficient and snowflake requires machine 169 | // configuration. 170 | func RequestIDHandler(name, headerName string) func(next http.Handler) http.Handler { 171 | return func(next http.Handler) http.Handler { 172 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 173 | ctx := r.Context() 174 | id, ok := IDFromContext(ctx) 175 | if !ok { 176 | id = xid.New() 177 | ctx = context.WithValue(ctx, idKey, id) 178 | r = r.WithContext(ctx) 179 | } 180 | if name != "" { 181 | FromContext(ctx).SetField(name, id) 182 | } 183 | if headerName != "" { 184 | w.Header().Set(headerName, id.String()) 185 | } 186 | next.ServeHTTP(w, r) 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: **Check [zerolog](https://github.com/rs/zerolog), the successor of xlog.** 2 | 3 | 4 | # HTTP Handler Logger 5 | 6 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/xlog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/xlog/master/LICENSE) [![Build Status](https://travis-ci.org/rs/xlog.svg?branch=master)](https://travis-ci.org/rs/xlog) [![Coverage](http://gocover.io/_badge/github.com/rs/xlog)](http://gocover.io/github.com/rs/xlog) 7 | 8 | `xlog` is a logger for [net/context](https://godoc.org/golang.org/x/net/context) aware HTTP applications. 9 | 10 | Unlike most loggers, `xlog` will never block your application because one its outputs is lagging. The log commands are connected to their outputs through a buffered channel and will prefer to discard messages if the buffer get full. All message formatting, serialization and transport happen in a dedicated go routine. 11 | 12 | Read more about `xlog` on [Dailymotion engineering blog](http://engineering.dailymotion.com/our-way-to-go/). 13 | 14 | ![](screenshot.png) 15 | 16 | ## Features 17 | 18 | - Per request log context 19 | - Per request and/or per message key/value fields 20 | - Log levels (Debug, Info, Warn, Error) 21 | - Color output when terminal is detected 22 | - Custom output (JSON, [logfmt](https://github.com/kr/logfmt), …) 23 | - Automatic gathering of request context like User-Agent, IP etc. 24 | - Drops message rather than blocking execution 25 | - Easy access logging thru [github.com/rs/xaccess](https://github.com/rs/xaccess) 26 | 27 | Works with both Go 1.7+ (with `net/context` support) and Go 1.6 if used with [github.com/rs/xhandler](https://github.com/rs/xhandler). 28 | 29 | ## Install 30 | 31 | go get github.com/rs/xlog 32 | 33 | ## Usage 34 | 35 | ```go 36 | c := alice.New() 37 | 38 | host, _ := os.Hostname() 39 | conf := xlog.Config{ 40 | // Log info level and higher 41 | Level: xlog.LevelInfo, 42 | // Set some global env fields 43 | Fields: xlog.F{ 44 | "role": "my-service", 45 | "host": host, 46 | }, 47 | // Output everything on console 48 | Output: xlog.NewOutputChannel(xlog.NewConsoleOutput()), 49 | } 50 | 51 | // Install the logger handler 52 | c = c.Append(xlog.NewHandler(conf)) 53 | 54 | // Optionally plug the xlog handler's input to Go's default logger 55 | log.SetFlags(0) 56 | xlogger := xlog.New(conf) 57 | log.SetOutput(xlogger) 58 | 59 | // Install some provided extra handler to set some request's context fields. 60 | // Thanks to those handler, all our logs will come with some pre-populated fields. 61 | c = c.Append(xlog.MethodHandler("method")) 62 | c = c.Append(xlog.URLHandler("url")) 63 | c = c.Append(xlog.RemoteAddrHandler("ip")) 64 | c = c.Append(xlog.UserAgentHandler("user_agent")) 65 | c = c.Append(xlog.RefererHandler("referer")) 66 | c = c.Append(xlog.RequestIDHandler("req_id", "Request-Id")) 67 | 68 | // Here is your final handler 69 | h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | // Get the logger from the request's context. You can safely assume it 71 | // will be always there: if the handler is removed, xlog.FromContext 72 | // will return a NopLogger 73 | l := xlog.FromRequest(r) 74 | 75 | // Then log some errors 76 | if err := errors.New("some error from elsewhere"); err != nil { 77 | l.Errorf("Here is an error: %v", err) 78 | } 79 | 80 | // Or some info with fields 81 | l.Info("Something happend", xlog.F{ 82 | "user": "current user id", 83 | "status": "ok", 84 | }) 85 | // Output: 86 | // { 87 | // "message": "Something happend", 88 | // "level": "info", 89 | // "file": "main.go:34", 90 | // "time": time.Time{...}, 91 | // "user": "current user id", 92 | // "status": "ok", 93 | // "ip": "1.2.3.4", 94 | // "user-agent": "Mozilla/1.2.3...", 95 | // "referer": "http://somewhere.com/path", 96 | // "role": "my-service", 97 | // "host": "somehost" 98 | // } 99 | })) 100 | http.Handle("/", h) 101 | 102 | if err := http.ListenAndServe(":8080", nil); err != nil { 103 | xlogger.Fatal(err) 104 | } 105 | ``` 106 | 107 | ### Copy Logger 108 | 109 | You may want to get a copy of the current logger to pass a modified version to a function without touching the original: 110 | 111 | ```go 112 | l := xlog.FromContext(ctx) 113 | l2 := xlog.Copy(l) 114 | l2.SetField("foo", "bar") 115 | ``` 116 | 117 | Make sure you copy a request context logger if you plan to use it in a go routine that may still exist after the end of the current request. Contextual loggers are reused after each requests to lower the pressure on the garbage collector. If you would use such a logger in a go routine, you may end up using a logger from another request/context or worse, a nil pointer: 118 | 119 | ```go 120 | l := xlog.FromContext(ctx) 121 | l2 := xlog.Copy(l) 122 | go func() { 123 | // use the safe copy 124 | l2.Info("something") 125 | }() 126 | ``` 127 | 128 | ### Global Logger 129 | 130 | You may use the standard Go logger and plug `xlog` as it's output as `xlog` implements `io.Writer`: 131 | 132 | ```go 133 | xlogger := xlog.New(conf) 134 | log.SetOutput(xlogger) 135 | ``` 136 | 137 | This has the advantage to make all your existing code or libraries already using Go's standard logger to use `xlog` with no change. The drawback though, is that you won't have control on the logging level and won't be able to add custom fields (other than ones set on the logger itself via configuration or `SetFields()`) for those messages. 138 | 139 | Another option for code you manage but which is outside of a HTTP request handler is to use the `xlog` provided default logger: 140 | 141 | ```go 142 | xlog.Debugf("some message with %s", variable, xlog.F{"and": "field support"}) 143 | ``` 144 | 145 | This way you have access to all the possibilities offered by `xlog` without having to carry the logger instance around. The default global logger has no fields set and has its output set to the console with no buffering channel. You may want to change that using the `xlog.SetLogger()` method: 146 | 147 | ```go 148 | xlog.SetLogger(xlog.New(xlog.Config{ 149 | Level: xlog.LevelInfo, 150 | Output: xlog.NewConsoleOutput(), 151 | Fields: xlog.F{ 152 | "role": "my-service", 153 | }, 154 | })) 155 | ``` 156 | 157 | ### Configure Output 158 | 159 | By default, output is setup to output debug and info message on `STDOUT` and warning and errors to `STDERR`. You can easily change this setup. 160 | 161 | XLog output can be customized using composable output handlers. Thanks to the [LevelOutput](https://godoc.org/github.com/rs/xlog#LevelOutput), [MultiOutput](https://godoc.org/github.com/rs/xlog#MultiOutput) and [FilterOutput](https://godoc.org/github.com/rs/xlog#FilterOutput), it is easy to route messages precisely. 162 | 163 | ```go 164 | conf := xlog.Config{ 165 | Output: xlog.NewOutputChannel(xlog.MultiOutput{ 166 | // Send all logs with field type=mymodule to a remote syslog 167 | 0: xlog.FilterOutput{ 168 | Cond: func(fields map[string]interface{}) bool { 169 | return fields["type"] == "mymodule" 170 | }, 171 | Output: xlog.NewSyslogOutput("tcp", "1.2.3.4:1234", "mymodule"), 172 | }, 173 | // Setup different output per log level 174 | 1: xlog.LevelOutput{ 175 | // Send errors to the console 176 | Error: xlog.NewConsoleOutput(), 177 | // Send syslog output for error level 178 | Info: xlog.NewSyslogOutput("", "", ""), 179 | }, 180 | }), 181 | }) 182 | 183 | h = xlog.NewHandler(conf) 184 | ``` 185 | 186 | #### Built-in Output Modules 187 | 188 | | Name | Description | 189 | |------|-------------| 190 | | [OutputChannel](https://godoc.org/github.com/rs/xlog#OutputChannel) | Buffers messages before sending. This output should always be the output directly set to xlog's configuration. 191 | | [MultiOutput](https://godoc.org/github.com/rs/xlog#MultiOutput) | Routes the same message to several outputs. If one or more outputs return error, the last error is returned. 192 | | [FilterOutput](https://godoc.org/github.com/rs/xlog#FilterOutput) | Tests a condition on the message and forward it to the child output if true. 193 | | [LevelOutput](https://godoc.org/github.com/rs/xlog#LevelOutput) | Routes messages per level outputs. 194 | | [ConsoleOutput](https://godoc.org/github.com/rs/xlog#NewConsoleOutput) | Prints messages in a human readable form on the stdout with color when supported. Fallback to logfmt output if the stdout isn't a terminal. 195 | | [JSONOutput](https://godoc.org/github.com/rs/xlog#NewJSONOutput) | Serialize messages in JSON. 196 | | [LogfmtOutput](https://godoc.org/github.com/rs/xlog#NewLogfmtOutput) | Serialize messages using Heroku like [logfmt](https://github.com/kr/logfmt). 197 | | [LogstashOutput](https://godoc.org/github.com/rs/xlog#NewLogstashOutput) | Serialize JSON message using Logstash 2.0 (schema v1) structured format. 198 | | [SyslogOutput](https://godoc.org/github.com/rs/xlog#NewSyslogOutput) | Send messages to syslog. 199 | | [UIDOutput](https://godoc.org/github.com/rs/xlog#NewUIDOutput) | Append a globally unique id to every message and forward it to the next output. 200 | 201 | ## Third Party Extensions 202 | 203 | | Project | Author | Description | 204 | |---------|--------|-------------| 205 | | [gRPClog](https://github.com/clawio/grpcxlog) | [Hugo González Labrador](https://github.com/labkode) | An adapter to use xlog as the logger for grpclog. 206 | | [xlog-nsq](https://github.com/rs/xlog-nsq) | [Olivier Poitrey](https://github.com/rs) | An xlog to [NSQ](http://nsq.io) output. 207 | | [xlog-sentry](https://github.com/trong/xlog-sentry) | [trong](https://github.com/trong) | An xlog to [Sentry](https://getsentry.com/) output. 208 | 209 | ## Licenses 210 | 211 | All source code is licensed under the [MIT License](https://raw.github.com/rs/xlog/master/LICENSE). 212 | -------------------------------------------------------------------------------- /xlog_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var fakeNow = time.Date(0, 0, 0, 0, 0, 0, 0, time.Local) 15 | var critialLoggerMux = sync.Mutex{} 16 | 17 | func init() { 18 | now = func() time.Time { 19 | return fakeNow 20 | } 21 | } 22 | 23 | func TestNew(t *testing.T) { 24 | oc := NewOutputChannel(newTestOutput()) 25 | defer oc.Close() 26 | c := Config{ 27 | Level: LevelError, 28 | Output: oc, 29 | Fields: F{"foo": "bar"}, 30 | } 31 | L := New(c) 32 | l, ok := L.(*logger) 33 | if assert.True(t, ok) { 34 | assert.Equal(t, LevelError, l.level) 35 | assert.Equal(t, c.Output, l.output) 36 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 37 | // Ensure l.fields is a clone 38 | c.Fields["bar"] = "baz" 39 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 40 | assert.Equal(t, false, l.disablePooling) 41 | l.close() 42 | } 43 | } 44 | 45 | func TestNewPoolDisabled(t *testing.T) { 46 | oc := NewOutputChannel(newTestOutput()) 47 | defer oc.Close() 48 | originalPool := loggerPool 49 | defer func(p *sync.Pool) { 50 | loggerPool = originalPool 51 | }(originalPool) 52 | loggerPool = &sync.Pool{ 53 | New: func() interface{} { 54 | assert.Fail(t, "pool used when disabled") 55 | return nil 56 | }, 57 | } 58 | c := Config{ 59 | Level: LevelError, 60 | Output: oc, 61 | Fields: F{"foo": "bar"}, 62 | DisablePooling: true, 63 | } 64 | L := New(c) 65 | l, ok := L.(*logger) 66 | if assert.True(t, ok) { 67 | assert.Equal(t, LevelError, l.level) 68 | assert.Equal(t, c.Output, l.output) 69 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 70 | // Ensure l.fields is a clone 71 | c.Fields["bar"] = "baz" 72 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 73 | assert.Equal(t, true, l.disablePooling) 74 | l.close() 75 | // Assert again to ensure close does not remove internal state 76 | assert.Equal(t, LevelError, l.level) 77 | assert.Equal(t, c.Output, l.output) 78 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 79 | // Ensure l.fields is a clone 80 | c.Fields["bar"] = "baz" 81 | assert.Equal(t, F{"foo": "bar"}, F(l.fields)) 82 | assert.Equal(t, true, l.disablePooling) 83 | } 84 | } 85 | 86 | func TestCopy(t *testing.T) { 87 | oc := NewOutputChannel(newTestOutput()) 88 | defer oc.Close() 89 | c := Config{ 90 | Level: LevelError, 91 | Output: oc, 92 | Fields: F{"foo": "bar"}, 93 | } 94 | l := New(c).(*logger) 95 | l2 := Copy(l).(*logger) 96 | assert.Equal(t, l.output, l2.output) 97 | assert.Equal(t, l.level, l2.level) 98 | assert.Equal(t, l.fields, l2.fields) 99 | l2.SetField("bar", "baz") 100 | assert.Equal(t, F{"foo": "bar"}, l.fields) 101 | assert.Equal(t, F{"foo": "bar", "bar": "baz"}, l2.fields) 102 | 103 | assert.Equal(t, NopLogger, Copy(NopLogger)) 104 | assert.Equal(t, NopLogger, Copy(nil)) 105 | } 106 | 107 | func TestNewDefautOutput(t *testing.T) { 108 | L := New(Config{}) 109 | l, ok := L.(*logger) 110 | if assert.True(t, ok) { 111 | assert.NotNil(t, l.output) 112 | l.close() 113 | } 114 | } 115 | 116 | func TestSend(t *testing.T) { 117 | o := newTestOutput() 118 | l := New(Config{Output: o}).(*logger) 119 | l.send(LevelDebug, 1, "test", F{"foo": "bar"}) 120 | last := <-o.w 121 | assert.Contains(t, last["file"], "log_test.go:") 122 | delete(last, "file") 123 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test", "foo": "bar"}, last) 124 | 125 | l.SetField("bar", "baz") 126 | l.send(LevelInfo, 1, "test", F{"foo": "bar"}) 127 | last = <-o.w 128 | assert.Contains(t, last["file"], "log_test.go:") 129 | delete(last, "file") 130 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test", "foo": "bar", "bar": "baz"}, last) 131 | 132 | l = New(Config{Output: o, Level: 1}).(*logger) 133 | o.reset() 134 | l.send(0, 2, "test", F{"foo": "bar"}) 135 | assert.True(t, o.empty()) 136 | } 137 | 138 | func TestSendDrop(t *testing.T) { 139 | t.Skip() 140 | r, w := io.Pipe() 141 | go func() { 142 | critialLoggerMux.Lock() 143 | defer critialLoggerMux.Unlock() 144 | oldCritialLogger := critialLogger 145 | critialLogger = log.New(w, "", 0) 146 | o := newTestOutput() 147 | oc := NewOutputChannelBuffer(Discard, 1) 148 | l := New(Config{Output: oc}).(*logger) 149 | l.send(LevelDebug, 2, "test", F{"foo": "bar"}) 150 | l.send(LevelDebug, 2, "test", F{"foo": "bar"}) 151 | l.send(LevelDebug, 2, "test", F{"foo": "bar"}) 152 | o.get() 153 | o.get() 154 | o.get() 155 | oc.Close() 156 | critialLogger = oldCritialLogger 157 | w.Close() 158 | }() 159 | b, err := ioutil.ReadAll(r) 160 | assert.NoError(t, err) 161 | assert.Contains(t, string(b), "send error: buffer full") 162 | } 163 | 164 | func TestExtractFields(t *testing.T) { 165 | v := []interface{}{"a", 1, map[string]interface{}{"foo": "bar"}} 166 | f := extractFields(&v) 167 | assert.Equal(t, map[string]interface{}{"foo": "bar"}, f) 168 | assert.Equal(t, []interface{}{"a", 1}, v) 169 | 170 | v = []interface{}{map[string]interface{}{"foo": "bar"}, "a", 1} 171 | f = extractFields(&v) 172 | assert.Nil(t, f) 173 | assert.Equal(t, []interface{}{map[string]interface{}{"foo": "bar"}, "a", 1}, v) 174 | 175 | v = []interface{}{"a", 1, F{"foo": "bar"}} 176 | f = extractFields(&v) 177 | assert.Equal(t, map[string]interface{}{"foo": "bar"}, f) 178 | assert.Equal(t, []interface{}{"a", 1}, v) 179 | 180 | v = []interface{}{} 181 | f = extractFields(&v) 182 | assert.Nil(t, f) 183 | assert.Equal(t, []interface{}{}, v) 184 | } 185 | 186 | func TestGetFields(t *testing.T) { 187 | oc := NewOutputChannelBuffer(Discard, 1) 188 | l := New(Config{Output: oc}).(*logger) 189 | l.SetField("k", "v") 190 | assert.Equal(t, F{"k": "v"}, l.GetFields()) 191 | } 192 | 193 | func TestDebug(t *testing.T) { 194 | o := newTestOutput() 195 | l := New(Config{Output: o}).(*logger) 196 | l.Debug("test", F{"foo": "bar"}) 197 | last := <-o.w 198 | assert.Contains(t, last["file"], "log_test.go:") 199 | delete(last, "file") 200 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test", "foo": "bar"}, last) 201 | } 202 | 203 | func TestDebugf(t *testing.T) { 204 | o := newTestOutput() 205 | l := New(Config{Output: o}).(*logger) 206 | l.Debugf("test %d", 1, F{"foo": "bar"}) 207 | last := <-o.w 208 | assert.Contains(t, last["file"], "log_test.go:") 209 | delete(last, "file") 210 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test 1", "foo": "bar"}, last) 211 | } 212 | 213 | func TestInfo(t *testing.T) { 214 | o := newTestOutput() 215 | l := New(Config{Output: o}).(*logger) 216 | l.Info("test", F{"foo": "bar"}) 217 | last := <-o.w 218 | assert.Contains(t, last["file"], "log_test.go:") 219 | delete(last, "file") 220 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test", "foo": "bar"}, last) 221 | } 222 | 223 | func TestInfof(t *testing.T) { 224 | o := newTestOutput() 225 | l := New(Config{Output: o}).(*logger) 226 | l.Infof("test %d", 1, F{"foo": "bar"}) 227 | last := <-o.w 228 | assert.Contains(t, last["file"], "log_test.go:") 229 | delete(last, "file") 230 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test 1", "foo": "bar"}, last) 231 | } 232 | 233 | func TestWarn(t *testing.T) { 234 | o := newTestOutput() 235 | l := New(Config{Output: o}).(*logger) 236 | l.Warn("test", F{"foo": "bar"}) 237 | last := <-o.w 238 | assert.Contains(t, last["file"], "log_test.go:") 239 | delete(last, "file") 240 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "warn", "message": "test", "foo": "bar"}, last) 241 | } 242 | 243 | func TestWarnf(t *testing.T) { 244 | o := newTestOutput() 245 | l := New(Config{Output: o}).(*logger) 246 | l.Warnf("test %d", 1, F{"foo": "bar"}) 247 | last := <-o.w 248 | assert.Contains(t, last["file"], "log_test.go:") 249 | delete(last, "file") 250 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "warn", "message": "test 1", "foo": "bar"}, last) 251 | } 252 | 253 | func TestError(t *testing.T) { 254 | o := newTestOutput() 255 | l := New(Config{Output: o}).(*logger) 256 | l.Error("test", F{"foo": "bar"}) 257 | last := <-o.w 258 | assert.Contains(t, last["file"], "log_test.go:") 259 | delete(last, "file") 260 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test", "foo": "bar"}, last) 261 | } 262 | 263 | func TestErrorf(t *testing.T) { 264 | o := newTestOutput() 265 | l := New(Config{Output: o}).(*logger) 266 | l.Errorf("test %d%v", 1, F{"foo": "bar"}) 267 | last := <-o.w 268 | assert.Contains(t, last["file"], "log_test.go:") 269 | delete(last, "file") 270 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test 1", "foo": "bar"}, last) 271 | } 272 | 273 | func TestFatal(t *testing.T) { 274 | e := exit1 275 | exited := 0 276 | exit1 = func() { exited++ } 277 | defer func() { exit1 = e }() 278 | o := newTestOutput() 279 | l := New(Config{Output: NewOutputChannel(o)}).(*logger) 280 | l.Fatal("test", F{"foo": "bar"}) 281 | last := <-o.w 282 | assert.Contains(t, last["file"], "log_test.go:") 283 | delete(last, "file") 284 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test", "foo": "bar"}, last) 285 | assert.Equal(t, 1, exited) 286 | } 287 | 288 | func TestFatalf(t *testing.T) { 289 | e := exit1 290 | exited := 0 291 | exit1 = func() { exited++ } 292 | defer func() { exit1 = e }() 293 | o := newTestOutput() 294 | l := New(Config{Output: NewOutputChannel(o)}).(*logger) 295 | l.Fatalf("test %d%v", 1, F{"foo": "bar"}) 296 | last := <-o.w 297 | assert.Contains(t, last["file"], "log_test.go:") 298 | delete(last, "file") 299 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test 1", "foo": "bar"}, last) 300 | assert.Equal(t, 1, exited) 301 | } 302 | 303 | func TestWrite(t *testing.T) { 304 | o := newTestOutput() 305 | xl := New(Config{Output: NewOutputChannel(o)}).(*logger) 306 | l := log.New(xl, "prefix ", 0) 307 | l.Printf("test") 308 | last := <-o.w 309 | assert.Contains(t, last["file"], "log_test.go:") 310 | delete(last, "file") 311 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "prefix test"}, last) 312 | } 313 | 314 | func TestOutput(t *testing.T) { 315 | o := newTestOutput() 316 | l := New(Config{Output: o}).(*logger) 317 | l.Output(2, "test") 318 | last := <-o.w 319 | assert.Contains(t, last["file"], "log_test.go:") 320 | delete(last, "file") 321 | assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test"}, last) 322 | } 323 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "os" 9 | "sort" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/rs/xid" 15 | "github.com/rs/xlog/internal/term" 16 | ) 17 | 18 | // Output sends a log message fields to a destination. 19 | type Output interface { 20 | Write(fields map[string]interface{}) error 21 | } 22 | 23 | // OutputFunc is an adapter to allow the use of ordinary functions as Output handlers. 24 | // If it is a function with the appropriate signature, OutputFunc(f) is a Output object 25 | // that calls f on Write(). 26 | type OutputFunc func(fields map[string]interface{}) error 27 | 28 | func (of OutputFunc) Write(fields map[string]interface{}) error { 29 | return of(fields) 30 | } 31 | 32 | // OutputChannel is a send buffered channel between xlog and an Output. 33 | type OutputChannel struct { 34 | input chan map[string]interface{} 35 | output Output 36 | stop chan struct{} 37 | } 38 | 39 | // ErrBufferFull is returned when the output channel buffer is full and messages 40 | // are discarded. 41 | var ErrBufferFull = errors.New("buffer full") 42 | 43 | // NewOutputChannel creates a consumer buffered channel for the given output 44 | // with a default buffer of 100 messages. 45 | func NewOutputChannel(o Output) *OutputChannel { 46 | return NewOutputChannelBuffer(o, 100) 47 | } 48 | 49 | // NewOutputChannelBuffer creates a consumer buffered channel for the given output 50 | // with a customizable buffer size. 51 | func NewOutputChannelBuffer(o Output, bufSize int) *OutputChannel { 52 | oc := &OutputChannel{ 53 | input: make(chan map[string]interface{}, bufSize), 54 | output: o, 55 | stop: make(chan struct{}), 56 | } 57 | 58 | go func() { 59 | for { 60 | select { 61 | case msg := <-oc.input: 62 | if err := o.Write(msg); err != nil { 63 | critialLogger.Print("cannot write log message: ", err.Error()) 64 | } 65 | case <-oc.stop: 66 | close(oc.stop) 67 | return 68 | } 69 | } 70 | }() 71 | 72 | return oc 73 | } 74 | 75 | // Write implements the Output interface 76 | func (oc *OutputChannel) Write(fields map[string]interface{}) (err error) { 77 | select { 78 | case oc.input <- fields: 79 | // Sent with success 80 | default: 81 | // Channel is full, message dropped 82 | err = ErrBufferFull 83 | } 84 | return err 85 | } 86 | 87 | // Flush flushes all the buffered message to the output 88 | func (oc *OutputChannel) Flush() { 89 | for { 90 | select { 91 | case msg := <-oc.input: 92 | if err := oc.output.Write(msg); err != nil { 93 | critialLogger.Print("cannot write log message: ", err.Error()) 94 | } 95 | default: 96 | return 97 | } 98 | } 99 | } 100 | 101 | // Close closes the output channel and release the consumer's go routine. 102 | func (oc *OutputChannel) Close() { 103 | if oc.stop == nil { 104 | return 105 | } 106 | oc.stop <- struct{}{} 107 | <-oc.stop 108 | oc.stop = nil 109 | oc.Flush() 110 | } 111 | 112 | // Discard is an Output that discards all log message going thru it. 113 | var Discard = OutputFunc(func(fields map[string]interface{}) error { 114 | return nil 115 | }) 116 | 117 | var bufPool = &sync.Pool{ 118 | New: func() interface{} { 119 | return &bytes.Buffer{} 120 | }, 121 | } 122 | 123 | // MultiOutput routes the same message to serveral outputs. 124 | // If one or more outputs return an error, the last error is returned. 125 | type MultiOutput []Output 126 | 127 | func (m MultiOutput) Write(fields map[string]interface{}) (err error) { 128 | for _, o := range m { 129 | e := o.Write(fields) 130 | if e != nil { 131 | err = e 132 | } 133 | } 134 | return 135 | } 136 | 137 | // FilterOutput test a condition on the message and forward it to the child output 138 | // if it returns true. 139 | type FilterOutput struct { 140 | Cond func(fields map[string]interface{}) bool 141 | Output Output 142 | } 143 | 144 | func (f FilterOutput) Write(fields map[string]interface{}) (err error) { 145 | if f.Output == nil { 146 | return 147 | } 148 | if f.Cond(fields) { 149 | return f.Output.Write(fields) 150 | } 151 | return 152 | } 153 | 154 | // LevelOutput routes messages to different output based on the message's level. 155 | type LevelOutput struct { 156 | Debug Output 157 | Info Output 158 | Warn Output 159 | Error Output 160 | Fatal Output 161 | } 162 | 163 | func (l LevelOutput) Write(fields map[string]interface{}) error { 164 | var o Output 165 | switch fields[KeyLevel] { 166 | case "debug": 167 | o = l.Debug 168 | case "info": 169 | o = l.Info 170 | case "warn": 171 | o = l.Warn 172 | case "error": 173 | o = l.Error 174 | case "fatal": 175 | o = l.Fatal 176 | } 177 | if o != nil { 178 | return o.Write(fields) 179 | } 180 | return nil 181 | } 182 | 183 | // RecorderOutput stores the raw messages in it's Messages field. This output is useful for testing. 184 | type RecorderOutput struct { 185 | Messages []F 186 | } 187 | 188 | func (l *RecorderOutput) Write(fields map[string]interface{}) error { 189 | if l.Messages == nil { 190 | l.Messages = []F{fields} 191 | } else { 192 | l.Messages = append(l.Messages, fields) 193 | } 194 | return nil 195 | } 196 | 197 | // Reset empty the output from stored messages 198 | func (l *RecorderOutput) Reset() { 199 | l.Messages = []F{} 200 | } 201 | 202 | type consoleOutput struct { 203 | w io.Writer 204 | } 205 | 206 | var isTerminal = term.IsTerminal 207 | 208 | // NewConsoleOutput returns a Output printing message in a colored human readable form on the 209 | // stderr. If the stderr is not on a terminal, a LogfmtOutput is returned instead. 210 | func NewConsoleOutput() Output { 211 | return NewConsoleOutputW(os.Stderr, NewLogfmtOutput(os.Stderr)) 212 | } 213 | 214 | // NewConsoleOutputW returns a Output printing message in a colored human readable form with 215 | // the provided writer. If the writer is not on a terminal, the noTerm output is returned. 216 | func NewConsoleOutputW(w io.Writer, noTerm Output) Output { 217 | if isTerminal(w) { 218 | return consoleOutput{w: w} 219 | } 220 | return noTerm 221 | } 222 | 223 | func (o consoleOutput) Write(fields map[string]interface{}) error { 224 | buf := bufPool.Get().(*bytes.Buffer) 225 | defer func() { 226 | buf.Reset() 227 | bufPool.Put(buf) 228 | }() 229 | if ts, ok := fields[KeyTime].(time.Time); ok { 230 | buf.Write([]byte(ts.Format("2006/01/02 15:04:05 "))) 231 | } 232 | if lvl, ok := fields[KeyLevel].(string); ok { 233 | levelColor := blue 234 | switch lvl { 235 | case "debug": 236 | levelColor = gray 237 | case "warn": 238 | levelColor = yellow 239 | case "error": 240 | levelColor = red 241 | } 242 | colorPrint(buf, strings.ToUpper(lvl[0:4]), levelColor) 243 | buf.WriteByte(' ') 244 | } 245 | if msg, ok := fields[KeyMessage].(string); ok { 246 | msg = strings.Replace(msg, "\n", "\\n", -1) 247 | buf.Write([]byte(msg)) 248 | } 249 | // Gather field keys 250 | keys := []string{} 251 | for k := range fields { 252 | switch k { 253 | case KeyLevel, KeyMessage, KeyTime: 254 | continue 255 | } 256 | keys = append(keys, k) 257 | } 258 | // Sort fields by key names 259 | sort.Strings(keys) 260 | // Print fields using logfmt format 261 | for _, k := range keys { 262 | buf.WriteByte(' ') 263 | colorPrint(buf, k, green) 264 | buf.WriteByte('=') 265 | if err := writeValue(buf, fields[k]); err != nil { 266 | return err 267 | } 268 | } 269 | buf.WriteByte('\n') 270 | _, err := o.w.Write(buf.Bytes()) 271 | return err 272 | } 273 | 274 | type logfmtOutput struct { 275 | w io.Writer 276 | } 277 | 278 | // NewLogfmtOutput returns a new output using logstash JSON schema v1 279 | func NewLogfmtOutput(w io.Writer) Output { 280 | return logfmtOutput{w: w} 281 | } 282 | 283 | func (o logfmtOutput) Write(fields map[string]interface{}) error { 284 | buf := bufPool.Get().(*bytes.Buffer) 285 | defer func() { 286 | buf.Reset() 287 | bufPool.Put(buf) 288 | }() 289 | // Gather field keys 290 | keys := []string{} 291 | for k := range fields { 292 | switch k { 293 | case KeyLevel, KeyMessage, KeyTime: 294 | continue 295 | } 296 | keys = append(keys, k) 297 | } 298 | // Sort fields by key names 299 | sort.Strings(keys) 300 | // Prepend default fields in a specific order 301 | keys = append([]string{KeyLevel, KeyMessage, KeyTime}, keys...) 302 | l := len(keys) 303 | for i, k := range keys { 304 | buf.Write([]byte(k)) 305 | buf.WriteByte('=') 306 | if err := writeValue(buf, fields[k]); err != nil { 307 | return err 308 | } 309 | if i+1 < l { 310 | buf.WriteByte(' ') 311 | } else { 312 | buf.WriteByte('\n') 313 | } 314 | } 315 | _, err := o.w.Write(buf.Bytes()) 316 | return err 317 | } 318 | 319 | // NewJSONOutput returns a new JSON output with the given writer. 320 | func NewJSONOutput(w io.Writer) Output { 321 | enc := json.NewEncoder(w) 322 | return OutputFunc(func(fields map[string]interface{}) error { 323 | return enc.Encode(fields) 324 | }) 325 | } 326 | 327 | // NewLogstashOutput returns an output to generate logstash friendly JSON format. 328 | func NewLogstashOutput(w io.Writer) Output { 329 | return OutputFunc(func(fields map[string]interface{}) error { 330 | lsf := map[string]interface{}{ 331 | "@version": 1, 332 | } 333 | for k, v := range fields { 334 | switch k { 335 | case KeyTime: 336 | k = "@timestamp" 337 | case KeyLevel: 338 | if s, ok := v.(string); ok { 339 | v = strings.ToUpper(s) 340 | } 341 | } 342 | if t, ok := v.(time.Time); ok { 343 | lsf[k] = t.Format(time.RFC3339) 344 | } else { 345 | lsf[k] = v 346 | } 347 | } 348 | b, err := json.Marshal(lsf) 349 | if err != nil { 350 | return err 351 | } 352 | _, err = w.Write(b) 353 | return err 354 | }) 355 | } 356 | 357 | // NewUIDOutput returns an output filter adding a globally unique id (using github.com/rs/xid) 358 | // to all message going thru this output. The o parameter defines the next output to pass data 359 | // to. 360 | func NewUIDOutput(field string, o Output) Output { 361 | return OutputFunc(func(fields map[string]interface{}) error { 362 | fields[field] = xid.New().String() 363 | return o.Write(fields) 364 | }) 365 | } 366 | 367 | // NewTrimOutput trims any field of type string with a value length greater than maxLen 368 | // to maxLen. 369 | func NewTrimOutput(maxLen int, o Output) Output { 370 | return OutputFunc(func(fields map[string]interface{}) error { 371 | for k, v := range fields { 372 | if s, ok := v.(string); ok && len(s) > maxLen { 373 | fields[k] = s[:maxLen] 374 | } 375 | } 376 | return o.Write(fields) 377 | }) 378 | } 379 | 380 | // NewTrimFieldsOutput trims listed field fields of type string with a value length greater than maxLen 381 | // to maxLen. 382 | func NewTrimFieldsOutput(trimFields []string, maxLen int, o Output) Output { 383 | return OutputFunc(func(fields map[string]interface{}) error { 384 | for _, f := range trimFields { 385 | if s, ok := fields[f].(string); ok && len(s) > maxLen { 386 | fields[f] = s[:maxLen] 387 | } 388 | } 389 | return o.Write(fields) 390 | }) 391 | } 392 | -------------------------------------------------------------------------------- /output_test.go: -------------------------------------------------------------------------------- 1 | package xlog 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type testOutput struct { 17 | err error 18 | w chan map[string]interface{} 19 | } 20 | 21 | func newTestOutput() *testOutput { 22 | return &testOutput{w: make(chan map[string]interface{}, 10)} 23 | } 24 | 25 | func newTestOutputErr(err error) *testOutput { 26 | return &testOutput{w: make(chan map[string]interface{}, 10), err: err} 27 | } 28 | 29 | func (o *testOutput) Write(fields map[string]interface{}) (err error) { 30 | o.w <- fields 31 | return o.err 32 | } 33 | 34 | func (o *testOutput) reset() { 35 | o.w = make(chan map[string]interface{}, 10) 36 | } 37 | 38 | func (o *testOutput) empty() bool { 39 | select { 40 | case <-o.w: 41 | return false 42 | default: 43 | return true 44 | } 45 | } 46 | 47 | func (o *testOutput) get() map[string]interface{} { 48 | select { 49 | case last := <-o.w: 50 | return last 51 | case <-time.After(2 * time.Second): 52 | return nil 53 | } 54 | } 55 | 56 | func TestOutputChannel(t *testing.T) { 57 | o := newTestOutput() 58 | oc := NewOutputChannel(o) 59 | defer oc.Close() 60 | oc.input <- F{"foo": "bar"} 61 | assert.Equal(t, F{"foo": "bar"}, F(o.get())) 62 | } 63 | 64 | func TestOutputChannelError(t *testing.T) { 65 | // Trigger error path 66 | r, w := io.Pipe() 67 | go func() { 68 | critialLoggerMux.Lock() 69 | defer critialLoggerMux.Unlock() 70 | oldCritialLogger := critialLogger 71 | critialLogger = log.New(w, "", 0) 72 | o := newTestOutputErr(errors.New("some error")) 73 | oc := NewOutputChannel(o) 74 | oc.input <- F{"foo": "bar"} 75 | o.get() 76 | oc.Close() 77 | critialLogger = oldCritialLogger 78 | w.Close() 79 | }() 80 | b, err := ioutil.ReadAll(r) 81 | assert.NoError(t, err) 82 | assert.Contains(t, string(b), "cannot write log message: some error") 83 | } 84 | 85 | func TestOutputChannelClose(t *testing.T) { 86 | oc := NewOutputChannel(newTestOutput()) 87 | defer oc.Close() 88 | assert.NotNil(t, oc.stop) 89 | oc.Close() 90 | assert.Nil(t, oc.stop) 91 | oc.Close() 92 | } 93 | 94 | func TestDiscard(t *testing.T) { 95 | assert.NoError(t, Discard.Write(F{})) 96 | } 97 | 98 | func TestMultiOutput(t *testing.T) { 99 | o1 := newTestOutput() 100 | o2 := newTestOutput() 101 | mo := MultiOutput{o1, o2} 102 | err := mo.Write(F{"foo": "bar"}) 103 | assert.NoError(t, err) 104 | assert.Equal(t, F{"foo": "bar"}, F(<-o1.w)) 105 | assert.Equal(t, F{"foo": "bar"}, F(<-o2.w)) 106 | } 107 | 108 | func TestMultiOutputWithError(t *testing.T) { 109 | o1 := newTestOutputErr(errors.New("some error")) 110 | o2 := newTestOutput() 111 | mo := MultiOutput{o1, o2} 112 | err := mo.Write(F{"foo": "bar"}) 113 | assert.EqualError(t, err, "some error") 114 | // Still send data to all outputs 115 | assert.Equal(t, F{"foo": "bar"}, F(<-o1.w)) 116 | assert.Equal(t, F{"foo": "bar"}, F(<-o2.w)) 117 | } 118 | 119 | func TestFilterOutput(t *testing.T) { 120 | o := newTestOutput() 121 | f := FilterOutput{ 122 | Cond: func(fields map[string]interface{}) bool { 123 | return fields["foo"] == "bar" 124 | }, 125 | Output: o, 126 | } 127 | err := f.Write(F{"foo": "bar"}) 128 | assert.NoError(t, err) 129 | assert.Equal(t, F{"foo": "bar"}, F(o.get())) 130 | 131 | o.reset() 132 | err = f.Write(F{"foo": "baz"}) 133 | assert.NoError(t, err) 134 | assert.True(t, o.empty()) 135 | 136 | f.Output = nil 137 | err = f.Write(F{"foo": "baz"}) 138 | assert.NoError(t, err) 139 | } 140 | 141 | func TestLevelOutput(t *testing.T) { 142 | oInfo := newTestOutput() 143 | oError := newTestOutput() 144 | oFatal := newTestOutput() 145 | oWarn := &testOutput{err: errors.New("some error")} 146 | reset := func() { 147 | oInfo.reset() 148 | oError.reset() 149 | oFatal.reset() 150 | oWarn.reset() 151 | } 152 | l := LevelOutput{ 153 | Info: oInfo, 154 | Error: oError, 155 | Fatal: oFatal, 156 | Warn: oWarn, 157 | } 158 | 159 | err := l.Write(F{"level": "fatal", "foo": "bar"}) 160 | assert.NoError(t, err) 161 | assert.True(t, oInfo.empty()) 162 | assert.True(t, oError.empty()) 163 | assert.Equal(t, F{"level": "fatal", "foo": "bar"}, F(<-oFatal.w)) 164 | assert.True(t, oWarn.empty()) 165 | 166 | reset() 167 | err = l.Write(F{"level": "error", "foo": "bar"}) 168 | assert.NoError(t, err) 169 | assert.True(t, oInfo.empty()) 170 | assert.Equal(t, F{"level": "error", "foo": "bar"}, F(<-oError.w)) 171 | assert.True(t, oFatal.empty()) 172 | assert.True(t, oWarn.empty()) 173 | 174 | reset() 175 | err = l.Write(F{"level": "info", "foo": "bar"}) 176 | assert.NoError(t, err) 177 | assert.Equal(t, F{"level": "info", "foo": "bar"}, F(<-oInfo.w)) 178 | assert.True(t, oFatal.empty()) 179 | assert.True(t, oError.empty()) 180 | assert.True(t, oWarn.empty()) 181 | 182 | reset() 183 | err = l.Write(F{"level": "warn", "foo": "bar"}) 184 | assert.EqualError(t, err, "some error") 185 | assert.True(t, oInfo.empty()) 186 | assert.True(t, oError.empty()) 187 | assert.True(t, oFatal.empty()) 188 | assert.Equal(t, F{"level": "warn", "foo": "bar"}, F(<-oWarn.w)) 189 | 190 | reset() 191 | err = l.Write(F{"level": "debug", "foo": "bar"}) 192 | assert.NoError(t, err) 193 | assert.True(t, oInfo.empty()) 194 | assert.True(t, oError.empty()) 195 | assert.True(t, oFatal.empty()) 196 | assert.True(t, oWarn.empty()) 197 | 198 | reset() 199 | err = l.Write(F{"foo": "bar"}) 200 | assert.NoError(t, err) 201 | assert.True(t, oInfo.empty()) 202 | assert.True(t, oError.empty()) 203 | assert.True(t, oFatal.empty()) 204 | assert.True(t, oWarn.empty()) 205 | } 206 | 207 | func TestSyslogOutput(t *testing.T) { 208 | buf := bytes.NewBuffer(nil) 209 | critialLoggerMux.Lock() 210 | oldCritialLogger := critialLogger 211 | critialLogger = log.New(buf, "", 0) 212 | defer func() { 213 | critialLogger = oldCritialLogger 214 | critialLoggerMux.Unlock() 215 | }() 216 | m := NewSyslogOutput("udp", "127.0.0.1:1234", "mytag") 217 | assert.IsType(t, LevelOutput{}, m) 218 | assert.Panics(t, func() { 219 | NewSyslogOutput("tcp", "an invalid host name", "mytag") 220 | }) 221 | assert.Regexp(t, "syslog dial error: dial tcp:.*missing port in address.*", buf.String()) 222 | } 223 | 224 | func TestRecorderOutput(t *testing.T) { 225 | o := RecorderOutput{} 226 | o.Write(F{"foo": "bar"}) 227 | o.Write(F{"bar": "baz"}) 228 | assert.Equal(t, []F{{"foo": "bar"}, {"bar": "baz"}}, o.Messages) 229 | o.Reset() 230 | assert.Equal(t, []F{}, o.Messages) 231 | } 232 | 233 | func TestNewConsoleOutput(t *testing.T) { 234 | old := isTerminal 235 | defer func() { isTerminal = old }() 236 | isTerminal = func(w io.Writer) bool { return true } 237 | c := NewConsoleOutput() 238 | if assert.IsType(t, consoleOutput{}, c) { 239 | assert.Equal(t, os.Stderr, c.(consoleOutput).w) 240 | } 241 | isTerminal = func(w io.Writer) bool { return false } 242 | c = NewConsoleOutput() 243 | if assert.IsType(t, logfmtOutput{}, c) { 244 | assert.Equal(t, os.Stderr, c.(logfmtOutput).w) 245 | } 246 | } 247 | 248 | func TestNewConsoleOutputW(t *testing.T) { 249 | b := bytes.NewBuffer([]byte{}) 250 | c := NewConsoleOutputW(b, NewLogfmtOutput(b)) 251 | assert.IsType(t, logfmtOutput{}, c) 252 | old := isTerminal 253 | defer func() { isTerminal = old }() 254 | isTerminal = func(w io.Writer) bool { return true } 255 | c = NewConsoleOutputW(b, NewLogfmtOutput(b)) 256 | if assert.IsType(t, consoleOutput{}, c) { 257 | assert.Equal(t, b, c.(consoleOutput).w) 258 | } 259 | } 260 | 261 | func TestConsoleOutput(t *testing.T) { 262 | buf := &bytes.Buffer{} 263 | c := consoleOutput{w: buf} 264 | err := c.Write(F{"message": "some message", "level": "info", "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), "foo": "bar"}) 265 | assert.NoError(t, err) 266 | assert.Equal(t, "2000/01/02 03:04:05 \x1b[34mINFO\x1b[0m some message \x1b[32mfoo\x1b[0m=bar\n", buf.String()) 267 | buf.Reset() 268 | err = c.Write(F{"message": "some debug", "level": "debug"}) 269 | assert.NoError(t, err) 270 | assert.Equal(t, "\x1b[37mDEBU\x1b[0m some debug\n", buf.String()) 271 | buf.Reset() 272 | err = c.Write(F{"message": "some warning", "level": "warn"}) 273 | assert.NoError(t, err) 274 | assert.Equal(t, "\x1b[33mWARN\x1b[0m some warning\n", buf.String()) 275 | buf.Reset() 276 | err = c.Write(F{"message": "some error", "level": "error"}) 277 | assert.NoError(t, err) 278 | assert.Equal(t, "\x1b[31mERRO\x1b[0m some error\n", buf.String()) 279 | } 280 | 281 | func TestLogfmtOutput(t *testing.T) { 282 | buf := &bytes.Buffer{} 283 | c := NewLogfmtOutput(buf) 284 | err := c.Write(F{ 285 | "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), 286 | "message": "some message", 287 | "level": "info", 288 | "string": "foo", 289 | "null": nil, 290 | "quoted": "needs \" quotes", 291 | "err": errors.New("error"), 292 | "errq": errors.New("error with \" quote"), 293 | }) 294 | assert.NoError(t, err) 295 | assert.Equal(t, "level=info message=\"some message\" time=\"2000-01-02 03:04:05 +0000 UTC\" err=error errq=\"error with \\\" quote\" null=null quoted=\"needs \\\" quotes\" string=foo\n", buf.String()) 296 | } 297 | 298 | func TestJSONOutput(t *testing.T) { 299 | buf := &bytes.Buffer{} 300 | j := NewJSONOutput(buf) 301 | err := j.Write(F{"message": "some message", "level": "info", "foo": "bar"}) 302 | assert.NoError(t, err) 303 | assert.Equal(t, "{\"foo\":\"bar\",\"level\":\"info\",\"message\":\"some message\"}\n", buf.String()) 304 | } 305 | 306 | func TestLogstashOutput(t *testing.T) { 307 | buf := &bytes.Buffer{} 308 | o := NewLogstashOutput(buf) 309 | err := o.Write(F{ 310 | "message": "some message", 311 | "level": "info", 312 | "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), 313 | "file": "test.go:234", 314 | "foo": "bar", 315 | }) 316 | assert.NoError(t, err) 317 | assert.Equal(t, "{\"@timestamp\":\"2000-01-02T03:04:05Z\",\"@version\":1,\"file\":\"test.go:234\",\"foo\":\"bar\",\"level\":\"INFO\",\"message\":\"some message\"}", buf.String()) 318 | } 319 | 320 | func TestUIDOutput(t *testing.T) { 321 | o := newTestOutput() 322 | i := NewUIDOutput("id", o) 323 | err := i.Write(F{"message": "some message", "level": "info", "foo": "bar"}) 324 | last := o.get() 325 | assert.NoError(t, err) 326 | assert.NotNil(t, last["id"]) 327 | assert.Len(t, last["id"], 20) 328 | } 329 | 330 | func TestTrimOutput(t *testing.T) { 331 | o := newTestOutput() 332 | i := NewTrimOutput(10, o) 333 | err := i.Write(F{"short": "short", "long": "too long message", "number": 20}) 334 | last := o.get() 335 | assert.NoError(t, err) 336 | assert.Equal(t, "short", last["short"]) 337 | assert.Equal(t, "too long m", last["long"]) 338 | assert.Equal(t, 20, last["number"]) 339 | } 340 | 341 | func TestTrimFieldsOutput(t *testing.T) { 342 | o := newTestOutput() 343 | i := NewTrimFieldsOutput([]string{"short", "trim", "number"}, 10, o) 344 | err := i.Write(F{"short": "short", "long": "too long message", "trim": "too long message", "number": 20}) 345 | last := o.get() 346 | assert.NoError(t, err) 347 | assert.Equal(t, "short", last["short"]) 348 | assert.Equal(t, "too long m", last["trim"]) 349 | assert.Equal(t, "too long message", last["long"]) 350 | assert.Equal(t, 20, last["number"]) 351 | } 352 | -------------------------------------------------------------------------------- /xlog.go: -------------------------------------------------------------------------------- 1 | // Package xlog is a logger coupled with HTTP net/context aware middleware. 2 | // 3 | // Unlike most loggers, xlog will never block your application because one its 4 | // outputs is lagging. The log commands are connected to their outputs through 5 | // a buffered channel and will prefer to discard messages if the buffer get full. 6 | // All message formatting, serialization and transport happen in a dedicated go 7 | // routine. 8 | // 9 | // Features: 10 | // 11 | // - Per request log context 12 | // - Per request and/or per message key/value fields 13 | // - Log levels (Debug, Info, Warn, Error) 14 | // - Color output when terminal is detected 15 | // - Custom output (JSON, logfmt, …) 16 | // - Automatic gathering of request context like User-Agent, IP etc. 17 | // - Drops message rather than blocking execution 18 | // - Easy access logging thru github.com/rs/xaccess 19 | // 20 | // It works best in combination with github.com/rs/xhandler. 21 | package xlog // import "github.com/rs/xlog" 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "log" 27 | "os" 28 | "path" 29 | "runtime" 30 | "strconv" 31 | "strings" 32 | "sync" 33 | "time" 34 | ) 35 | 36 | // Logger defines the interface for a xlog compatible logger 37 | type Logger interface { 38 | // Implements io.Writer so it can be set a output of log.Logger 39 | io.Writer 40 | 41 | // SetField sets a field on the logger's context. All future messages on this logger 42 | // will have this field set. 43 | SetField(name string, value interface{}) 44 | // GetFields returns all the fields set on the logger 45 | GetFields() F 46 | // Debug logs a debug message. If last parameter is a map[string]string, it's content 47 | // is added as fields to the message. 48 | Debug(v ...interface{}) 49 | // Debug logs a debug message with format. If last parameter is a map[string]string, 50 | // it's content is added as fields to the message. 51 | Debugf(format string, v ...interface{}) 52 | // Info logs a info message. If last parameter is a map[string]string, it's content 53 | // is added as fields to the message. 54 | Info(v ...interface{}) 55 | // Info logs a info message with format. If last parameter is a map[string]string, 56 | // it's content is added as fields to the message. 57 | Infof(format string, v ...interface{}) 58 | // Warn logs a warning message. If last parameter is a map[string]string, it's content 59 | // is added as fields to the message. 60 | Warn(v ...interface{}) 61 | // Warn logs a warning message with format. If last parameter is a map[string]string, 62 | // it's content is added as fields to the message. 63 | Warnf(format string, v ...interface{}) 64 | // Error logs an error message. If last parameter is a map[string]string, it's content 65 | // is added as fields to the message. 66 | Error(v ...interface{}) 67 | // Error logs an error message with format. If last parameter is a map[string]string, 68 | // it's content is added as fields to the message. 69 | Errorf(format string, v ...interface{}) 70 | // Fatal logs an error message followed by a call to os.Exit(1). If last parameter is a 71 | // map[string]string, it's content is added as fields to the message. 72 | Fatal(v ...interface{}) 73 | // Fatalf logs an error message with format followed by a call to ox.Exit(1). If last 74 | // parameter is a map[string]string, it's content is added as fields to the message. 75 | Fatalf(format string, v ...interface{}) 76 | // Output mimics std logger interface 77 | Output(calldepth int, s string) error 78 | // OutputF outputs message with fields. 79 | OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) 80 | } 81 | 82 | // LoggerCopier defines a logger with copy support 83 | type LoggerCopier interface { 84 | // Copy returns a copy of the logger 85 | Copy() Logger 86 | } 87 | 88 | // Config defines logger's configuration 89 | type Config struct { 90 | // Level is the maximum level to output, logs with lower level are discarded. 91 | Level Level 92 | // Fields defines default fields to use with all messages. 93 | Fields map[string]interface{} 94 | // Output to use to write log messages to. 95 | // 96 | // You should always wrap your output with an OutputChannel otherwise your 97 | // logger will be connected to its output synchronously. 98 | Output Output 99 | // DisablePooling removes the use of a sync.Pool for cases where logger 100 | // instances are needed beyond the scope of a request handler. This option 101 | // puts a greater pressure on GC and increases the amount of memory allocated 102 | // and freed. Use only if persistent loggers are a requirement. 103 | DisablePooling bool 104 | } 105 | 106 | // F represents a set of log message fields 107 | type F map[string]interface{} 108 | 109 | type logger struct { 110 | level Level 111 | output Output 112 | fields F 113 | disablePooling bool 114 | } 115 | 116 | // Common field names for log messages. 117 | var ( 118 | KeyTime = "time" 119 | KeyMessage = "message" 120 | KeyLevel = "level" 121 | KeyFile = "file" 122 | ) 123 | 124 | var now = time.Now 125 | var exit1 = func() { os.Exit(1) } 126 | 127 | // critialLogger is a logger to use when xlog is not able to deliver a message 128 | var critialLogger = log.New(os.Stderr, "xlog: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile) 129 | 130 | var loggerPool = &sync.Pool{ 131 | New: func() interface{} { 132 | return &logger{} 133 | }, 134 | } 135 | 136 | // New manually creates a logger. 137 | // 138 | // This function should only be used out of a request. Use FromContext in request. 139 | func New(c Config) Logger { 140 | var l *logger 141 | if c.DisablePooling { 142 | l = &logger{} 143 | } else { 144 | l = loggerPool.Get().(*logger) 145 | } 146 | l.level = c.Level 147 | l.output = c.Output 148 | if l.output == nil { 149 | l.output = NewOutputChannel(NewConsoleOutput()) 150 | } 151 | for k, v := range c.Fields { 152 | l.SetField(k, v) 153 | } 154 | l.disablePooling = c.DisablePooling 155 | return l 156 | } 157 | 158 | // Copy returns a copy of the passed logger if the logger implements 159 | // LoggerCopier or the NopLogger otherwise. 160 | func Copy(l Logger) Logger { 161 | if l, ok := l.(LoggerCopier); ok { 162 | return l.Copy() 163 | } 164 | return NopLogger 165 | } 166 | 167 | // Copy returns a copy of the logger 168 | func (l *logger) Copy() Logger { 169 | l2 := &logger{ 170 | level: l.level, 171 | output: l.output, 172 | fields: map[string]interface{}{}, 173 | disablePooling: l.disablePooling, 174 | } 175 | for k, v := range l.fields { 176 | l2.fields[k] = v 177 | } 178 | return l2 179 | } 180 | 181 | // close returns the logger to the pool for reuse 182 | func (l *logger) close() { 183 | if !l.disablePooling { 184 | l.level = 0 185 | l.output = nil 186 | l.fields = nil 187 | loggerPool.Put(l) 188 | } 189 | } 190 | 191 | func (l *logger) send(level Level, calldepth int, msg string, fields map[string]interface{}) { 192 | if level < l.level || l.output == nil { 193 | return 194 | } 195 | data := make(map[string]interface{}, 4+len(fields)+len(l.fields)) 196 | data[KeyTime] = now() 197 | data[KeyLevel] = level.String() 198 | data[KeyMessage] = msg 199 | if _, file, line, ok := runtime.Caller(calldepth); ok { 200 | data[KeyFile] = path.Base(file) + ":" + strconv.FormatInt(int64(line), 10) 201 | } 202 | for k, v := range fields { 203 | data[k] = v 204 | } 205 | if l.fields != nil { 206 | for k, v := range l.fields { 207 | data[k] = v 208 | } 209 | } 210 | if err := l.output.Write(data); err != nil { 211 | critialLogger.Print("send error: ", err.Error()) 212 | } 213 | } 214 | 215 | func extractFields(v *[]interface{}) map[string]interface{} { 216 | if l := len(*v); l > 0 { 217 | if f, ok := (*v)[l-1].(map[string]interface{}); ok { 218 | *v = (*v)[:l-1] 219 | return f 220 | } 221 | if f, ok := (*v)[l-1].(F); ok { 222 | *v = (*v)[:l-1] 223 | return f 224 | } 225 | } 226 | return nil 227 | } 228 | 229 | // SetField implements Logger interface 230 | func (l *logger) SetField(name string, value interface{}) { 231 | if l.fields == nil { 232 | l.fields = map[string]interface{}{} 233 | } 234 | l.fields[name] = value 235 | } 236 | 237 | // GetFields implements Logger interface 238 | func (l *logger) GetFields() F { 239 | return l.fields 240 | } 241 | 242 | // Output implements Logger interface 243 | func (l *logger) OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) { 244 | l.send(level, calldepth+1, msg, fields) 245 | } 246 | 247 | // Debug implements Logger interface 248 | func (l *logger) Debug(v ...interface{}) { 249 | f := extractFields(&v) 250 | l.send(LevelDebug, 2, fmt.Sprint(v...), f) 251 | } 252 | 253 | // Debugf implements Logger interface 254 | func (l *logger) Debugf(format string, v ...interface{}) { 255 | f := extractFields(&v) 256 | l.send(LevelDebug, 2, fmt.Sprintf(format, v...), f) 257 | } 258 | 259 | // Info implements Logger interface 260 | func (l *logger) Info(v ...interface{}) { 261 | f := extractFields(&v) 262 | l.send(LevelInfo, 2, fmt.Sprint(v...), f) 263 | } 264 | 265 | // Infof implements Logger interface 266 | func (l *logger) Infof(format string, v ...interface{}) { 267 | f := extractFields(&v) 268 | l.send(LevelInfo, 2, fmt.Sprintf(format, v...), f) 269 | } 270 | 271 | // Warn implements Logger interface 272 | func (l *logger) Warn(v ...interface{}) { 273 | f := extractFields(&v) 274 | l.send(LevelWarn, 2, fmt.Sprint(v...), f) 275 | } 276 | 277 | // Warnf implements Logger interface 278 | func (l *logger) Warnf(format string, v ...interface{}) { 279 | f := extractFields(&v) 280 | l.send(LevelWarn, 2, fmt.Sprintf(format, v...), f) 281 | } 282 | 283 | // Error implements Logger interface 284 | func (l *logger) Error(v ...interface{}) { 285 | f := extractFields(&v) 286 | l.send(LevelError, 2, fmt.Sprint(v...), f) 287 | } 288 | 289 | // Errorf implements Logger interface 290 | // 291 | // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last 292 | // argument to workaround go vet false alarm. 293 | func (l *logger) Errorf(format string, v ...interface{}) { 294 | f := extractFields(&v) 295 | if f != nil { 296 | // Let user add a %v at the end of the message when fields are passed to satisfy go vet 297 | l := len(format) 298 | if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { 299 | format = format[0 : l-2] 300 | } 301 | } 302 | l.send(LevelError, 2, fmt.Sprintf(format, v...), f) 303 | } 304 | 305 | // Fatal implements Logger interface 306 | func (l *logger) Fatal(v ...interface{}) { 307 | f := extractFields(&v) 308 | l.send(LevelFatal, 2, fmt.Sprint(v...), f) 309 | if o, ok := l.output.(*OutputChannel); ok { 310 | o.Close() 311 | } 312 | exit1() 313 | } 314 | 315 | // Fatalf implements Logger interface 316 | // 317 | // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last 318 | // argument to workaround go vet false alarm. 319 | func (l *logger) Fatalf(format string, v ...interface{}) { 320 | f := extractFields(&v) 321 | if f != nil { 322 | // Let user add a %v at the end of the message when fields are passed to satisfy go vet 323 | l := len(format) 324 | if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { 325 | format = format[0 : l-2] 326 | } 327 | } 328 | l.send(LevelFatal, 2, fmt.Sprintf(format, v...), f) 329 | if o, ok := l.output.(*OutputChannel); ok { 330 | o.Close() 331 | } 332 | exit1() 333 | } 334 | 335 | // Write implements io.Writer interface 336 | func (l *logger) Write(p []byte) (int, error) { 337 | msg := strings.TrimRight(string(p), "\n") 338 | l.send(LevelInfo, 4, msg, nil) 339 | if o, ok := l.output.(*OutputChannel); ok { 340 | o.Flush() 341 | } 342 | return len(p), nil 343 | } 344 | 345 | // Output implements common logger interface 346 | func (l *logger) Output(calldepth int, s string) error { 347 | l.send(LevelInfo, 2, s, nil) 348 | return nil 349 | } 350 | --------------------------------------------------------------------------------