Repository: rs/xlog Branch: master Commit: 131980fab91b Files: 34 Total size: 91.2 KB Directory structure: gitextract_0x8cvxva/ ├── .travis.yml ├── LICENSE ├── README.md ├── handler.go ├── handler_examples_test.go ├── handler_pre17.go ├── handler_pre17_test.go ├── handler_test.go ├── internal/ │ └── term/ │ ├── LICENSE │ ├── term.go │ ├── term_appengine.go │ ├── term_darwin.go │ ├── term_freebsd.go │ ├── term_linux.go │ ├── term_notwindows.go │ ├── term_openbsd.go │ └── term_windows.go ├── levels.go ├── levels_test.go ├── nop.go ├── nop_test.go ├── output.go ├── output_examples_test.go ├── output_syslog.go ├── output_test.go ├── std.go ├── std_example_test.go ├── std_test.go ├── util.go ├── util_test.go ├── xlog.go ├── xlog_bench_test.go ├── xlog_examples_test.go └── xlog_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .travis.yml ================================================ language: go go: - 1.7 - 1.8 - tip matrix: allow_failures: - go: tip script: go test -v -race -cpu=1,2,4 ./... ================================================ FILE: LICENSE ================================================ Copyright (c) 2015 Olivier Poitrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ :warning: **Check [zerolog](https://github.com/rs/zerolog), the successor of xlog.** # HTTP Handler Logger [![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) `xlog` is a logger for [net/context](https://godoc.org/golang.org/x/net/context) aware HTTP applications. 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. Read more about `xlog` on [Dailymotion engineering blog](http://engineering.dailymotion.com/our-way-to-go/). ![](screenshot.png) ## Features - Per request log context - Per request and/or per message key/value fields - Log levels (Debug, Info, Warn, Error) - Color output when terminal is detected - Custom output (JSON, [logfmt](https://github.com/kr/logfmt), …) - Automatic gathering of request context like User-Agent, IP etc. - Drops message rather than blocking execution - Easy access logging thru [github.com/rs/xaccess](https://github.com/rs/xaccess) 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). ## Install go get github.com/rs/xlog ## Usage ```go c := alice.New() host, _ := os.Hostname() conf := xlog.Config{ // Log info level and higher Level: xlog.LevelInfo, // Set some global env fields Fields: xlog.F{ "role": "my-service", "host": host, }, // Output everything on console Output: xlog.NewOutputChannel(xlog.NewConsoleOutput()), } // Install the logger handler c = c.Append(xlog.NewHandler(conf)) // Optionally plug the xlog handler's input to Go's default logger log.SetFlags(0) xlogger := xlog.New(conf) log.SetOutput(xlogger) // Install some provided extra handler to set some request's context fields. // Thanks to those handler, all our logs will come with some pre-populated fields. c = c.Append(xlog.MethodHandler("method")) c = c.Append(xlog.URLHandler("url")) c = c.Append(xlog.RemoteAddrHandler("ip")) c = c.Append(xlog.UserAgentHandler("user_agent")) c = c.Append(xlog.RefererHandler("referer")) c = c.Append(xlog.RequestIDHandler("req_id", "Request-Id")) // Here is your final handler h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get the logger from the request's context. You can safely assume it // will be always there: if the handler is removed, xlog.FromContext // will return a NopLogger l := xlog.FromRequest(r) // Then log some errors if err := errors.New("some error from elsewhere"); err != nil { l.Errorf("Here is an error: %v", err) } // Or some info with fields l.Info("Something happend", xlog.F{ "user": "current user id", "status": "ok", }) // Output: // { // "message": "Something happend", // "level": "info", // "file": "main.go:34", // "time": time.Time{...}, // "user": "current user id", // "status": "ok", // "ip": "1.2.3.4", // "user-agent": "Mozilla/1.2.3...", // "referer": "http://somewhere.com/path", // "role": "my-service", // "host": "somehost" // } })) http.Handle("/", h) if err := http.ListenAndServe(":8080", nil); err != nil { xlogger.Fatal(err) } ``` ### Copy Logger You may want to get a copy of the current logger to pass a modified version to a function without touching the original: ```go l := xlog.FromContext(ctx) l2 := xlog.Copy(l) l2.SetField("foo", "bar") ``` 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: ```go l := xlog.FromContext(ctx) l2 := xlog.Copy(l) go func() { // use the safe copy l2.Info("something") }() ``` ### Global Logger You may use the standard Go logger and plug `xlog` as it's output as `xlog` implements `io.Writer`: ```go xlogger := xlog.New(conf) log.SetOutput(xlogger) ``` 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. Another option for code you manage but which is outside of a HTTP request handler is to use the `xlog` provided default logger: ```go xlog.Debugf("some message with %s", variable, xlog.F{"and": "field support"}) ``` 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: ```go xlog.SetLogger(xlog.New(xlog.Config{ Level: xlog.LevelInfo, Output: xlog.NewConsoleOutput(), Fields: xlog.F{ "role": "my-service", }, })) ``` ### Configure Output 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. 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. ```go conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.MultiOutput{ // Send all logs with field type=mymodule to a remote syslog 0: xlog.FilterOutput{ Cond: func(fields map[string]interface{}) bool { return fields["type"] == "mymodule" }, Output: xlog.NewSyslogOutput("tcp", "1.2.3.4:1234", "mymodule"), }, // Setup different output per log level 1: xlog.LevelOutput{ // Send errors to the console Error: xlog.NewConsoleOutput(), // Send syslog output for error level Info: xlog.NewSyslogOutput("", "", ""), }, }), }) h = xlog.NewHandler(conf) ``` #### Built-in Output Modules | Name | Description | |------|-------------| | [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. | [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. | [FilterOutput](https://godoc.org/github.com/rs/xlog#FilterOutput) | Tests a condition on the message and forward it to the child output if true. | [LevelOutput](https://godoc.org/github.com/rs/xlog#LevelOutput) | Routes messages per level outputs. | [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. | [JSONOutput](https://godoc.org/github.com/rs/xlog#NewJSONOutput) | Serialize messages in JSON. | [LogfmtOutput](https://godoc.org/github.com/rs/xlog#NewLogfmtOutput) | Serialize messages using Heroku like [logfmt](https://github.com/kr/logfmt). | [LogstashOutput](https://godoc.org/github.com/rs/xlog#NewLogstashOutput) | Serialize JSON message using Logstash 2.0 (schema v1) structured format. | [SyslogOutput](https://godoc.org/github.com/rs/xlog#NewSyslogOutput) | Send messages to syslog. | [UIDOutput](https://godoc.org/github.com/rs/xlog#NewUIDOutput) | Append a globally unique id to every message and forward it to the next output. ## Third Party Extensions | Project | Author | Description | |---------|--------|-------------| | [gRPClog](https://github.com/clawio/grpcxlog) | [Hugo González Labrador](https://github.com/labkode) | An adapter to use xlog as the logger for grpclog. | [xlog-nsq](https://github.com/rs/xlog-nsq) | [Olivier Poitrey](https://github.com/rs) | An xlog to [NSQ](http://nsq.io) output. | [xlog-sentry](https://github.com/trong/xlog-sentry) | [trong](https://github.com/trong) | An xlog to [Sentry](https://getsentry.com/) output. ## Licenses All source code is licensed under the [MIT License](https://raw.github.com/rs/xlog/master/LICENSE). ================================================ FILE: handler.go ================================================ // +build go1.7 package xlog import ( "context" "net" "net/http" "github.com/rs/xid" ) type key int const ( logKey key = iota idKey ) // IDFromContext returns the unique id associated to the request if any. func IDFromContext(ctx context.Context) (xid.ID, bool) { id, ok := ctx.Value(idKey).(xid.ID) return id, ok } // IDFromRequest returns the unique id accociated to the request if any. func IDFromRequest(r *http.Request) (xid.ID, bool) { if r == nil { return xid.ID{}, false } return IDFromContext(r.Context()) } // FromContext gets the logger out of the context. // If not logger is stored in the context, a NopLogger is returned. func FromContext(ctx context.Context) Logger { if ctx == nil { return NopLogger } l, ok := ctx.Value(logKey).(Logger) if !ok { return NopLogger } return l } // FromRequest gets the logger in the request's context. // This is a shortcut for xlog.FromContext(r.Context()) func FromRequest(r *http.Request) Logger { if r == nil { return NopLogger } return FromContext(r.Context()) } // NewContext returns a copy of the parent context and associates it with the provided logger. func NewContext(ctx context.Context, l Logger) context.Context { return context.WithValue(ctx, logKey, l) } // NewHandler instanciates a new xlog HTTP handler. // // If not configured, the output is set to NewConsoleOutput() by default. func NewHandler(c Config) func(http.Handler) http.Handler { if c.Output == nil { c.Output = NewOutputChannel(NewConsoleOutput()) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var l Logger if r != nil { l = New(c) r = r.WithContext(NewContext(r.Context(), l)) } next.ServeHTTP(w, r) if l, ok := l.(*logger); ok { l.close() } }) } } // URLHandler returns a handler setting the request's URL as a field // to the current context's logger using the passed name as field name. func URLHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromContext(r.Context()) l.SetField(name, r.URL.String()) next.ServeHTTP(w, r) }) } } // MethodHandler returns a handler setting the request's method as a field // to the current context's logger using the passed name as field name. func MethodHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromContext(r.Context()) l.SetField(name, r.Method) next.ServeHTTP(w, r) }) } } // RequestHandler returns a handler setting the request's method and URL as a field // to the current context's logger using the passed name as field name. func RequestHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromContext(r.Context()) l.SetField(name, r.Method+" "+r.URL.String()) next.ServeHTTP(w, r) }) } } // RemoteAddrHandler returns a handler setting the request's remote address as a field // to the current context's logger using the passed name as field name. func RemoteAddrHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { l := FromContext(r.Context()) l.SetField(name, host) } next.ServeHTTP(w, r) }) } } // UserAgentHandler returns a handler setting the request's client's user-agent as // a field to the current context's logger using the passed name as field name. func UserAgentHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ua := r.Header.Get("User-Agent"); ua != "" { l := FromContext(r.Context()) l.SetField(name, ua) } next.ServeHTTP(w, r) }) } } // RefererHandler returns a handler setting the request's referer header as // a field to the current context's logger using the passed name as field name. func RefererHandler(name string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ref := r.Header.Get("Referer"); ref != "" { l := FromContext(r.Context()) l.SetField(name, ref) } next.ServeHTTP(w, r) }) } } // RequestIDHandler returns a handler setting a unique id to the request which can // be gathered using IDFromContext(ctx). This generated id is added as a field to the // logger using the passed name as field name. The id is also added as a response // header if the headerName is not empty. // // The generated id is a URL safe base64 encoded mongo object-id-like unique id. // Mongo unique id generation algorithm has been selected as a trade-off between // size and ease of use: UUID is less space efficient and snowflake requires machine // configuration. func RequestIDHandler(name, headerName string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, ok := IDFromContext(ctx) if !ok { id = xid.New() ctx = context.WithValue(ctx, idKey, id) r = r.WithContext(ctx) } if name != "" { FromContext(ctx).SetField(name, id) } if headerName != "" { w.Header().Set(headerName, id.String()) } next.ServeHTTP(w, r) }) } } ================================================ FILE: handler_examples_test.go ================================================ // +build go1.7 package xlog_test import ( "errors" "log" "net/http" "os" "github.com/justinas/alice" "github.com/rs/xlog" ) func Example_handler() { c := alice.New() host, _ := os.Hostname() conf := xlog.Config{ // Set some global env fields Fields: xlog.F{ "role": "my-service", "host": host, }, } // Install the logger handler with default output on the console c = c.Append(xlog.NewHandler(conf)) // Plug the xlog handler's input to Go's default logger log.SetFlags(0) log.SetOutput(xlog.New(conf)) // Install some provided extra handler to set some request's context fields. // Thanks to those handler, all our logs will come with some pre-populated fields. c = c.Append(xlog.RemoteAddrHandler("ip")) c = c.Append(xlog.UserAgentHandler("user_agent")) c = c.Append(xlog.RefererHandler("referer")) c = c.Append(xlog.RequestIDHandler("req_id", "Request-Id")) // Here is your final handler h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get the logger from the request's context. You can safely assume it // will be always there: if the handler is removed, xlog.FromContext // will return a NopLogger l := xlog.FromRequest(r) // Then log some errors if err := errors.New("some error from elsewhere"); err != nil { l.Errorf("Here is an error: %v", err) } // Or some info with fields l.Info("Something happend", xlog.F{ "user": "current user id", "status": "ok", }) })) http.Handle("/", h) if err := http.ListenAndServe(":8080", nil); err != nil { log.SetOutput(os.Stderr) // make sure we print to console log.Fatal(err) } } ================================================ FILE: handler_pre17.go ================================================ // +build !go1.7 package xlog import ( "net" "net/http" "github.com/rs/xhandler" "github.com/rs/xid" "golang.org/x/net/context" ) type key int const ( logKey key = iota idKey ) // IDFromContext returns the unique id associated to the request if any. func IDFromContext(ctx context.Context) (xid.ID, bool) { id, ok := ctx.Value(idKey).(xid.ID) return id, ok } // FromContext gets the logger out of the context. // If not logger is stored in the context, a NopLogger is returned. func FromContext(ctx context.Context) Logger { if ctx == nil { return NopLogger } l, ok := ctx.Value(logKey).(Logger) if !ok { return NopLogger } return l } // NewContext returns a copy of the parent context and associates it with the provided logger. func NewContext(ctx context.Context, l Logger) context.Context { return context.WithValue(ctx, logKey, l) } // NewHandler instanciates a new xlog HTTP handler. // // If not configured, the output is set to NewConsoleOutput() by default. func NewHandler(c Config) func(xhandler.HandlerC) xhandler.HandlerC { if c.Output == nil { c.Output = NewOutputChannel(NewConsoleOutput()) } return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := New(c) ctx = NewContext(ctx, l) next.ServeHTTPC(ctx, w, r) if l, ok := l.(*logger); ok { l.close() } }) } } // URLHandler returns a handler setting the request's URL as a field // to the current context's logger using the passed name as field name. func URLHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { FromContext(ctx).SetField(name, r.URL.String()) next.ServeHTTPC(ctx, w, r) }) } } // MethodHandler returns a handler setting the request's method as a field // to the current context's logger using the passed name as field name. func MethodHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { FromContext(ctx).SetField(name, r.Method) next.ServeHTTPC(ctx, w, r) }) } } // RequestHandler returns a handler setting the request's method and URL as a field // to the current context's logger using the passed name as field name. func RequestHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { FromContext(ctx).SetField(name, r.Method+" "+r.URL.String()) next.ServeHTTPC(ctx, w, r) }) } } // RemoteAddrHandler returns a handler setting the request's remote address as a field // to the current context's logger using the passed name as field name. func RemoteAddrHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { FromContext(ctx).SetField(name, host) } next.ServeHTTPC(ctx, w, r) }) } } // UserAgentHandler returns a handler setting the request's client's user-agent as // a field to the current context's logger using the passed name as field name. func UserAgentHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { if ua := r.Header.Get("User-Agent"); ua != "" { FromContext(ctx).SetField(name, ua) } next.ServeHTTPC(ctx, w, r) }) } } // RefererHandler returns a handler setting the request's referer header as // a field to the current context's logger using the passed name as field name. func RefererHandler(name string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { if ref := r.Header.Get("Referer"); ref != "" { FromContext(ctx).SetField(name, ref) } next.ServeHTTPC(ctx, w, r) }) } } // RequestIDHandler returns a handler setting a unique id to the request which can // be gathered using IDFromContext(ctx). This generated id is added as a field to the // logger using the passed name as field name. The id is also added as a response // header if the headerName is not empty. // // The generated id is a URL safe base64 encoded mongo object-id-like unique id. // Mongo unique id generation algorithm has been selected as a trade-off between // size and ease of use: UUID is less space efficient and snowflake requires machine // configuration. func RequestIDHandler(name, headerName string) func(next xhandler.HandlerC) xhandler.HandlerC { return func(next xhandler.HandlerC) xhandler.HandlerC { return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { id, ok := IDFromContext(ctx) if !ok { id = xid.New() ctx = context.WithValue(ctx, idKey, id) } if name != "" { FromContext(ctx).SetField(name, id) } if headerName != "" { w.Header().Set(headerName, id.String()) } next.ServeHTTPC(ctx, w, r) }) } } ================================================ FILE: handler_pre17_test.go ================================================ // +build !go1.7 package xlog import ( "net/http" "net/http/httptest" "net/url" "testing" "github.com/rs/xhandler" "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) func TestFromContext(t *testing.T) { assert.Equal(t, NopLogger, FromContext(nil)) assert.Equal(t, NopLogger, FromContext(context.Background())) l := &logger{} ctx := NewContext(context.Background(), l) assert.Equal(t, l, FromContext(ctx)) } func TestNewHandler(t *testing.T) { c := Config{ Level: LevelInfo, Fields: F{"foo": "bar"}, Output: NewOutputChannel(&testOutput{}), } lh := NewHandler(c) h := lh(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx) assert.NotNil(t, l) assert.NotEqual(t, NopLogger, l) if l, ok := l.(*logger); assert.True(t, ok) { assert.Equal(t, LevelInfo, l.level) assert.Equal(t, c.Output, l.output) assert.Equal(t, F{"foo": "bar"}, F(l.fields)) } })) h.ServeHTTPC(context.Background(), nil, nil) } func TestURLHandler(t *testing.T) { r := &http.Request{ URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, } h := URLHandler("url")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"url": "/path?foo=bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestMethodHandler(t *testing.T) { r := &http.Request{ Method: "POST", } h := MethodHandler("method")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"method": "POST"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestRequestHandler(t *testing.T) { r := &http.Request{ Method: "POST", URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, } h := RequestHandler("request")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"request": "POST /path?foo=bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestRemoteAddrHandler(t *testing.T) { r := &http.Request{ RemoteAddr: "1.2.3.4:1234", } h := RemoteAddrHandler("ip")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"ip": "1.2.3.4"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestRemoteAddrHandlerIPv6(t *testing.T) { r := &http.Request{ RemoteAddr: "[2001:db8:a0b:12f0::1]:1234", } h := RemoteAddrHandler("ip")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"ip": "2001:db8:a0b:12f0::1"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestUserAgentHandler(t *testing.T) { r := &http.Request{ Header: http.Header{ "User-Agent": []string{"some user agent string"}, }, } h := UserAgentHandler("ua")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"ua": "some user agent string"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestRefererHandler(t *testing.T) { r := &http.Request{ Header: http.Header{ "Referer": []string{"http://foo.com/bar"}, }, } h := RefererHandler("ua")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) assert.Equal(t, F{"ua": "http://foo.com/bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTPC(context.Background(), nil, r) } func TestRequestIDHandler(t *testing.T) { r := &http.Request{} h := RequestIDHandler("id", "Request-Id")(xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { l := FromContext(ctx).(*logger) if id, ok := IDFromContext(ctx); assert.True(t, ok) { assert.Equal(t, l.fields["id"], id) assert.Len(t, id.String(), 20) assert.Equal(t, id.String(), w.Header().Get("Request-Id")) } assert.Len(t, l.fields["id"], 12) })) h = NewHandler(Config{})(h) w := httptest.NewRecorder() h.ServeHTTPC(context.Background(), w, r) } ================================================ FILE: handler_test.go ================================================ // +build go1.7 package xlog import ( "context" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" ) func TestFromContext(t *testing.T) { assert.Equal(t, NopLogger, FromContext(nil)) assert.Equal(t, NopLogger, FromContext(context.Background())) l := &logger{} ctx := NewContext(context.Background(), l) assert.Equal(t, l, FromContext(ctx)) } func TestNewHandler(t *testing.T) { c := Config{ Level: LevelInfo, Fields: F{"foo": "bar"}, Output: NewOutputChannel(&testOutput{}), } lh := NewHandler(c) h := lh(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r) assert.NotNil(t, l) assert.NotEqual(t, NopLogger, l) if l, ok := l.(*logger); assert.True(t, ok) { assert.Equal(t, LevelInfo, l.level) assert.Equal(t, c.Output, l.output) assert.Equal(t, F{"foo": "bar"}, F(l.fields)) } })) h.ServeHTTP(nil, &http.Request{}) } func TestURLHandler(t *testing.T) { r := &http.Request{ URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, } h := URLHandler("url")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"url": "/path?foo=bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestMethodHandler(t *testing.T) { r := &http.Request{ Method: "POST", } h := MethodHandler("method")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"method": "POST"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestRequestHandler(t *testing.T) { r := &http.Request{ Method: "POST", URL: &url.URL{Path: "/path", RawQuery: "foo=bar"}, } h := RequestHandler("request")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"request": "POST /path?foo=bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestRemoteAddrHandler(t *testing.T) { r := &http.Request{ RemoteAddr: "1.2.3.4:1234", } h := RemoteAddrHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"ip": "1.2.3.4"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestRemoteAddrHandlerIPv6(t *testing.T) { r := &http.Request{ RemoteAddr: "[2001:db8:a0b:12f0::1]:1234", } h := RemoteAddrHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"ip": "2001:db8:a0b:12f0::1"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestUserAgentHandler(t *testing.T) { r := &http.Request{ Header: http.Header{ "User-Agent": []string{"some user agent string"}, }, } h := UserAgentHandler("ua")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"ua": "some user agent string"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestRefererHandler(t *testing.T) { r := &http.Request{ Header: http.Header{ "Referer": []string{"http://foo.com/bar"}, }, } h := RefererHandler("ua")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) assert.Equal(t, F{"ua": "http://foo.com/bar"}, F(l.fields)) })) h = NewHandler(Config{})(h) h.ServeHTTP(nil, r) } func TestRequestIDHandler(t *testing.T) { r := &http.Request{} h := RequestIDHandler("id", "Request-Id")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := FromRequest(r).(*logger) if id, ok := IDFromRequest(r); assert.True(t, ok) { assert.Equal(t, l.fields["id"], id) assert.Len(t, id.String(), 20) assert.Equal(t, id.String(), w.Header().Get("Request-Id")) } assert.Len(t, l.fields["id"], 12) })) h = NewHandler(Config{})(h) w := httptest.NewRecorder() h.ServeHTTP(w, r) } ================================================ FILE: internal/term/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Simon Eskildsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: internal/term/term.go ================================================ // Based on ssh/terminal: // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build linux,!appengine darwin freebsd openbsd package term type fder interface { Fd() uintptr } ================================================ FILE: internal/term/term_appengine.go ================================================ // Based on ssh/terminal: // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build appengine package term import "io" // IsTerminal always returns false on AppEngine. func IsTerminal(w io.Writer) bool { return false } ================================================ FILE: internal/term/term_darwin.go ================================================ // Based on ssh/terminal: // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package term import "syscall" const ioctlReadTermios = syscall.TIOCGETA ================================================ FILE: internal/term/term_freebsd.go ================================================ package term import ( "syscall" ) const ioctlReadTermios = syscall.TIOCGETA ================================================ FILE: internal/term/term_linux.go ================================================ // Based on ssh/terminal: // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build !appengine package term import "syscall" const ioctlReadTermios = syscall.TCGETS ================================================ FILE: internal/term/term_notwindows.go ================================================ // Based on ssh/terminal: // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build linux,!appengine darwin freebsd openbsd package term import ( "io" "syscall" "unsafe" ) // IsTerminal returns true if w writes to a terminal. func IsTerminal(w io.Writer) bool { fw, ok := w.(fder) if !ok { return false } var termios syscall.Termios _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fw.Fd(), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) return err == 0 } ================================================ FILE: internal/term/term_openbsd.go ================================================ package term import "syscall" const ioctlReadTermios = syscall.TIOCGETA ================================================ FILE: internal/term/term_windows.go ================================================ // Based on ssh/terminal: // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build windows package term import ( "io" "syscall" "unsafe" ) var kernel32 = syscall.NewLazyDLL("kernel32.dll") var ( procGetConsoleMode = kernel32.NewProc("GetConsoleMode") ) // IsTerminal returns true if w writes to a terminal. func IsTerminal(w io.Writer) bool { fw, ok := w.(interface { Fd() uintptr }) if !ok { return false } var st uint32 r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fw.Fd(), uintptr(unsafe.Pointer(&st)), 0) return r != 0 && e == 0 } ================================================ FILE: levels.go ================================================ package xlog import ( "bytes" "fmt" "strconv" ) // Level defines log levels type Level int // Log levels const ( LevelDebug Level = iota LevelInfo LevelWarn LevelError LevelFatal ) // Log level strings var ( levelDebug = "debug" levelInfo = "info" levelWarn = "warn" levelError = "error" levelFatal = "fatal" levelBytesDebug = []byte(levelDebug) levelBytesInfo = []byte(levelInfo) levelBytesWarn = []byte(levelWarn) levelBytesError = []byte(levelError) levelBytesFatal = []byte(levelFatal) ) // LevelFromString returns the level based on its string representation func LevelFromString(t string) (Level, error) { l := Level(0) err := (&l).UnmarshalText([]byte(t)) return l, err } // UnmarshalText lets Level implements the TextUnmarshaler interface used by encoding packages func (l *Level) UnmarshalText(text []byte) (err error) { if bytes.Equal(text, levelBytesDebug) { *l = LevelDebug } else if bytes.Equal(text, levelBytesInfo) { *l = LevelInfo } else if bytes.Equal(text, levelBytesWarn) { *l = LevelWarn } else if bytes.Equal(text, levelBytesError) { *l = LevelError } else if bytes.Equal(text, levelBytesFatal) { *l = LevelFatal } else { err = fmt.Errorf("Uknown level %v", string(text)) } return } // String returns the string representation of the level. func (l Level) String() string { var t string switch l { case LevelDebug: t = levelDebug case LevelInfo: t = levelInfo case LevelWarn: t = levelWarn case LevelError: t = levelError case LevelFatal: t = levelFatal default: t = strconv.FormatInt(int64(l), 10) } return t } // MarshalText lets Level implements the TextMarshaler interface used by encoding packages func (l Level) MarshalText() ([]byte, error) { var t []byte switch l { case LevelDebug: t = levelBytesDebug case LevelInfo: t = levelBytesInfo case LevelWarn: t = levelBytesWarn case LevelError: t = levelBytesError case LevelFatal: t = levelBytesFatal default: t = []byte(strconv.FormatInt(int64(l), 10)) } return t, nil } ================================================ FILE: levels_test.go ================================================ package xlog import ( "testing" "github.com/stretchr/testify/assert" ) func TestLevelFromString(t *testing.T) { l, err := LevelFromString("debug") assert.NoError(t, err) assert.Equal(t, LevelDebug, l) l, err = LevelFromString("info") assert.NoError(t, err) assert.Equal(t, LevelInfo, l) l, err = LevelFromString("warn") assert.NoError(t, err) assert.Equal(t, LevelWarn, l) l, err = LevelFromString("error") assert.NoError(t, err) assert.Equal(t, LevelError, l) l, err = LevelFromString("fatal") assert.NoError(t, err) assert.Equal(t, LevelFatal, l) _, err = LevelFromString("foo") assert.Error(t, err, "") } func TestLevelUnmarshalerText(t *testing.T) { l := Level(-1) err := l.UnmarshalText([]byte("debug")) assert.NoError(t, err) assert.Equal(t, LevelDebug, l) err = l.UnmarshalText([]byte("info")) assert.NoError(t, err) assert.Equal(t, LevelInfo, l) err = l.UnmarshalText([]byte("warn")) assert.NoError(t, err) assert.Equal(t, LevelWarn, l) err = l.UnmarshalText([]byte("error")) assert.NoError(t, err) assert.Equal(t, LevelError, l) err = l.UnmarshalText([]byte("fatal")) assert.NoError(t, err) assert.Equal(t, LevelFatal, l) assert.Error(t, l.UnmarshalText([]byte("invalid"))) } func TestLevelString(t *testing.T) { assert.Equal(t, "debug", LevelDebug.String()) assert.Equal(t, "info", LevelInfo.String()) assert.Equal(t, "warn", LevelWarn.String()) assert.Equal(t, "error", LevelError.String()) assert.Equal(t, "fatal", LevelFatal.String()) assert.Equal(t, "10", Level(10).String()) } func TestLevelMarshalerText(t *testing.T) { b, err := LevelDebug.MarshalText() assert.NoError(t, err) assert.Equal(t, string(levelBytesDebug), string(b)) b, err = LevelInfo.MarshalText() assert.NoError(t, err) assert.Equal(t, string(levelBytesInfo), string(b)) b, err = LevelWarn.MarshalText() assert.NoError(t, err) assert.Equal(t, string(levelBytesWarn), string(b)) b, err = LevelError.MarshalText() assert.NoError(t, err) assert.Equal(t, string(levelBytesError), string(b)) b, err = LevelFatal.MarshalText() assert.NoError(t, err) assert.Equal(t, string(levelBytesFatal), string(b)) b, err = Level(10).MarshalText() assert.NoError(t, err) assert.Equal(t, "10", string(b)) } ================================================ FILE: nop.go ================================================ package xlog type nop struct{} // NopLogger is an no-op implementation of xlog.Logger var NopLogger = &nop{} func (n nop) SetField(name string, value interface{}) {} func (n nop) GetFields() F { return map[string]interface{}{} } func (n nop) OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) {} func (n nop) Debug(v ...interface{}) {} func (n nop) Debugf(format string, v ...interface{}) {} func (n nop) Info(v ...interface{}) {} func (n nop) Infof(format string, v ...interface{}) {} func (n nop) Warn(v ...interface{}) {} func (n nop) Warnf(format string, v ...interface{}) {} func (n nop) Error(v ...interface{}) {} func (n nop) Errorf(format string, v ...interface{}) {} func (n nop) Fatal(v ...interface{}) { exit1() } func (n nop) Fatalf(format string, v ...interface{}) { exit1() } func (n nop) Write(p []byte) (int, error) { return len(p), nil } func (n nop) Output(calldepth int, s string) error { return nil } ================================================ FILE: nop_test.go ================================================ package xlog import "testing" func TestNopLogger(t *testing.T) { // cheap cover score upper NopLogger.SetField("name", "value") NopLogger.OutputF(LevelInfo, 0, "", nil) NopLogger.Debug() NopLogger.Debugf("format") NopLogger.Info() NopLogger.Infof("format") NopLogger.Warn() NopLogger.Warnf("format") NopLogger.Error() NopLogger.Errorf("format") exit1 = func() {} NopLogger.Fatal() NopLogger.Fatalf("format") NopLogger.Write([]byte{}) NopLogger.Output(0, "") } ================================================ FILE: output.go ================================================ package xlog import ( "bytes" "encoding/json" "errors" "io" "os" "sort" "strings" "sync" "time" "github.com/rs/xid" "github.com/rs/xlog/internal/term" ) // Output sends a log message fields to a destination. type Output interface { Write(fields map[string]interface{}) error } // OutputFunc is an adapter to allow the use of ordinary functions as Output handlers. // If it is a function with the appropriate signature, OutputFunc(f) is a Output object // that calls f on Write(). type OutputFunc func(fields map[string]interface{}) error func (of OutputFunc) Write(fields map[string]interface{}) error { return of(fields) } // OutputChannel is a send buffered channel between xlog and an Output. type OutputChannel struct { input chan map[string]interface{} output Output stop chan struct{} } // ErrBufferFull is returned when the output channel buffer is full and messages // are discarded. var ErrBufferFull = errors.New("buffer full") // NewOutputChannel creates a consumer buffered channel for the given output // with a default buffer of 100 messages. func NewOutputChannel(o Output) *OutputChannel { return NewOutputChannelBuffer(o, 100) } // NewOutputChannelBuffer creates a consumer buffered channel for the given output // with a customizable buffer size. func NewOutputChannelBuffer(o Output, bufSize int) *OutputChannel { oc := &OutputChannel{ input: make(chan map[string]interface{}, bufSize), output: o, stop: make(chan struct{}), } go func() { for { select { case msg := <-oc.input: if err := o.Write(msg); err != nil { critialLogger.Print("cannot write log message: ", err.Error()) } case <-oc.stop: close(oc.stop) return } } }() return oc } // Write implements the Output interface func (oc *OutputChannel) Write(fields map[string]interface{}) (err error) { select { case oc.input <- fields: // Sent with success default: // Channel is full, message dropped err = ErrBufferFull } return err } // Flush flushes all the buffered message to the output func (oc *OutputChannel) Flush() { for { select { case msg := <-oc.input: if err := oc.output.Write(msg); err != nil { critialLogger.Print("cannot write log message: ", err.Error()) } default: return } } } // Close closes the output channel and release the consumer's go routine. func (oc *OutputChannel) Close() { if oc.stop == nil { return } oc.stop <- struct{}{} <-oc.stop oc.stop = nil oc.Flush() } // Discard is an Output that discards all log message going thru it. var Discard = OutputFunc(func(fields map[string]interface{}) error { return nil }) var bufPool = &sync.Pool{ New: func() interface{} { return &bytes.Buffer{} }, } // MultiOutput routes the same message to serveral outputs. // If one or more outputs return an error, the last error is returned. type MultiOutput []Output func (m MultiOutput) Write(fields map[string]interface{}) (err error) { for _, o := range m { e := o.Write(fields) if e != nil { err = e } } return } // FilterOutput test a condition on the message and forward it to the child output // if it returns true. type FilterOutput struct { Cond func(fields map[string]interface{}) bool Output Output } func (f FilterOutput) Write(fields map[string]interface{}) (err error) { if f.Output == nil { return } if f.Cond(fields) { return f.Output.Write(fields) } return } // LevelOutput routes messages to different output based on the message's level. type LevelOutput struct { Debug Output Info Output Warn Output Error Output Fatal Output } func (l LevelOutput) Write(fields map[string]interface{}) error { var o Output switch fields[KeyLevel] { case "debug": o = l.Debug case "info": o = l.Info case "warn": o = l.Warn case "error": o = l.Error case "fatal": o = l.Fatal } if o != nil { return o.Write(fields) } return nil } // RecorderOutput stores the raw messages in it's Messages field. This output is useful for testing. type RecorderOutput struct { Messages []F } func (l *RecorderOutput) Write(fields map[string]interface{}) error { if l.Messages == nil { l.Messages = []F{fields} } else { l.Messages = append(l.Messages, fields) } return nil } // Reset empty the output from stored messages func (l *RecorderOutput) Reset() { l.Messages = []F{} } type consoleOutput struct { w io.Writer } var isTerminal = term.IsTerminal // NewConsoleOutput returns a Output printing message in a colored human readable form on the // stderr. If the stderr is not on a terminal, a LogfmtOutput is returned instead. func NewConsoleOutput() Output { return NewConsoleOutputW(os.Stderr, NewLogfmtOutput(os.Stderr)) } // NewConsoleOutputW returns a Output printing message in a colored human readable form with // the provided writer. If the writer is not on a terminal, the noTerm output is returned. func NewConsoleOutputW(w io.Writer, noTerm Output) Output { if isTerminal(w) { return consoleOutput{w: w} } return noTerm } func (o consoleOutput) Write(fields map[string]interface{}) error { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() if ts, ok := fields[KeyTime].(time.Time); ok { buf.Write([]byte(ts.Format("2006/01/02 15:04:05 "))) } if lvl, ok := fields[KeyLevel].(string); ok { levelColor := blue switch lvl { case "debug": levelColor = gray case "warn": levelColor = yellow case "error": levelColor = red } colorPrint(buf, strings.ToUpper(lvl[0:4]), levelColor) buf.WriteByte(' ') } if msg, ok := fields[KeyMessage].(string); ok { msg = strings.Replace(msg, "\n", "\\n", -1) buf.Write([]byte(msg)) } // Gather field keys keys := []string{} for k := range fields { switch k { case KeyLevel, KeyMessage, KeyTime: continue } keys = append(keys, k) } // Sort fields by key names sort.Strings(keys) // Print fields using logfmt format for _, k := range keys { buf.WriteByte(' ') colorPrint(buf, k, green) buf.WriteByte('=') if err := writeValue(buf, fields[k]); err != nil { return err } } buf.WriteByte('\n') _, err := o.w.Write(buf.Bytes()) return err } type logfmtOutput struct { w io.Writer } // NewLogfmtOutput returns a new output using logstash JSON schema v1 func NewLogfmtOutput(w io.Writer) Output { return logfmtOutput{w: w} } func (o logfmtOutput) Write(fields map[string]interface{}) error { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() // Gather field keys keys := []string{} for k := range fields { switch k { case KeyLevel, KeyMessage, KeyTime: continue } keys = append(keys, k) } // Sort fields by key names sort.Strings(keys) // Prepend default fields in a specific order keys = append([]string{KeyLevel, KeyMessage, KeyTime}, keys...) l := len(keys) for i, k := range keys { buf.Write([]byte(k)) buf.WriteByte('=') if err := writeValue(buf, fields[k]); err != nil { return err } if i+1 < l { buf.WriteByte(' ') } else { buf.WriteByte('\n') } } _, err := o.w.Write(buf.Bytes()) return err } // NewJSONOutput returns a new JSON output with the given writer. func NewJSONOutput(w io.Writer) Output { enc := json.NewEncoder(w) return OutputFunc(func(fields map[string]interface{}) error { return enc.Encode(fields) }) } // NewLogstashOutput returns an output to generate logstash friendly JSON format. func NewLogstashOutput(w io.Writer) Output { return OutputFunc(func(fields map[string]interface{}) error { lsf := map[string]interface{}{ "@version": 1, } for k, v := range fields { switch k { case KeyTime: k = "@timestamp" case KeyLevel: if s, ok := v.(string); ok { v = strings.ToUpper(s) } } if t, ok := v.(time.Time); ok { lsf[k] = t.Format(time.RFC3339) } else { lsf[k] = v } } b, err := json.Marshal(lsf) if err != nil { return err } _, err = w.Write(b) return err }) } // NewUIDOutput returns an output filter adding a globally unique id (using github.com/rs/xid) // to all message going thru this output. The o parameter defines the next output to pass data // to. func NewUIDOutput(field string, o Output) Output { return OutputFunc(func(fields map[string]interface{}) error { fields[field] = xid.New().String() return o.Write(fields) }) } // NewTrimOutput trims any field of type string with a value length greater than maxLen // to maxLen. func NewTrimOutput(maxLen int, o Output) Output { return OutputFunc(func(fields map[string]interface{}) error { for k, v := range fields { if s, ok := v.(string); ok && len(s) > maxLen { fields[k] = s[:maxLen] } } return o.Write(fields) }) } // NewTrimFieldsOutput trims listed field fields of type string with a value length greater than maxLen // to maxLen. func NewTrimFieldsOutput(trimFields []string, maxLen int, o Output) Output { return OutputFunc(func(fields map[string]interface{}) error { for _, f := range trimFields { if s, ok := fields[f].(string); ok && len(s) > maxLen { fields[f] = s[:maxLen] } } return o.Write(fields) }) } ================================================ FILE: output_examples_test.go ================================================ package xlog_test import ( "log/syslog" "github.com/rs/xlog" ) func Example_combinedOutputs() { conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.MultiOutput{ // Output interesting messages to console 0: xlog.FilterOutput{ Cond: func(fields map[string]interface{}) bool { val, found := fields["type"] return found && val == "interesting" }, Output: xlog.NewConsoleOutput(), }, // Also setup by-level loggers 1: xlog.LevelOutput{ // Send debug messages to console if they match type Debug: xlog.FilterOutput{ Cond: func(fields map[string]interface{}) bool { val, found := fields["type"] return found && val == "interesting" }, Output: xlog.NewConsoleOutput(), }, }, // Also send everything over syslog 2: xlog.NewSyslogOutput("", "", ""), }), } lh := xlog.NewHandler(conf) _ = lh } func ExampleMultiOutput() { conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.MultiOutput{ // Output everything to console 0: xlog.NewConsoleOutput(), // and also to local syslog 1: xlog.NewSyslogOutput("", "", ""), }), } lh := xlog.NewHandler(conf) _ = lh } func ExampleFilterOutput() { conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.FilterOutput{ // Match messages containing a field type = interesting Cond: func(fields map[string]interface{}) bool { val, found := fields["type"] return found && val == "interesting" }, // Output matching messages to the console Output: xlog.NewConsoleOutput(), }), } lh := xlog.NewHandler(conf) _ = lh } func ExampleLevelOutput() { conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.LevelOutput{ // Send debug message to console Debug: xlog.NewConsoleOutput(), // and error messages to syslog Error: xlog.NewSyslogOutput("", "", ""), // other levels are discarded }), } lh := xlog.NewHandler(conf) _ = lh } func ExampleNewSyslogWriter() { conf := xlog.Config{ Output: xlog.NewOutputChannel(xlog.LevelOutput{ Debug: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_DEBUG, "")), Info: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_INFO, "")), Warn: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_WARNING, "")), Error: xlog.NewLogstashOutput(xlog.NewSyslogWriter("", "", syslog.LOG_LOCAL0|syslog.LOG_ERR, "")), }), } lh := xlog.NewHandler(conf) _ = lh } ================================================ FILE: output_syslog.go ================================================ // +build !windows package xlog import ( "io" "log/syslog" ) // NewSyslogOutput returns JSONOutputs in a LevelOutput with writers set to syslog // with the proper priority added to a LOG_USER facility. // If network and address are empty, Dial will connect to the local syslog server. func NewSyslogOutput(network, address, tag string) Output { return NewSyslogOutputFacility(network, address, tag, syslog.LOG_USER) } // NewSyslogOutputFacility returns JSONOutputs in a LevelOutput with writers set to syslog // with the proper priority added to the passed facility. // If network and address are empty, Dial will connect to the local syslog server. func NewSyslogOutputFacility(network, address, tag string, facility syslog.Priority) Output { o := LevelOutput{ Debug: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_DEBUG, tag)), Info: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_INFO, tag)), Warn: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_WARNING, tag)), Error: NewJSONOutput(NewSyslogWriter(network, address, facility|syslog.LOG_ERR, tag)), } return o } // NewSyslogWriter returns a writer ready to be used with output modules. // If network and address are empty, Dial will connect to the local syslog server. func NewSyslogWriter(network, address string, prio syslog.Priority, tag string) io.Writer { s, err := syslog.Dial(network, address, prio, tag) if err != nil { m := "syslog dial error: " + err.Error() critialLogger.Print(m) panic(m) } return s } ================================================ FILE: output_test.go ================================================ package xlog import ( "bytes" "errors" "io" "io/ioutil" "log" "os" "testing" "time" "github.com/stretchr/testify/assert" ) type testOutput struct { err error w chan map[string]interface{} } func newTestOutput() *testOutput { return &testOutput{w: make(chan map[string]interface{}, 10)} } func newTestOutputErr(err error) *testOutput { return &testOutput{w: make(chan map[string]interface{}, 10), err: err} } func (o *testOutput) Write(fields map[string]interface{}) (err error) { o.w <- fields return o.err } func (o *testOutput) reset() { o.w = make(chan map[string]interface{}, 10) } func (o *testOutput) empty() bool { select { case <-o.w: return false default: return true } } func (o *testOutput) get() map[string]interface{} { select { case last := <-o.w: return last case <-time.After(2 * time.Second): return nil } } func TestOutputChannel(t *testing.T) { o := newTestOutput() oc := NewOutputChannel(o) defer oc.Close() oc.input <- F{"foo": "bar"} assert.Equal(t, F{"foo": "bar"}, F(o.get())) } func TestOutputChannelError(t *testing.T) { // Trigger error path r, w := io.Pipe() go func() { critialLoggerMux.Lock() defer critialLoggerMux.Unlock() oldCritialLogger := critialLogger critialLogger = log.New(w, "", 0) o := newTestOutputErr(errors.New("some error")) oc := NewOutputChannel(o) oc.input <- F{"foo": "bar"} o.get() oc.Close() critialLogger = oldCritialLogger w.Close() }() b, err := ioutil.ReadAll(r) assert.NoError(t, err) assert.Contains(t, string(b), "cannot write log message: some error") } func TestOutputChannelClose(t *testing.T) { oc := NewOutputChannel(newTestOutput()) defer oc.Close() assert.NotNil(t, oc.stop) oc.Close() assert.Nil(t, oc.stop) oc.Close() } func TestDiscard(t *testing.T) { assert.NoError(t, Discard.Write(F{})) } func TestMultiOutput(t *testing.T) { o1 := newTestOutput() o2 := newTestOutput() mo := MultiOutput{o1, o2} err := mo.Write(F{"foo": "bar"}) assert.NoError(t, err) assert.Equal(t, F{"foo": "bar"}, F(<-o1.w)) assert.Equal(t, F{"foo": "bar"}, F(<-o2.w)) } func TestMultiOutputWithError(t *testing.T) { o1 := newTestOutputErr(errors.New("some error")) o2 := newTestOutput() mo := MultiOutput{o1, o2} err := mo.Write(F{"foo": "bar"}) assert.EqualError(t, err, "some error") // Still send data to all outputs assert.Equal(t, F{"foo": "bar"}, F(<-o1.w)) assert.Equal(t, F{"foo": "bar"}, F(<-o2.w)) } func TestFilterOutput(t *testing.T) { o := newTestOutput() f := FilterOutput{ Cond: func(fields map[string]interface{}) bool { return fields["foo"] == "bar" }, Output: o, } err := f.Write(F{"foo": "bar"}) assert.NoError(t, err) assert.Equal(t, F{"foo": "bar"}, F(o.get())) o.reset() err = f.Write(F{"foo": "baz"}) assert.NoError(t, err) assert.True(t, o.empty()) f.Output = nil err = f.Write(F{"foo": "baz"}) assert.NoError(t, err) } func TestLevelOutput(t *testing.T) { oInfo := newTestOutput() oError := newTestOutput() oFatal := newTestOutput() oWarn := &testOutput{err: errors.New("some error")} reset := func() { oInfo.reset() oError.reset() oFatal.reset() oWarn.reset() } l := LevelOutput{ Info: oInfo, Error: oError, Fatal: oFatal, Warn: oWarn, } err := l.Write(F{"level": "fatal", "foo": "bar"}) assert.NoError(t, err) assert.True(t, oInfo.empty()) assert.True(t, oError.empty()) assert.Equal(t, F{"level": "fatal", "foo": "bar"}, F(<-oFatal.w)) assert.True(t, oWarn.empty()) reset() err = l.Write(F{"level": "error", "foo": "bar"}) assert.NoError(t, err) assert.True(t, oInfo.empty()) assert.Equal(t, F{"level": "error", "foo": "bar"}, F(<-oError.w)) assert.True(t, oFatal.empty()) assert.True(t, oWarn.empty()) reset() err = l.Write(F{"level": "info", "foo": "bar"}) assert.NoError(t, err) assert.Equal(t, F{"level": "info", "foo": "bar"}, F(<-oInfo.w)) assert.True(t, oFatal.empty()) assert.True(t, oError.empty()) assert.True(t, oWarn.empty()) reset() err = l.Write(F{"level": "warn", "foo": "bar"}) assert.EqualError(t, err, "some error") assert.True(t, oInfo.empty()) assert.True(t, oError.empty()) assert.True(t, oFatal.empty()) assert.Equal(t, F{"level": "warn", "foo": "bar"}, F(<-oWarn.w)) reset() err = l.Write(F{"level": "debug", "foo": "bar"}) assert.NoError(t, err) assert.True(t, oInfo.empty()) assert.True(t, oError.empty()) assert.True(t, oFatal.empty()) assert.True(t, oWarn.empty()) reset() err = l.Write(F{"foo": "bar"}) assert.NoError(t, err) assert.True(t, oInfo.empty()) assert.True(t, oError.empty()) assert.True(t, oFatal.empty()) assert.True(t, oWarn.empty()) } func TestSyslogOutput(t *testing.T) { buf := bytes.NewBuffer(nil) critialLoggerMux.Lock() oldCritialLogger := critialLogger critialLogger = log.New(buf, "", 0) defer func() { critialLogger = oldCritialLogger critialLoggerMux.Unlock() }() m := NewSyslogOutput("udp", "127.0.0.1:1234", "mytag") assert.IsType(t, LevelOutput{}, m) assert.Panics(t, func() { NewSyslogOutput("tcp", "an invalid host name", "mytag") }) assert.Regexp(t, "syslog dial error: dial tcp:.*missing port in address.*", buf.String()) } func TestRecorderOutput(t *testing.T) { o := RecorderOutput{} o.Write(F{"foo": "bar"}) o.Write(F{"bar": "baz"}) assert.Equal(t, []F{{"foo": "bar"}, {"bar": "baz"}}, o.Messages) o.Reset() assert.Equal(t, []F{}, o.Messages) } func TestNewConsoleOutput(t *testing.T) { old := isTerminal defer func() { isTerminal = old }() isTerminal = func(w io.Writer) bool { return true } c := NewConsoleOutput() if assert.IsType(t, consoleOutput{}, c) { assert.Equal(t, os.Stderr, c.(consoleOutput).w) } isTerminal = func(w io.Writer) bool { return false } c = NewConsoleOutput() if assert.IsType(t, logfmtOutput{}, c) { assert.Equal(t, os.Stderr, c.(logfmtOutput).w) } } func TestNewConsoleOutputW(t *testing.T) { b := bytes.NewBuffer([]byte{}) c := NewConsoleOutputW(b, NewLogfmtOutput(b)) assert.IsType(t, logfmtOutput{}, c) old := isTerminal defer func() { isTerminal = old }() isTerminal = func(w io.Writer) bool { return true } c = NewConsoleOutputW(b, NewLogfmtOutput(b)) if assert.IsType(t, consoleOutput{}, c) { assert.Equal(t, b, c.(consoleOutput).w) } } func TestConsoleOutput(t *testing.T) { buf := &bytes.Buffer{} c := consoleOutput{w: buf} err := c.Write(F{"message": "some message", "level": "info", "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), "foo": "bar"}) assert.NoError(t, err) assert.Equal(t, "2000/01/02 03:04:05 \x1b[34mINFO\x1b[0m some message \x1b[32mfoo\x1b[0m=bar\n", buf.String()) buf.Reset() err = c.Write(F{"message": "some debug", "level": "debug"}) assert.NoError(t, err) assert.Equal(t, "\x1b[37mDEBU\x1b[0m some debug\n", buf.String()) buf.Reset() err = c.Write(F{"message": "some warning", "level": "warn"}) assert.NoError(t, err) assert.Equal(t, "\x1b[33mWARN\x1b[0m some warning\n", buf.String()) buf.Reset() err = c.Write(F{"message": "some error", "level": "error"}) assert.NoError(t, err) assert.Equal(t, "\x1b[31mERRO\x1b[0m some error\n", buf.String()) } func TestLogfmtOutput(t *testing.T) { buf := &bytes.Buffer{} c := NewLogfmtOutput(buf) err := c.Write(F{ "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), "message": "some message", "level": "info", "string": "foo", "null": nil, "quoted": "needs \" quotes", "err": errors.New("error"), "errq": errors.New("error with \" quote"), }) assert.NoError(t, err) 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()) } func TestJSONOutput(t *testing.T) { buf := &bytes.Buffer{} j := NewJSONOutput(buf) err := j.Write(F{"message": "some message", "level": "info", "foo": "bar"}) assert.NoError(t, err) assert.Equal(t, "{\"foo\":\"bar\",\"level\":\"info\",\"message\":\"some message\"}\n", buf.String()) } func TestLogstashOutput(t *testing.T) { buf := &bytes.Buffer{} o := NewLogstashOutput(buf) err := o.Write(F{ "message": "some message", "level": "info", "time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), "file": "test.go:234", "foo": "bar", }) assert.NoError(t, err) assert.Equal(t, "{\"@timestamp\":\"2000-01-02T03:04:05Z\",\"@version\":1,\"file\":\"test.go:234\",\"foo\":\"bar\",\"level\":\"INFO\",\"message\":\"some message\"}", buf.String()) } func TestUIDOutput(t *testing.T) { o := newTestOutput() i := NewUIDOutput("id", o) err := i.Write(F{"message": "some message", "level": "info", "foo": "bar"}) last := o.get() assert.NoError(t, err) assert.NotNil(t, last["id"]) assert.Len(t, last["id"], 20) } func TestTrimOutput(t *testing.T) { o := newTestOutput() i := NewTrimOutput(10, o) err := i.Write(F{"short": "short", "long": "too long message", "number": 20}) last := o.get() assert.NoError(t, err) assert.Equal(t, "short", last["short"]) assert.Equal(t, "too long m", last["long"]) assert.Equal(t, 20, last["number"]) } func TestTrimFieldsOutput(t *testing.T) { o := newTestOutput() i := NewTrimFieldsOutput([]string{"short", "trim", "number"}, 10, o) err := i.Write(F{"short": "short", "long": "too long message", "trim": "too long message", "number": 20}) last := o.get() assert.NoError(t, err) assert.Equal(t, "short", last["short"]) assert.Equal(t, "too long m", last["trim"]) assert.Equal(t, "too long message", last["long"]) assert.Equal(t, 20, last["number"]) } ================================================ FILE: std.go ================================================ package xlog import "fmt" var std = New(Config{ Output: NewConsoleOutput(), }) // SetLogger changes the global logger instance func SetLogger(logger Logger) { std = logger } // Debug calls the Debug() method on the default logger func Debug(v ...interface{}) { f := extractFields(&v) std.OutputF(LevelDebug, 2, fmt.Sprint(v...), f) } // Debugf calls the Debugf() method on the default logger func Debugf(format string, v ...interface{}) { f := extractFields(&v) std.OutputF(LevelDebug, 2, fmt.Sprintf(format, v...), f) } // Info calls the Info() method on the default logger func Info(v ...interface{}) { f := extractFields(&v) std.OutputF(LevelInfo, 2, fmt.Sprint(v...), f) } // Infof calls the Infof() method on the default logger func Infof(format string, v ...interface{}) { f := extractFields(&v) std.OutputF(LevelInfo, 2, fmt.Sprintf(format, v...), f) } // Warn calls the Warn() method on the default logger func Warn(v ...interface{}) { f := extractFields(&v) std.OutputF(LevelWarn, 2, fmt.Sprint(v...), f) } // Warnf calls the Warnf() method on the default logger func Warnf(format string, v ...interface{}) { f := extractFields(&v) std.OutputF(LevelWarn, 2, fmt.Sprintf(format, v...), f) } // Error calls the Error() method on the default logger func Error(v ...interface{}) { f := extractFields(&v) std.OutputF(LevelError, 2, fmt.Sprint(v...), f) } // Errorf calls the Errorf() method on the default logger // // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last // argument to workaround go vet false alarm. func Errorf(format string, v ...interface{}) { f := extractFields(&v) if f != nil { // Let user add a %v at the end of the message when fields are passed to satisfy go vet l := len(format) if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { format = format[0 : l-2] } } std.OutputF(LevelError, 2, fmt.Sprintf(format, v...), f) } // Fatal calls the Fatal() method on the default logger func Fatal(v ...interface{}) { f := extractFields(&v) std.OutputF(LevelFatal, 2, fmt.Sprint(v...), f) if l, ok := std.(*logger); ok { if o, ok := l.output.(*OutputChannel); ok { o.Close() } } exit1() } // Fatalf calls the Fatalf() method on the default logger // // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last // argument to workaround go vet false alarm. func Fatalf(format string, v ...interface{}) { f := extractFields(&v) if f != nil { // Let user add a %v at the end of the message when fields are passed to satisfy go vet l := len(format) if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { format = format[0 : l-2] } } std.OutputF(LevelFatal, 2, fmt.Sprintf(format, v...), f) if l, ok := std.(*logger); ok { if o, ok := l.output.(*OutputChannel); ok { o.Close() } } exit1() } ================================================ FILE: std_example_test.go ================================================ package xlog_test import "github.com/rs/xlog" func ExampleSetLogger() { xlog.SetLogger(xlog.New(xlog.Config{ Level: xlog.LevelInfo, Output: xlog.NewConsoleOutput(), Fields: xlog.F{ "role": "my-service", }, })) } ================================================ FILE: std_test.go ================================================ package xlog import ( "testing" "github.com/stretchr/testify/assert" ) func TestGlobalLogger(t *testing.T) { o := newTestOutput() oldStd := std defer func() { std = oldStd }() SetLogger(New(Config{Output: o})) Debug("test") last := o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "debug", last["level"]) o.reset() Debugf("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "debug", last["level"]) o.reset() Info("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "info", last["level"]) o.reset() Infof("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "info", last["level"]) o.reset() Warn("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "warn", last["level"]) o.reset() Warnf("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "warn", last["level"]) o.reset() Error("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "error", last["level"]) o.reset() Errorf("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "error", last["level"]) o.reset() oldExit := exit1 exit1 = func() {} defer func() { exit1 = oldExit }() Fatal("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "fatal", last["level"]) o.reset() Fatalf("test") last = o.get() assert.Equal(t, "test", last["message"]) assert.Equal(t, "fatal", last["level"]) o.reset() } func TestStdError(t *testing.T) { o := newTestOutput() oldStd := std defer func() { std = oldStd }() SetLogger(New(Config{Output: o})) Error("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "std_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test", "foo": "bar"}, last) } func TestStdErrorf(t *testing.T) { o := newTestOutput() oldStd := std defer func() { std = oldStd }() SetLogger(New(Config{Output: o})) Errorf("test %d%v", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "std_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test 1", "foo": "bar"}, last) } func TestStdFatal(t *testing.T) { e := exit1 exited := 0 exit1 = func() { exited++ } defer func() { exit1 = e }() o := newTestOutput() oldStd := std defer func() { std = oldStd }() SetLogger(New(Config{Output: o})) Fatal("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "std_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test", "foo": "bar"}, last) assert.Equal(t, 1, exited) } func TestStdFatalf(t *testing.T) { e := exit1 exited := 0 exit1 = func() { exited++ } defer func() { exit1 = e }() o := newTestOutput() oldStd := std defer func() { std = oldStd }() SetLogger(New(Config{Output: o})) Fatalf("test %d%v", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "std_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test 1", "foo": "bar"}, last) assert.Equal(t, 1, exited) } ================================================ FILE: util.go ================================================ package xlog import ( "encoding/json" "fmt" "io" "strings" ) type color int const ( red color = 31 green color = 32 yellow color = 33 blue color = 34 gray color = 37 ) func colorPrint(w io.Writer, s string, c color) { w.Write([]byte{0x1b, '[', byte('0' + c/10), byte('0' + c%10), 'm'}) w.Write([]byte(s)) w.Write([]byte("\x1b[0m")) } func needsQuotedValueRune(r rune) bool { return r <= ' ' || r == '=' || r == '"' } // writeValue writes a value on the writer in a logfmt compatible way func writeValue(w io.Writer, v interface{}) (err error) { switch v := v.(type) { case nil: _, err = w.Write([]byte("null")) case string: if strings.IndexFunc(v, needsQuotedValueRune) != -1 { var b []byte b, err = json.Marshal(v) if err == nil { w.Write(b) } } else { _, err = w.Write([]byte(v)) } case error: s := v.Error() err = writeValue(w, s) default: s := fmt.Sprint(v) err = writeValue(w, s) } return } ================================================ FILE: util_test.go ================================================ package xlog import ( "bytes" "errors" "testing" "time" "github.com/stretchr/testify/assert" ) func TestColorPrint(t *testing.T) { buf := &bytes.Buffer{} colorPrint(buf, "test", red) assert.Equal(t, "\x1b[31mtest\x1b[0m", buf.String()) buf.Reset() colorPrint(buf, "test", green) assert.Equal(t, "\x1b[32mtest\x1b[0m", buf.String()) buf.Reset() colorPrint(buf, "test", yellow) assert.Equal(t, "\x1b[33mtest\x1b[0m", buf.String()) buf.Reset() colorPrint(buf, "test", blue) assert.Equal(t, "\x1b[34mtest\x1b[0m", buf.String()) buf.Reset() colorPrint(buf, "test", gray) assert.Equal(t, "\x1b[37mtest\x1b[0m", buf.String()) } func TestNeedsQuotedValueRune(t *testing.T) { assert.True(t, needsQuotedValueRune('=')) assert.True(t, needsQuotedValueRune('"')) assert.True(t, needsQuotedValueRune(' ')) assert.False(t, needsQuotedValueRune('a')) assert.False(t, needsQuotedValueRune('\'')) } func TestWriteValue(t *testing.T) { buf := &bytes.Buffer{} write := func(v interface{}) string { buf.Reset() err := writeValue(buf, v) if err == nil { return buf.String() } return "" } assert.Equal(t, `foobar`, write(`foobar`)) assert.Equal(t, `"foo=bar"`, write(`foo=bar`)) assert.Equal(t, `"foo bar"`, write(`foo bar`)) assert.Equal(t, `"foo\"bar"`, write(`foo"bar`)) assert.Equal(t, `"foo\nbar"`, write("foo\nbar")) assert.Equal(t, `null`, write(nil)) assert.Equal(t, `"2000-01-02 03:04:05 +0000 UTC"`, write(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC))) assert.Equal(t, `"error \"with quote\""`, write(errors.New(`error "with quote"`))) } ================================================ FILE: xlog.go ================================================ // Package xlog is a logger coupled with HTTP net/context aware middleware. // // 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. // // Features: // // - Per request log context // - Per request and/or per message key/value fields // - Log levels (Debug, Info, Warn, Error) // - Color output when terminal is detected // - Custom output (JSON, logfmt, …) // - Automatic gathering of request context like User-Agent, IP etc. // - Drops message rather than blocking execution // - Easy access logging thru github.com/rs/xaccess // // It works best in combination with github.com/rs/xhandler. package xlog // import "github.com/rs/xlog" import ( "fmt" "io" "log" "os" "path" "runtime" "strconv" "strings" "sync" "time" ) // Logger defines the interface for a xlog compatible logger type Logger interface { // Implements io.Writer so it can be set a output of log.Logger io.Writer // SetField sets a field on the logger's context. All future messages on this logger // will have this field set. SetField(name string, value interface{}) // GetFields returns all the fields set on the logger GetFields() F // Debug logs a debug message. If last parameter is a map[string]string, it's content // is added as fields to the message. Debug(v ...interface{}) // Debug logs a debug message with format. If last parameter is a map[string]string, // it's content is added as fields to the message. Debugf(format string, v ...interface{}) // Info logs a info message. If last parameter is a map[string]string, it's content // is added as fields to the message. Info(v ...interface{}) // Info logs a info message with format. If last parameter is a map[string]string, // it's content is added as fields to the message. Infof(format string, v ...interface{}) // Warn logs a warning message. If last parameter is a map[string]string, it's content // is added as fields to the message. Warn(v ...interface{}) // Warn logs a warning message with format. If last parameter is a map[string]string, // it's content is added as fields to the message. Warnf(format string, v ...interface{}) // Error logs an error message. If last parameter is a map[string]string, it's content // is added as fields to the message. Error(v ...interface{}) // Error logs an error message with format. If last parameter is a map[string]string, // it's content is added as fields to the message. Errorf(format string, v ...interface{}) // Fatal logs an error message followed by a call to os.Exit(1). If last parameter is a // map[string]string, it's content is added as fields to the message. Fatal(v ...interface{}) // Fatalf logs an error message with format followed by a call to ox.Exit(1). If last // parameter is a map[string]string, it's content is added as fields to the message. Fatalf(format string, v ...interface{}) // Output mimics std logger interface Output(calldepth int, s string) error // OutputF outputs message with fields. OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) } // LoggerCopier defines a logger with copy support type LoggerCopier interface { // Copy returns a copy of the logger Copy() Logger } // Config defines logger's configuration type Config struct { // Level is the maximum level to output, logs with lower level are discarded. Level Level // Fields defines default fields to use with all messages. Fields map[string]interface{} // Output to use to write log messages to. // // You should always wrap your output with an OutputChannel otherwise your // logger will be connected to its output synchronously. Output Output // DisablePooling removes the use of a sync.Pool for cases where logger // instances are needed beyond the scope of a request handler. This option // puts a greater pressure on GC and increases the amount of memory allocated // and freed. Use only if persistent loggers are a requirement. DisablePooling bool } // F represents a set of log message fields type F map[string]interface{} type logger struct { level Level output Output fields F disablePooling bool } // Common field names for log messages. var ( KeyTime = "time" KeyMessage = "message" KeyLevel = "level" KeyFile = "file" ) var now = time.Now var exit1 = func() { os.Exit(1) } // critialLogger is a logger to use when xlog is not able to deliver a message var critialLogger = log.New(os.Stderr, "xlog: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile) var loggerPool = &sync.Pool{ New: func() interface{} { return &logger{} }, } // New manually creates a logger. // // This function should only be used out of a request. Use FromContext in request. func New(c Config) Logger { var l *logger if c.DisablePooling { l = &logger{} } else { l = loggerPool.Get().(*logger) } l.level = c.Level l.output = c.Output if l.output == nil { l.output = NewOutputChannel(NewConsoleOutput()) } for k, v := range c.Fields { l.SetField(k, v) } l.disablePooling = c.DisablePooling return l } // Copy returns a copy of the passed logger if the logger implements // LoggerCopier or the NopLogger otherwise. func Copy(l Logger) Logger { if l, ok := l.(LoggerCopier); ok { return l.Copy() } return NopLogger } // Copy returns a copy of the logger func (l *logger) Copy() Logger { l2 := &logger{ level: l.level, output: l.output, fields: map[string]interface{}{}, disablePooling: l.disablePooling, } for k, v := range l.fields { l2.fields[k] = v } return l2 } // close returns the logger to the pool for reuse func (l *logger) close() { if !l.disablePooling { l.level = 0 l.output = nil l.fields = nil loggerPool.Put(l) } } func (l *logger) send(level Level, calldepth int, msg string, fields map[string]interface{}) { if level < l.level || l.output == nil { return } data := make(map[string]interface{}, 4+len(fields)+len(l.fields)) data[KeyTime] = now() data[KeyLevel] = level.String() data[KeyMessage] = msg if _, file, line, ok := runtime.Caller(calldepth); ok { data[KeyFile] = path.Base(file) + ":" + strconv.FormatInt(int64(line), 10) } for k, v := range fields { data[k] = v } if l.fields != nil { for k, v := range l.fields { data[k] = v } } if err := l.output.Write(data); err != nil { critialLogger.Print("send error: ", err.Error()) } } func extractFields(v *[]interface{}) map[string]interface{} { if l := len(*v); l > 0 { if f, ok := (*v)[l-1].(map[string]interface{}); ok { *v = (*v)[:l-1] return f } if f, ok := (*v)[l-1].(F); ok { *v = (*v)[:l-1] return f } } return nil } // SetField implements Logger interface func (l *logger) SetField(name string, value interface{}) { if l.fields == nil { l.fields = map[string]interface{}{} } l.fields[name] = value } // GetFields implements Logger interface func (l *logger) GetFields() F { return l.fields } // Output implements Logger interface func (l *logger) OutputF(level Level, calldepth int, msg string, fields map[string]interface{}) { l.send(level, calldepth+1, msg, fields) } // Debug implements Logger interface func (l *logger) Debug(v ...interface{}) { f := extractFields(&v) l.send(LevelDebug, 2, fmt.Sprint(v...), f) } // Debugf implements Logger interface func (l *logger) Debugf(format string, v ...interface{}) { f := extractFields(&v) l.send(LevelDebug, 2, fmt.Sprintf(format, v...), f) } // Info implements Logger interface func (l *logger) Info(v ...interface{}) { f := extractFields(&v) l.send(LevelInfo, 2, fmt.Sprint(v...), f) } // Infof implements Logger interface func (l *logger) Infof(format string, v ...interface{}) { f := extractFields(&v) l.send(LevelInfo, 2, fmt.Sprintf(format, v...), f) } // Warn implements Logger interface func (l *logger) Warn(v ...interface{}) { f := extractFields(&v) l.send(LevelWarn, 2, fmt.Sprint(v...), f) } // Warnf implements Logger interface func (l *logger) Warnf(format string, v ...interface{}) { f := extractFields(&v) l.send(LevelWarn, 2, fmt.Sprintf(format, v...), f) } // Error implements Logger interface func (l *logger) Error(v ...interface{}) { f := extractFields(&v) l.send(LevelError, 2, fmt.Sprint(v...), f) } // Errorf implements Logger interface // // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last // argument to workaround go vet false alarm. func (l *logger) Errorf(format string, v ...interface{}) { f := extractFields(&v) if f != nil { // Let user add a %v at the end of the message when fields are passed to satisfy go vet l := len(format) if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { format = format[0 : l-2] } } l.send(LevelError, 2, fmt.Sprintf(format, v...), f) } // Fatal implements Logger interface func (l *logger) Fatal(v ...interface{}) { f := extractFields(&v) l.send(LevelFatal, 2, fmt.Sprint(v...), f) if o, ok := l.output.(*OutputChannel); ok { o.Close() } exit1() } // Fatalf implements Logger interface // // Go vet users: you may append %v at the end of you format when using xlog.F{} as a last // argument to workaround go vet false alarm. func (l *logger) Fatalf(format string, v ...interface{}) { f := extractFields(&v) if f != nil { // Let user add a %v at the end of the message when fields are passed to satisfy go vet l := len(format) if l > 2 && format[l-2] == '%' && format[l-1] == 'v' { format = format[0 : l-2] } } l.send(LevelFatal, 2, fmt.Sprintf(format, v...), f) if o, ok := l.output.(*OutputChannel); ok { o.Close() } exit1() } // Write implements io.Writer interface func (l *logger) Write(p []byte) (int, error) { msg := strings.TrimRight(string(p), "\n") l.send(LevelInfo, 4, msg, nil) if o, ok := l.output.(*OutputChannel); ok { o.Flush() } return len(p), nil } // Output implements common logger interface func (l *logger) Output(calldepth int, s string) error { l.send(LevelInfo, 2, s, nil) return nil } ================================================ FILE: xlog_bench_test.go ================================================ package xlog import "testing" func BenchmarkSend(b *testing.B) { l := New(Config{Output: Discard, Fields: F{"a": "b"}}).(*logger) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { l.send(0, 0, "test", F{"foo": "bar", "bar": "baz"}) } } ================================================ FILE: xlog_examples_test.go ================================================ // +build go1.7 package xlog_test import ( "context" "errors" "log" "github.com/rs/xlog" ) func Example_log() { ctx := context.TODO() l := xlog.FromContext(ctx) // Log a simple message l.Debug("message") if err := errors.New("some error"); err != nil { l.Errorf("Some error happened: %v", err) } // With optional fields l.Debugf("foo %s", "bar", xlog.F{ "field": "value", }) } func Example_stdlog() { // Define logger conf conf := xlog.Config{ Output: xlog.NewConsoleOutput(), } // Remove timestamp and other decorations of the std logger log.SetFlags(0) // Plug a xlog instance to Go's std logger log.SetOutput(xlog.New(conf)) } ================================================ FILE: xlog_test.go ================================================ package xlog import ( "io" "io/ioutil" "log" "sync" "testing" "time" "github.com/stretchr/testify/assert" ) var fakeNow = time.Date(0, 0, 0, 0, 0, 0, 0, time.Local) var critialLoggerMux = sync.Mutex{} func init() { now = func() time.Time { return fakeNow } } func TestNew(t *testing.T) { oc := NewOutputChannel(newTestOutput()) defer oc.Close() c := Config{ Level: LevelError, Output: oc, Fields: F{"foo": "bar"}, } L := New(c) l, ok := L.(*logger) if assert.True(t, ok) { assert.Equal(t, LevelError, l.level) assert.Equal(t, c.Output, l.output) assert.Equal(t, F{"foo": "bar"}, F(l.fields)) // Ensure l.fields is a clone c.Fields["bar"] = "baz" assert.Equal(t, F{"foo": "bar"}, F(l.fields)) assert.Equal(t, false, l.disablePooling) l.close() } } func TestNewPoolDisabled(t *testing.T) { oc := NewOutputChannel(newTestOutput()) defer oc.Close() originalPool := loggerPool defer func(p *sync.Pool) { loggerPool = originalPool }(originalPool) loggerPool = &sync.Pool{ New: func() interface{} { assert.Fail(t, "pool used when disabled") return nil }, } c := Config{ Level: LevelError, Output: oc, Fields: F{"foo": "bar"}, DisablePooling: true, } L := New(c) l, ok := L.(*logger) if assert.True(t, ok) { assert.Equal(t, LevelError, l.level) assert.Equal(t, c.Output, l.output) assert.Equal(t, F{"foo": "bar"}, F(l.fields)) // Ensure l.fields is a clone c.Fields["bar"] = "baz" assert.Equal(t, F{"foo": "bar"}, F(l.fields)) assert.Equal(t, true, l.disablePooling) l.close() // Assert again to ensure close does not remove internal state assert.Equal(t, LevelError, l.level) assert.Equal(t, c.Output, l.output) assert.Equal(t, F{"foo": "bar"}, F(l.fields)) // Ensure l.fields is a clone c.Fields["bar"] = "baz" assert.Equal(t, F{"foo": "bar"}, F(l.fields)) assert.Equal(t, true, l.disablePooling) } } func TestCopy(t *testing.T) { oc := NewOutputChannel(newTestOutput()) defer oc.Close() c := Config{ Level: LevelError, Output: oc, Fields: F{"foo": "bar"}, } l := New(c).(*logger) l2 := Copy(l).(*logger) assert.Equal(t, l.output, l2.output) assert.Equal(t, l.level, l2.level) assert.Equal(t, l.fields, l2.fields) l2.SetField("bar", "baz") assert.Equal(t, F{"foo": "bar"}, l.fields) assert.Equal(t, F{"foo": "bar", "bar": "baz"}, l2.fields) assert.Equal(t, NopLogger, Copy(NopLogger)) assert.Equal(t, NopLogger, Copy(nil)) } func TestNewDefautOutput(t *testing.T) { L := New(Config{}) l, ok := L.(*logger) if assert.True(t, ok) { assert.NotNil(t, l.output) l.close() } } func TestSend(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.send(LevelDebug, 1, "test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test", "foo": "bar"}, last) l.SetField("bar", "baz") l.send(LevelInfo, 1, "test", F{"foo": "bar"}) last = <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test", "foo": "bar", "bar": "baz"}, last) l = New(Config{Output: o, Level: 1}).(*logger) o.reset() l.send(0, 2, "test", F{"foo": "bar"}) assert.True(t, o.empty()) } func TestSendDrop(t *testing.T) { t.Skip() r, w := io.Pipe() go func() { critialLoggerMux.Lock() defer critialLoggerMux.Unlock() oldCritialLogger := critialLogger critialLogger = log.New(w, "", 0) o := newTestOutput() oc := NewOutputChannelBuffer(Discard, 1) l := New(Config{Output: oc}).(*logger) l.send(LevelDebug, 2, "test", F{"foo": "bar"}) l.send(LevelDebug, 2, "test", F{"foo": "bar"}) l.send(LevelDebug, 2, "test", F{"foo": "bar"}) o.get() o.get() o.get() oc.Close() critialLogger = oldCritialLogger w.Close() }() b, err := ioutil.ReadAll(r) assert.NoError(t, err) assert.Contains(t, string(b), "send error: buffer full") } func TestExtractFields(t *testing.T) { v := []interface{}{"a", 1, map[string]interface{}{"foo": "bar"}} f := extractFields(&v) assert.Equal(t, map[string]interface{}{"foo": "bar"}, f) assert.Equal(t, []interface{}{"a", 1}, v) v = []interface{}{map[string]interface{}{"foo": "bar"}, "a", 1} f = extractFields(&v) assert.Nil(t, f) assert.Equal(t, []interface{}{map[string]interface{}{"foo": "bar"}, "a", 1}, v) v = []interface{}{"a", 1, F{"foo": "bar"}} f = extractFields(&v) assert.Equal(t, map[string]interface{}{"foo": "bar"}, f) assert.Equal(t, []interface{}{"a", 1}, v) v = []interface{}{} f = extractFields(&v) assert.Nil(t, f) assert.Equal(t, []interface{}{}, v) } func TestGetFields(t *testing.T) { oc := NewOutputChannelBuffer(Discard, 1) l := New(Config{Output: oc}).(*logger) l.SetField("k", "v") assert.Equal(t, F{"k": "v"}, l.GetFields()) } func TestDebug(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Debug("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test", "foo": "bar"}, last) } func TestDebugf(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Debugf("test %d", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "debug", "message": "test 1", "foo": "bar"}, last) } func TestInfo(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Info("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test", "foo": "bar"}, last) } func TestInfof(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Infof("test %d", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test 1", "foo": "bar"}, last) } func TestWarn(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Warn("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "warn", "message": "test", "foo": "bar"}, last) } func TestWarnf(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Warnf("test %d", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "warn", "message": "test 1", "foo": "bar"}, last) } func TestError(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Error("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test", "foo": "bar"}, last) } func TestErrorf(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Errorf("test %d%v", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "error", "message": "test 1", "foo": "bar"}, last) } func TestFatal(t *testing.T) { e := exit1 exited := 0 exit1 = func() { exited++ } defer func() { exit1 = e }() o := newTestOutput() l := New(Config{Output: NewOutputChannel(o)}).(*logger) l.Fatal("test", F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test", "foo": "bar"}, last) assert.Equal(t, 1, exited) } func TestFatalf(t *testing.T) { e := exit1 exited := 0 exit1 = func() { exited++ } defer func() { exit1 = e }() o := newTestOutput() l := New(Config{Output: NewOutputChannel(o)}).(*logger) l.Fatalf("test %d%v", 1, F{"foo": "bar"}) last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "fatal", "message": "test 1", "foo": "bar"}, last) assert.Equal(t, 1, exited) } func TestWrite(t *testing.T) { o := newTestOutput() xl := New(Config{Output: NewOutputChannel(o)}).(*logger) l := log.New(xl, "prefix ", 0) l.Printf("test") last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "prefix test"}, last) } func TestOutput(t *testing.T) { o := newTestOutput() l := New(Config{Output: o}).(*logger) l.Output(2, "test") last := <-o.w assert.Contains(t, last["file"], "log_test.go:") delete(last, "file") assert.Equal(t, map[string]interface{}{"time": fakeNow, "level": "info", "message": "test"}, last) }