├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── History.md ├── LICENSE ├── Makefile ├── Readme.md ├── _examples ├── apexlogs │ └── main.go ├── cli │ └── cli.go ├── default │ └── default.go ├── delta │ └── delta.go ├── es │ └── es.go ├── json │ └── json.go ├── kinesis │ └── kinesis.go ├── logfmt │ └── logfmt.go ├── multi │ └── multi.go ├── stack │ └── stack.go ├── text │ └── text.go └── trace │ └── trace.go ├── assets └── title.png ├── context.go ├── context_test.go ├── default.go ├── doc.go ├── entry.go ├── entry_test.go ├── go.mod ├── go.sum ├── handlers ├── apexlogs │ └── apexlogs.go ├── cli │ └── cli.go ├── delta │ └── delta.go ├── discard │ └── discard.go ├── es │ └── es.go ├── graylog │ └── graylog.go ├── json │ ├── json.go │ └── json_test.go ├── kinesis │ └── kinesis.go ├── level │ ├── level.go │ └── level_test.go ├── logfmt │ ├── logfmt.go │ └── logfmt_test.go ├── memory │ └── memory.go ├── multi │ ├── multi.go │ └── multi_test.go ├── papertrail │ └── papertrail.go └── text │ ├── text.go │ └── text_test.go ├── interface.go ├── levels.go ├── levels_test.go ├── logger.go ├── logger_test.go ├── pkg.go ├── pkg_test.go └── stack.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | go: ['1.13', '1.14', '1.15'] 11 | steps: 12 | 13 | - uses: actions/checkout@v2 14 | - name: Set up Go ${{ matrix.go }} 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go }} 18 | 19 | - name: Deps 20 | run: go mod download 21 | 22 | - name: Vet 23 | run: go vet ./... 24 | 25 | - name: Test 26 | run: go test ./... 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.9.0 / 2020-08-18 3 | =================== 4 | 5 | * add `WithDuration()` method to record a duration as milliseconds 6 | * add: ignore nil errors in `WithError()` 7 | * change trace duration to milliseconds (arguably a breaking change) 8 | 9 | v1.8.0 / 2020-08-05 10 | =================== 11 | 12 | * refactor apexlogs handler to not make the AddEvents() call if there are no events to flush 13 | 14 | v1.7.1 / 2020-08-05 15 | =================== 16 | 17 | * fix potential nil panic in apexlogs handler 18 | 19 | v1.7.0 / 2020-08-03 20 | =================== 21 | 22 | * add FlushSync() to apexlogs handler 23 | 24 | v1.6.0 / 2020-07-13 25 | =================== 26 | 27 | * update apex/logs dep to v1.0.0 28 | * docs: mention that Flush() is non-blocking now, use Close() 29 | 30 | v1.5.0 / 2020-07-11 31 | =================== 32 | 33 | * add buffering to Apex Logs handler 34 | 35 | v1.4.0 / 2020-06-16 36 | =================== 37 | 38 | * add AuthToken to apexlogs handler 39 | 40 | v1.3.0 / 2020-05-26 41 | =================== 42 | 43 | * change FromContext() to always return a logger 44 | 45 | v1.2.0 / 2020-05-26 46 | =================== 47 | 48 | * add log.NewContext() and log.FromContext(). Closes #78 49 | 50 | v1.1.4 / 2020-04-22 51 | =================== 52 | 53 | * add apexlogs HTTPClient support 54 | 55 | v1.1.3 / 2020-04-22 56 | =================== 57 | 58 | * add events len check before flushing to apexlogs handler 59 | 60 | v1.1.2 / 2020-01-29 61 | =================== 62 | 63 | * refactor apexlogs handler to use github.com/apex/logs client 64 | 65 | v1.1.1 / 2019-06-24 66 | =================== 67 | 68 | * add go.mod 69 | * add rough pass at apexlogs handler 70 | 71 | v1.1.0 / 2018-10-11 72 | =================== 73 | 74 | * fix: cli handler to show non-string fields appropriately 75 | * fix: cli using fatih/color to better support windows 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | include github.com/tj/make/golang 3 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | ![Structured logging for golang](assets/title.png) 3 | 4 | Package log implements a simple structured logging API inspired by Logrus, designed with centralization in mind. Read more on [Medium](https://medium.com/@tjholowaychuk/apex-log-e8d9627f4a9a#.rav8yhkud). 5 | 6 | ## Handlers 7 | 8 | - __apexlogs__ – handler for [Apex Logs](https://apex.sh/logs/) 9 | - __cli__ – human-friendly CLI output 10 | - __discard__ – discards all logs 11 | - __es__ – Elasticsearch handler 12 | - __graylog__ – Graylog handler 13 | - __json__ – JSON output handler 14 | - __kinesis__ – AWS Kinesis handler 15 | - __level__ – level filter handler 16 | - __logfmt__ – logfmt plain-text formatter 17 | - __memory__ – in-memory handler for tests 18 | - __multi__ – fan-out to multiple handlers 19 | - __papertrail__ – Papertrail handler 20 | - __text__ – human-friendly colored output 21 | - __delta__ – outputs the delta between log calls and spinner 22 | 23 | ## Example 24 | 25 | Example using the [Apex Logs](https://apex.sh/logs/) handler. 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | "errors" 32 | "time" 33 | 34 | "github.com/apex/log" 35 | ) 36 | 37 | func main() { 38 | ctx := log.WithFields(log.Fields{ 39 | "file": "something.png", 40 | "type": "image/png", 41 | "user": "tobi", 42 | }) 43 | 44 | for range time.Tick(time.Millisecond * 200) { 45 | ctx.Info("upload") 46 | ctx.Info("upload complete") 47 | ctx.Warn("upload retry") 48 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 49 | ctx.Errorf("failed to upload %s", "img.png") 50 | } 51 | } 52 | ``` 53 | 54 | --- 55 | 56 | [![Build Status](https://semaphoreci.com/api/v1/projects/d8a8b1c0-45b0-4b89-b066-99d788d0b94c/642077/badge.svg)](https://semaphoreci.com/tj/log) 57 | [![GoDoc](https://godoc.org/github.com/apex/log?status.svg)](https://godoc.org/github.com/apex/log) 58 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 59 | ![](https://img.shields.io/badge/status-stable-green.svg) 60 | 61 | 62 | -------------------------------------------------------------------------------- /_examples/apexlogs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/apexlogs" 10 | ) 11 | 12 | func main() { 13 | url := os.Getenv("APEX_LOGS_URL") 14 | projectID := os.Getenv("APEX_LOGS_PROJECT_ID") 15 | authToken := os.Getenv("APEX_LOGS_AUTH_TOKEN") 16 | 17 | h := apexlogs.New(url, projectID, authToken) 18 | 19 | defer h.Close() 20 | 21 | log.SetLevel(log.DebugLevel) 22 | log.SetHandler(h) 23 | 24 | ctx := log.WithFields(log.Fields{ 25 | "file": "something.png", 26 | "type": "image/png", 27 | "user": "tobi", 28 | }) 29 | 30 | go func() { 31 | for range time.Tick(time.Second) { 32 | ctx.Debug("doing stuff") 33 | } 34 | }() 35 | 36 | go func() { 37 | for range time.Tick(100 * time.Millisecond) { 38 | ctx.Info("uploading") 39 | ctx.Info("upload complete") 40 | } 41 | }() 42 | 43 | go func() { 44 | for range time.Tick(time.Second) { 45 | ctx.Warn("upload slow") 46 | } 47 | }() 48 | 49 | go func() { 50 | for range time.Tick(2 * time.Second) { 51 | err := errors.New("boom") 52 | ctx.WithError(err).Error("upload failed") 53 | } 54 | }() 55 | 56 | select {} 57 | } 58 | -------------------------------------------------------------------------------- /_examples/cli/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/cli" 9 | ) 10 | 11 | func main() { 12 | log.SetHandler(cli.Default) 13 | log.SetLevel(log.DebugLevel) 14 | 15 | ctx := log.WithFields(log.Fields{ 16 | "file": "something.png", 17 | "type": "image/png", 18 | "user": "tobi", 19 | }) 20 | 21 | go func() { 22 | for range time.Tick(time.Second) { 23 | ctx.Debug("doing stuff") 24 | } 25 | }() 26 | 27 | go func() { 28 | for range time.Tick(100 * time.Millisecond) { 29 | ctx.Info("uploading") 30 | ctx.Info("upload complete") 31 | } 32 | }() 33 | 34 | go func() { 35 | for range time.Tick(time.Second) { 36 | ctx.Warn("upload slow") 37 | } 38 | }() 39 | 40 | go func() { 41 | for range time.Tick(2 * time.Second) { 42 | err := errors.New("boom") 43 | ctx.WithError(err).Error("upload failed") 44 | } 45 | }() 46 | 47 | select {} 48 | } 49 | -------------------------------------------------------------------------------- /_examples/default/default.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | ) 9 | 10 | func main() { 11 | ctx := log.WithFields(log.Fields{ 12 | "file": "something.png", 13 | "type": "image/png", 14 | "user": "tobi", 15 | }) 16 | 17 | for range time.Tick(time.Millisecond * 200) { 18 | ctx.Info("upload") 19 | ctx.Info("upload complete") 20 | ctx.Warn("upload retry") 21 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 22 | ctx.Errorf("failed to upload %s", "img.png") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /_examples/delta/delta.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/delta" 9 | ) 10 | 11 | func main() { 12 | log.SetHandler(delta.Default) 13 | log.SetLevel(log.DebugLevel) 14 | 15 | ctx := log.WithFields(log.Fields{ 16 | "file": "something.png", 17 | "type": "image/png", 18 | "user": "tobi", 19 | }) 20 | 21 | go func() { 22 | for range time.Tick(time.Second) { 23 | ctx.Debug("doing stuff") 24 | } 25 | }() 26 | 27 | go func() { 28 | for range time.Tick(100 * time.Millisecond) { 29 | ctx.Info("uploading") 30 | ctx.Info("upload complete") 31 | } 32 | }() 33 | 34 | go func() { 35 | for range time.Tick(time.Second) { 36 | ctx.Warn("upload slow") 37 | } 38 | }() 39 | 40 | go func() { 41 | for range time.Tick(2 * time.Second) { 42 | err := errors.New("boom") 43 | ctx.WithError(err).Error("upload failed") 44 | } 45 | }() 46 | 47 | select {} 48 | } 49 | -------------------------------------------------------------------------------- /_examples/es/es.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/apex/log" 10 | "github.com/apex/log/handlers/es" 11 | "github.com/apex/log/handlers/multi" 12 | "github.com/apex/log/handlers/text" 13 | "github.com/tj/go-elastic" 14 | ) 15 | 16 | func main() { 17 | esClient := elastic.New("http://192.168.99.101:9200") 18 | esClient.HTTPClient = &http.Client{ 19 | Timeout: 5 * time.Second, 20 | } 21 | 22 | e := es.New(&es.Config{ 23 | Client: esClient, 24 | BufferSize: 100, 25 | }) 26 | 27 | t := text.New(os.Stderr) 28 | 29 | log.SetHandler(multi.New(e, t)) 30 | 31 | ctx := log.WithFields(log.Fields{ 32 | "file": "something.png", 33 | "type": "image/png", 34 | "user": "tobi", 35 | }) 36 | 37 | go func() { 38 | for range time.Tick(time.Millisecond * 200) { 39 | ctx.Info("upload") 40 | ctx.Info("upload complete") 41 | ctx.Warn("upload retry") 42 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 43 | ctx.Errorf("failed to upload %s", "img.png") 44 | } 45 | }() 46 | 47 | go func() { 48 | for range time.Tick(time.Millisecond * 25) { 49 | ctx.Info("upload") 50 | } 51 | }() 52 | 53 | select {} 54 | } 55 | -------------------------------------------------------------------------------- /_examples/json/json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/json" 10 | ) 11 | 12 | func main() { 13 | log.SetHandler(json.New(os.Stderr)) 14 | 15 | ctx := log.WithFields(log.Fields{ 16 | "file": "something.png", 17 | "type": "image/png", 18 | "user": "tobi", 19 | }) 20 | 21 | for range time.Tick(time.Millisecond * 200) { 22 | ctx.Info("upload") 23 | ctx.Info("upload complete") 24 | ctx.Warn("upload retry") 25 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /_examples/kinesis/kinesis.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/kinesis" 9 | "github.com/apex/log/handlers/multi" 10 | "github.com/apex/log/handlers/text" 11 | ) 12 | 13 | func main() { 14 | log.SetHandler(multi.New( 15 | text.New(os.Stderr), 16 | kinesis.New("logs"), 17 | )) 18 | 19 | ctx := log.WithFields(log.Fields{ 20 | "file": "something.png", 21 | "type": "image/png", 22 | "user": "tobi", 23 | }) 24 | 25 | for range time.Tick(time.Millisecond * 100) { 26 | ctx.Info("upload") 27 | ctx.Info("upload complete") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /_examples/logfmt/logfmt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/logfmt" 10 | ) 11 | 12 | func main() { 13 | log.SetHandler(logfmt.New(os.Stderr)) 14 | 15 | ctx := log.WithFields(log.Fields{ 16 | "file": "something.png", 17 | "type": "image/png", 18 | "user": "tobi", 19 | }) 20 | 21 | for range time.Tick(time.Millisecond * 200) { 22 | ctx.Info("upload") 23 | ctx.Info("upload complete") 24 | ctx.Warn("upload retry") 25 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /_examples/multi/multi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/json" 10 | "github.com/apex/log/handlers/multi" 11 | "github.com/apex/log/handlers/text" 12 | ) 13 | 14 | func main() { 15 | log.SetHandler(multi.New( 16 | text.New(os.Stderr), 17 | json.New(os.Stderr), 18 | )) 19 | 20 | ctx := log.WithFields(log.Fields{ 21 | "file": "something.png", 22 | "type": "image/png", 23 | "user": "tobi", 24 | }) 25 | 26 | for range time.Tick(time.Millisecond * 200) { 27 | ctx.Info("upload") 28 | ctx.Info("upload complete") 29 | ctx.Warn("upload retry") 30 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /_examples/stack/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/logfmt" 10 | ) 11 | 12 | func main() { 13 | log.SetHandler(logfmt.New(os.Stderr)) 14 | 15 | filename := "something.png" 16 | body := []byte("whatever") 17 | 18 | ctx := log.WithField("filename", filename) 19 | 20 | err := upload(filename, body) 21 | if err != nil { 22 | ctx.WithError(err).Error("upload failed") 23 | } 24 | } 25 | 26 | // Faux upload. 27 | func upload(name string, b []byte) error { 28 | err := put("/images/"+name, b) 29 | if err != nil { 30 | return errors.Wrap(err, "uploading to s3") 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // Faux PUT. 37 | func put(key string, b []byte) error { 38 | return errors.New("unauthorized") 39 | } 40 | -------------------------------------------------------------------------------- /_examples/text/text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/text" 10 | ) 11 | 12 | func main() { 13 | log.SetHandler(text.New(os.Stderr)) 14 | 15 | ctx := log.WithFields(log.Fields{ 16 | "file": "something.png", 17 | "type": "image/png", 18 | "user": "tobi", 19 | }) 20 | 21 | for range time.Tick(time.Millisecond * 200) { 22 | ctx.Info("upload") 23 | ctx.Info("upload complete") 24 | ctx.Warn("upload retry") 25 | ctx.WithError(errors.New("unauthorized")).Error("upload failed") 26 | ctx.Errorf("failed to upload %s", "img.png") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /_examples/trace/trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/text" 9 | ) 10 | 11 | func work(ctx log.Interface) (err error) { 12 | path := "Readme.md" 13 | defer ctx.WithField("path", path).Trace("opening").Stop(&err) 14 | _, err = os.Open(path) 15 | return 16 | } 17 | 18 | func main() { 19 | log.SetHandler(text.New(os.Stderr)) 20 | 21 | ctx := log.WithFields(log.Fields{ 22 | "app": "myapp", 23 | "env": "prod", 24 | }) 25 | 26 | for range time.Tick(time.Second) { 27 | _ = work(ctx) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/log/8da83152b5d6177b4bfe3d12810a5afd25355170/assets/title.png -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "context" 4 | 5 | // logKey is a private context key. 6 | type logKey struct{} 7 | 8 | // NewContext returns a new context with logger. 9 | func NewContext(ctx context.Context, v Interface) context.Context { 10 | return context.WithValue(ctx, logKey{}, v) 11 | } 12 | 13 | // FromContext returns the logger from context, or log.Log. 14 | func FromContext(ctx context.Context) Interface { 15 | if v, ok := ctx.Value(logKey{}).(Interface); ok { 16 | return v 17 | } 18 | return Log 19 | } 20 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | 9 | "github.com/apex/log" 10 | ) 11 | 12 | func TestFromContext(t *testing.T) { 13 | ctx := context.Background() 14 | 15 | logger := log.FromContext(ctx) 16 | assert.Equal(t, log.Log, logger) 17 | 18 | logs := log.WithField("foo", "bar") 19 | ctx = log.NewContext(ctx, logs) 20 | 21 | logger = log.FromContext(ctx) 22 | assert.Equal(t, logs, logger) 23 | } 24 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "sort" 8 | ) 9 | 10 | // field used for sorting. 11 | type field struct { 12 | Name string 13 | Value interface{} 14 | } 15 | 16 | // by sorts fields by name. 17 | type byName []field 18 | 19 | func (a byName) Len() int { return len(a) } 20 | func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 21 | func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } 22 | 23 | // handleStdLog outpouts to the stlib log. 24 | func handleStdLog(e *Entry) error { 25 | level := levelNames[e.Level] 26 | 27 | var fields []field 28 | 29 | for k, v := range e.Fields { 30 | fields = append(fields, field{k, v}) 31 | } 32 | 33 | sort.Sort(byName(fields)) 34 | 35 | var b bytes.Buffer 36 | fmt.Fprintf(&b, "%5s %-25s", level, e.Message) 37 | 38 | for _, f := range fields { 39 | fmt.Fprintf(&b, " %s=%v", f.Name, f.Value) 40 | } 41 | 42 | log.Println(b.String()) 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package log implements a simple structured logging API designed with few assumptions. Designed for 3 | centralized logging solutions such as Kinesis which require encoding and decoding before fanning-out 4 | to handlers. 5 | 6 | You may use this package with inline handlers, much like Logrus, however a centralized solution 7 | is recommended so that apps do not need to be re-deployed to add or remove logging service 8 | providers. 9 | */ 10 | package log 11 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // assert interface compliance. 11 | var _ Interface = (*Entry)(nil) 12 | 13 | // Now returns the current time. 14 | var Now = time.Now 15 | 16 | // Entry represents a single log entry. 17 | type Entry struct { 18 | Logger *Logger `json:"-"` 19 | Fields Fields `json:"fields"` 20 | Level Level `json:"level"` 21 | Timestamp time.Time `json:"timestamp"` 22 | Message string `json:"message"` 23 | start time.Time 24 | fields []Fields 25 | } 26 | 27 | // NewEntry returns a new entry for `log`. 28 | func NewEntry(log *Logger) *Entry { 29 | return &Entry{ 30 | Logger: log, 31 | } 32 | } 33 | 34 | // WithFields returns a new entry with `fields` set. 35 | func (e *Entry) WithFields(fields Fielder) *Entry { 36 | f := []Fields{} 37 | f = append(f, e.fields...) 38 | f = append(f, fields.Fields()) 39 | return &Entry{ 40 | Logger: e.Logger, 41 | fields: f, 42 | } 43 | } 44 | 45 | // WithField returns a new entry with the `key` and `value` set. 46 | func (e *Entry) WithField(key string, value interface{}) *Entry { 47 | return e.WithFields(Fields{key: value}) 48 | } 49 | 50 | // WithDuration returns a new entry with the "duration" field set 51 | // to the given duration in milliseconds. 52 | func (e *Entry) WithDuration(d time.Duration) *Entry { 53 | return e.WithField("duration", d.Milliseconds()) 54 | } 55 | 56 | // WithError returns a new entry with the "error" set to `err`. 57 | // 58 | // The given error may implement .Fielder, if it does the method 59 | // will add all its `.Fields()` into the returned entry. 60 | func (e *Entry) WithError(err error) *Entry { 61 | if err == nil { 62 | return e 63 | } 64 | 65 | ctx := e.WithField("error", err.Error()) 66 | 67 | if s, ok := err.(stackTracer); ok { 68 | frame := s.StackTrace()[0] 69 | 70 | name := fmt.Sprintf("%n", frame) 71 | file := fmt.Sprintf("%+s", frame) 72 | line := fmt.Sprintf("%d", frame) 73 | 74 | parts := strings.Split(file, "\n\t") 75 | if len(parts) > 1 { 76 | file = parts[1] 77 | } 78 | 79 | ctx = ctx.WithField("source", fmt.Sprintf("%s: %s:%s", name, file, line)) 80 | } 81 | 82 | if f, ok := err.(Fielder); ok { 83 | ctx = ctx.WithFields(f.Fields()) 84 | } 85 | 86 | return ctx 87 | } 88 | 89 | // Debug level message. 90 | func (e *Entry) Debug(msg string) { 91 | e.Logger.log(DebugLevel, e, msg) 92 | } 93 | 94 | // Info level message. 95 | func (e *Entry) Info(msg string) { 96 | e.Logger.log(InfoLevel, e, msg) 97 | } 98 | 99 | // Warn level message. 100 | func (e *Entry) Warn(msg string) { 101 | e.Logger.log(WarnLevel, e, msg) 102 | } 103 | 104 | // Error level message. 105 | func (e *Entry) Error(msg string) { 106 | e.Logger.log(ErrorLevel, e, msg) 107 | } 108 | 109 | // Fatal level message, followed by an exit. 110 | func (e *Entry) Fatal(msg string) { 111 | e.Logger.log(FatalLevel, e, msg) 112 | os.Exit(1) 113 | } 114 | 115 | // Debugf level formatted message. 116 | func (e *Entry) Debugf(msg string, v ...interface{}) { 117 | e.Debug(fmt.Sprintf(msg, v...)) 118 | } 119 | 120 | // Infof level formatted message. 121 | func (e *Entry) Infof(msg string, v ...interface{}) { 122 | e.Info(fmt.Sprintf(msg, v...)) 123 | } 124 | 125 | // Warnf level formatted message. 126 | func (e *Entry) Warnf(msg string, v ...interface{}) { 127 | e.Warn(fmt.Sprintf(msg, v...)) 128 | } 129 | 130 | // Errorf level formatted message. 131 | func (e *Entry) Errorf(msg string, v ...interface{}) { 132 | e.Error(fmt.Sprintf(msg, v...)) 133 | } 134 | 135 | // Fatalf level formatted message, followed by an exit. 136 | func (e *Entry) Fatalf(msg string, v ...interface{}) { 137 | e.Fatal(fmt.Sprintf(msg, v...)) 138 | } 139 | 140 | // Trace returns a new entry with a Stop method to fire off 141 | // a corresponding completion log, useful with defer. 142 | func (e *Entry) Trace(msg string) *Entry { 143 | e.Info(msg) 144 | v := e.WithFields(e.Fields) 145 | v.Message = msg 146 | v.start = time.Now() 147 | return v 148 | } 149 | 150 | // Stop should be used with Trace, to fire off the completion message. When 151 | // an `err` is passed the "error" field is set, and the log level is error. 152 | func (e *Entry) Stop(err *error) { 153 | if err == nil || *err == nil { 154 | e.WithDuration(time.Since(e.start)).Info(e.Message) 155 | } else { 156 | e.WithDuration(time.Since(e.start)).WithError(*err).Error(e.Message) 157 | } 158 | } 159 | 160 | // mergedFields returns the fields list collapsed into a single map. 161 | func (e *Entry) mergedFields() Fields { 162 | f := Fields{} 163 | 164 | for _, fields := range e.fields { 165 | for k, v := range fields { 166 | f[k] = v 167 | } 168 | } 169 | 170 | return f 171 | } 172 | 173 | // finalize returns a copy of the Entry with Fields merged. 174 | func (e *Entry) finalize(level Level, msg string) *Entry { 175 | return &Entry{ 176 | Logger: e.Logger, 177 | Fields: e.mergedFields(), 178 | Level: level, 179 | Message: msg, 180 | Timestamp: Now(), 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /entry_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEntry_WithFields(t *testing.T) { 12 | a := NewEntry(nil) 13 | assert.Nil(t, a.Fields) 14 | 15 | b := a.WithFields(Fields{"foo": "bar"}) 16 | assert.Equal(t, Fields{}, a.mergedFields()) 17 | assert.Equal(t, Fields{"foo": "bar"}, b.mergedFields()) 18 | 19 | c := a.WithFields(Fields{"foo": "hello", "bar": "world"}) 20 | 21 | e := c.finalize(InfoLevel, "upload") 22 | assert.Equal(t, e.Message, "upload") 23 | assert.Equal(t, e.Fields, Fields{"foo": "hello", "bar": "world"}) 24 | assert.Equal(t, e.Level, InfoLevel) 25 | assert.NotEmpty(t, e.Timestamp) 26 | } 27 | 28 | func TestEntry_WithField(t *testing.T) { 29 | a := NewEntry(nil) 30 | b := a.WithField("foo", "bar") 31 | assert.Equal(t, Fields{}, a.mergedFields()) 32 | assert.Equal(t, Fields{"foo": "bar"}, b.mergedFields()) 33 | } 34 | 35 | func TestEntry_WithError(t *testing.T) { 36 | a := NewEntry(nil) 37 | b := a.WithError(fmt.Errorf("boom")) 38 | assert.Equal(t, Fields{}, a.mergedFields()) 39 | assert.Equal(t, Fields{"error": "boom"}, b.mergedFields()) 40 | } 41 | 42 | func TestEntry_WithError_fields(t *testing.T) { 43 | a := NewEntry(nil) 44 | b := a.WithError(errFields("boom")) 45 | assert.Equal(t, Fields{}, a.mergedFields()) 46 | assert.Equal(t, Fields{ 47 | "error": "boom", 48 | "reason": "timeout", 49 | }, b.mergedFields()) 50 | } 51 | 52 | func TestEntry_WithError_nil(t *testing.T) { 53 | a := NewEntry(nil) 54 | b := a.WithError(nil) 55 | assert.Equal(t, Fields{}, a.mergedFields()) 56 | assert.Equal(t, Fields{}, b.mergedFields()) 57 | } 58 | 59 | func TestEntry_WithDuration(t *testing.T) { 60 | a := NewEntry(nil) 61 | b := a.WithDuration(time.Second * 2) 62 | assert.Equal(t, Fields{"duration": int64(2000)}, b.mergedFields()) 63 | } 64 | 65 | type errFields string 66 | 67 | func (ef errFields) Error() string { 68 | return string(ef) 69 | } 70 | 71 | func (ef errFields) Fields() Fields { 72 | return Fields{"reason": "timeout"} 73 | } 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/apex/log 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/apex/logs v1.0.0 7 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a 8 | github.com/aphistic/sweet v0.2.0 // indirect 9 | github.com/aws/aws-sdk-go v1.20.6 10 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 11 | github.com/fatih/color v1.7.0 12 | github.com/go-logfmt/logfmt v0.4.0 13 | github.com/golang/protobuf v1.3.1 // indirect 14 | github.com/google/uuid v1.1.1 // indirect 15 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 // indirect 16 | github.com/kr/pretty v0.2.0 // indirect 17 | github.com/mattn/go-colorable v0.1.2 18 | github.com/pkg/errors v0.9.1 19 | github.com/rogpeppe/fastuuid v1.1.0 20 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect 21 | github.com/smartystreets/gunit v1.0.0 // indirect 22 | github.com/stretchr/testify v1.6.1 23 | github.com/tj/assert v0.0.3 24 | github.com/tj/go-buffer v1.1.0 25 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 26 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b 27 | github.com/tj/go-spin v1.1.0 28 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect 29 | golang.org/x/text v0.3.2 // indirect 30 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 31 | gopkg.in/yaml.v2 v2.2.2 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apex/logs v1.0.0 h1:adOwhOTeXzZTnVuEK13wuJNBFutP0sOfutRS8NY+G6A= 2 | github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 3 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a h1:2KLQMJ8msqoPHIPDufkxVcoTtcmE5+1sL9950m4R9Pk= 4 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 5 | github.com/aphistic/sweet v0.2.0 h1:I4z+fAUqvKfvZV/CHi5dV0QuwbmIvYYFDjG0Ss5QpAs= 6 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 7 | github.com/aws/aws-sdk-go v1.20.6 h1:kmy4Gvdlyez1fV4kw5RYxZzWKVyuHZHgPWeU/YvRsV4= 8 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 9 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= 10 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 11 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 16 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 17 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 18 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 19 | github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= 20 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 21 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 22 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 24 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 26 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 28 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 29 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 30 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 31 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= 32 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 33 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= 34 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 35 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 36 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 41 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 42 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 43 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 44 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 45 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 46 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 47 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 48 | github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= 49 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 50 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 51 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/rogpeppe/fastuuid v1.1.0 h1:INyGLmTCMGFr6OVIb977ghJvABML2CMVjPoRfNDdYDo= 57 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 58 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 59 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 60 | github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= 61 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 62 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= 63 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 64 | github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI= 65 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 68 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 69 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 70 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA= 72 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 73 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 74 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 75 | github.com/tj/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M= 76 | github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= 77 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc= 78 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 79 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w= 80 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 81 | github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds= 82 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 84 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= 85 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 86 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 87 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 89 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 91 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 93 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 95 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 97 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 99 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 100 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 101 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 106 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 108 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 109 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 110 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 111 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 112 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 113 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 114 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 115 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 116 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= 117 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | -------------------------------------------------------------------------------- /handlers/apexlogs/apexlogs.go: -------------------------------------------------------------------------------- 1 | // Package apexlogs implements a handler for Apex Logs https://apex.sh/logs/. 2 | package apexlogs 3 | 4 | import ( 5 | "context" 6 | stdlog "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/tj/go-buffer" 11 | 12 | "github.com/apex/log" 13 | "github.com/apex/logs" 14 | ) 15 | 16 | // logger instance. 17 | var logger = stdlog.New(os.Stderr, "buffer ", stdlog.LstdFlags) 18 | 19 | // levelMap is a mapping of severity levels. 20 | var levelMap = map[log.Level]string{ 21 | log.DebugLevel: "debug", 22 | log.InfoLevel: "info", 23 | log.WarnLevel: "warning", 24 | log.ErrorLevel: "error", 25 | log.FatalLevel: "emergency", 26 | } 27 | 28 | // Handler implementation. 29 | type Handler struct { 30 | projectID string 31 | httpClient *http.Client 32 | bufferOptions []buffer.Option 33 | 34 | b *buffer.Buffer 35 | c logs.Client 36 | } 37 | 38 | // Option function. 39 | type Option func(*Handler) 40 | 41 | // New Apex Logs handler with the url, projectID, authToken and options. 42 | func New(url, projectID, authToken string, options ...Option) *Handler { 43 | var v Handler 44 | v.projectID = projectID 45 | 46 | // options 47 | for _, o := range options { 48 | o(&v) 49 | } 50 | 51 | // logs client 52 | v.c = logs.Client{ 53 | URL: url, 54 | AuthToken: authToken, 55 | HTTPClient: v.httpClient, 56 | } 57 | 58 | // event buffer 59 | var o []buffer.Option 60 | o = append(o, buffer.WithFlushHandler(v.handleFlush)) 61 | o = append(o, buffer.WithErrorHandler(v.handleError)) 62 | o = append(o, v.bufferOptions...) 63 | v.b = buffer.New(o...) 64 | 65 | return &v 66 | } 67 | 68 | // WithHTTPClient sets the HTTP client used for requests. 69 | func WithHTTPClient(client *http.Client) Option { 70 | return func(v *Handler) { 71 | v.httpClient = client 72 | } 73 | } 74 | 75 | // WithBufferOptions sets options for the underlying buffer used to batch logs. 76 | func WithBufferOptions(options ...buffer.Option) Option { 77 | return func(v *Handler) { 78 | v.bufferOptions = options 79 | } 80 | } 81 | 82 | // HandleLog implements log.Handler. 83 | func (h *Handler) HandleLog(e *log.Entry) error { 84 | h.b.Push(logs.Event{ 85 | Level: levelMap[e.Level], 86 | Message: e.Message, 87 | Fields: map[string]interface{}(e.Fields), 88 | Timestamp: e.Timestamp, 89 | }) 90 | 91 | return nil 92 | } 93 | 94 | // Flush any pending logs. This method is non-blocking. 95 | func (h *Handler) Flush() { 96 | h.b.Flush() 97 | } 98 | 99 | // FlushSync any pending logs. This method is blocking. 100 | func (h *Handler) FlushSync() { 101 | h.b.FlushSync() 102 | } 103 | 104 | // Close flushes any pending logs, and waits for flushing to complete. This 105 | // method should be called before exiting your program to ensure entries have 106 | // flushed properly. 107 | func (h *Handler) Close() { 108 | h.b.Close() 109 | } 110 | 111 | // handleFlush implementation. 112 | func (h *Handler) handleFlush(ctx context.Context, values []interface{}) error { 113 | var events []logs.Event 114 | 115 | for _, v := range values { 116 | events = append(events, v.(logs.Event)) 117 | } 118 | 119 | if len(events) == 0 { 120 | return nil 121 | } 122 | 123 | return h.c.AddEvents(logs.AddEventsInput{ 124 | ProjectID: h.projectID, 125 | Events: events, 126 | }) 127 | } 128 | 129 | // handleError implementation. 130 | func (h *Handler) handleError(err error) { 131 | logger.Printf("error flushing logs: %v", err) 132 | } 133 | -------------------------------------------------------------------------------- /handlers/cli/cli.go: -------------------------------------------------------------------------------- 1 | // Package cli implements a colored text handler suitable for command-line interfaces. 2 | package cli 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/apex/log" 12 | "github.com/fatih/color" 13 | colorable "github.com/mattn/go-colorable" 14 | ) 15 | 16 | // Default handler outputting to stderr. 17 | var Default = New(os.Stderr) 18 | 19 | // start time. 20 | var start = time.Now() 21 | 22 | var bold = color.New(color.Bold) 23 | 24 | // Colors mapping. 25 | var Colors = [...]*color.Color{ 26 | log.DebugLevel: color.New(color.FgWhite), 27 | log.InfoLevel: color.New(color.FgBlue), 28 | log.WarnLevel: color.New(color.FgYellow), 29 | log.ErrorLevel: color.New(color.FgRed), 30 | log.FatalLevel: color.New(color.FgRed), 31 | } 32 | 33 | // Strings mapping. 34 | var Strings = [...]string{ 35 | log.DebugLevel: "•", 36 | log.InfoLevel: "•", 37 | log.WarnLevel: "•", 38 | log.ErrorLevel: "⨯", 39 | log.FatalLevel: "⨯", 40 | } 41 | 42 | // Handler implementation. 43 | type Handler struct { 44 | mu sync.Mutex 45 | Writer io.Writer 46 | Padding int 47 | } 48 | 49 | // New handler. 50 | func New(w io.Writer) *Handler { 51 | if f, ok := w.(*os.File); ok { 52 | return &Handler{ 53 | Writer: colorable.NewColorable(f), 54 | Padding: 3, 55 | } 56 | } 57 | 58 | return &Handler{ 59 | Writer: w, 60 | Padding: 3, 61 | } 62 | } 63 | 64 | // HandleLog implements log.Handler. 65 | func (h *Handler) HandleLog(e *log.Entry) error { 66 | color := Colors[e.Level] 67 | level := Strings[e.Level] 68 | names := e.Fields.Names() 69 | 70 | h.mu.Lock() 71 | defer h.mu.Unlock() 72 | 73 | color.Fprintf(h.Writer, "%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message) 74 | 75 | for _, name := range names { 76 | if name == "source" { 77 | continue 78 | } 79 | fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name)) 80 | } 81 | 82 | fmt.Fprintln(h.Writer) 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /handlers/delta/delta.go: -------------------------------------------------------------------------------- 1 | // Package delta provides a log handler which times the delta 2 | // between each log call, useful for debug output for command-line 3 | // programs. 4 | package delta 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "time" 11 | 12 | "github.com/apex/log" 13 | "github.com/aybabtme/rgbterm" 14 | "github.com/tj/go-spin" 15 | ) 16 | 17 | // TODO: move colors and share in text handler etc 18 | 19 | // color function. 20 | type colorFunc func(string) string 21 | 22 | // gray string. 23 | func gray(s string) string { 24 | return rgbterm.FgString(s, 150, 150, 150) 25 | } 26 | 27 | // blue string. 28 | func blue(s string) string { 29 | return rgbterm.FgString(s, 77, 173, 247) 30 | } 31 | 32 | // cyan string. 33 | func cyan(s string) string { 34 | return rgbterm.FgString(s, 34, 184, 207) 35 | } 36 | 37 | // green string. 38 | func green(s string) string { 39 | return rgbterm.FgString(s, 0, 200, 255) 40 | } 41 | 42 | // red string. 43 | func red(s string) string { 44 | return rgbterm.FgString(s, 194, 37, 92) 45 | } 46 | 47 | // yellow string. 48 | func yellow(s string) string { 49 | return rgbterm.FgString(s, 252, 196, 25) 50 | } 51 | 52 | // Colors mapping. 53 | var Colors = [...]colorFunc{ 54 | log.DebugLevel: gray, 55 | log.InfoLevel: blue, 56 | log.WarnLevel: yellow, 57 | log.ErrorLevel: red, 58 | log.FatalLevel: red, 59 | } 60 | 61 | // Strings mapping. 62 | var Strings = [...]string{ 63 | log.DebugLevel: "DEBU", 64 | log.InfoLevel: "INFO", 65 | log.WarnLevel: "WARN", 66 | log.ErrorLevel: "ERRO", 67 | log.FatalLevel: "FATA", 68 | } 69 | 70 | // Default handler. 71 | var Default = New(os.Stderr) 72 | 73 | // Handler implementation. 74 | type Handler struct { 75 | entries chan *log.Entry 76 | start time.Time 77 | spin *spin.Spinner 78 | prev *log.Entry 79 | done chan struct{} 80 | w io.Writer 81 | } 82 | 83 | // New handler. 84 | func New(w io.Writer) *Handler { 85 | h := &Handler{ 86 | entries: make(chan *log.Entry), 87 | done: make(chan struct{}), 88 | start: time.Now(), 89 | spin: spin.New(), 90 | w: w, 91 | } 92 | 93 | go h.loop() 94 | 95 | return h 96 | } 97 | 98 | // Close the handler. 99 | func (h *Handler) Close() error { 100 | h.done <- struct{}{} 101 | close(h.done) 102 | close(h.entries) 103 | return nil 104 | } 105 | 106 | // loop for rendering. 107 | func (h *Handler) loop() { 108 | ticker := time.NewTicker(100 * time.Millisecond) 109 | 110 | for { 111 | select { 112 | case e := <-h.entries: 113 | if h.prev != nil { 114 | h.render(h.prev, true) 115 | } 116 | h.render(e, false) 117 | h.prev = e 118 | case <-ticker.C: 119 | if h.prev != nil { 120 | h.render(h.prev, false) 121 | } 122 | h.spin.Next() 123 | case <-h.done: 124 | ticker.Stop() 125 | if h.prev != nil { 126 | h.render(h.prev, true) 127 | } 128 | return 129 | } 130 | } 131 | } 132 | 133 | func (h *Handler) render(e *log.Entry, done bool) { 134 | color := Colors[e.Level] 135 | level := Strings[e.Level] 136 | names := e.Fields.Names() 137 | 138 | // delta and spinner 139 | if done { 140 | fmt.Fprintf(h.w, "\r %-7s", time.Since(h.start).Round(time.Millisecond)) 141 | } else { 142 | fmt.Fprintf(h.w, "\r %s %-7s", h.spin.Current(), time.Since(h.start).Round(time.Millisecond)) 143 | } 144 | 145 | // message 146 | fmt.Fprintf(h.w, " %s %s", color(level), color(e.Message)) 147 | 148 | // fields 149 | for _, name := range names { 150 | v := e.Fields.Get(name) 151 | 152 | if v == "" { 153 | continue 154 | } 155 | 156 | fmt.Fprintf(h.w, " %s%s%v", color(name), gray("="), v) 157 | } 158 | 159 | // newline 160 | if done { 161 | fmt.Fprintf(h.w, "\n") 162 | h.start = time.Now() 163 | } 164 | } 165 | 166 | // HandleLog implements log.Handler. 167 | func (h *Handler) HandleLog(e *log.Entry) error { 168 | h.entries <- e 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /handlers/discard/discard.go: -------------------------------------------------------------------------------- 1 | // Package discard implements a no-op handler useful for benchmarks and tests. 2 | package discard 3 | 4 | import ( 5 | "github.com/apex/log" 6 | ) 7 | 8 | // Default handler. 9 | var Default = New() 10 | 11 | // Handler implementation. 12 | type Handler struct{} 13 | 14 | // New handler. 15 | func New() *Handler { 16 | return &Handler{} 17 | } 18 | 19 | // HandleLog implements log.Handler. 20 | func (h *Handler) HandleLog(e *log.Entry) error { 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /handlers/es/es.go: -------------------------------------------------------------------------------- 1 | // Package es implements an Elasticsearch batch handler. Currently this implementation 2 | // assumes the index format of "logs-YY-MM-DD". 3 | package es 4 | 5 | import ( 6 | "io" 7 | stdlog "log" 8 | "sync" 9 | "time" 10 | 11 | "github.com/tj/go-elastic/batch" 12 | 13 | "github.com/apex/log" 14 | ) 15 | 16 | // TODO(tj): allow dumping logs to stderr on timeout 17 | // TODO(tj): allow custom format that does not include .fields etc 18 | // TODO(tj): allow interval flushes 19 | // TODO(tj): allow explicit Flush() (for Lambda where you have to flush at the end of function) 20 | 21 | // Elasticsearch interface. 22 | type Elasticsearch interface { 23 | Bulk(io.Reader) error 24 | } 25 | 26 | // Config for handler. 27 | type Config struct { 28 | BufferSize int // BufferSize is the number of logs to buffer before flush (default: 100) 29 | Format string // Format for index 30 | Client Elasticsearch // Client for ES 31 | } 32 | 33 | // defaults applies defaults to the config. 34 | func (c *Config) defaults() { 35 | if c.BufferSize == 0 { 36 | c.BufferSize = 100 37 | } 38 | 39 | if c.Format == "" { 40 | c.Format = "logs-06-01-02" 41 | } 42 | } 43 | 44 | // Handler implementation. 45 | type Handler struct { 46 | *Config 47 | 48 | mu sync.Mutex 49 | batch *batch.Batch 50 | } 51 | 52 | // New handler with BufferSize 53 | func New(config *Config) *Handler { 54 | config.defaults() 55 | return &Handler{ 56 | Config: config, 57 | } 58 | } 59 | 60 | // HandleLog implements log.Handler. 61 | func (h *Handler) HandleLog(e *log.Entry) error { 62 | h.mu.Lock() 63 | defer h.mu.Unlock() 64 | 65 | if h.batch == nil { 66 | h.batch = &batch.Batch{ 67 | Index: time.Now().Format(h.Config.Format), 68 | Elastic: h.Client, 69 | Type: "log", 70 | } 71 | } 72 | 73 | h.batch.Add(e) 74 | 75 | if h.batch.Size() >= h.BufferSize { 76 | go h.flush(h.batch) 77 | h.batch = nil 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // flush the given `batch` asynchronously. 84 | func (h *Handler) flush(batch *batch.Batch) { 85 | size := batch.Size() 86 | start := time.Now() 87 | stdlog.Printf("log/elastic: flushing %d logs", size) 88 | 89 | if err := batch.Flush(); err != nil { 90 | stdlog.Printf("log/elastic: failed to flush %d logs: %s", size, err) 91 | } 92 | 93 | stdlog.Printf("log/elastic: flushed %d logs in %s", size, time.Since(start)) 94 | } 95 | -------------------------------------------------------------------------------- /handlers/graylog/graylog.go: -------------------------------------------------------------------------------- 1 | // Package implements a Graylog-backed handler. 2 | package graylog 3 | 4 | import ( 5 | "github.com/apex/log" 6 | "github.com/aphistic/golf" 7 | ) 8 | 9 | // Handler implementation. 10 | type Handler struct { 11 | logger *golf.Logger 12 | client *golf.Client 13 | } 14 | 15 | // New handler. 16 | // Connection string should be in format "udp://:". 17 | // Server should have GELF input enabled on that port. 18 | func New(url string) (*Handler, error) { 19 | c, err := golf.NewClient() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | err = c.Dial(url) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | l, err := c.NewLogger() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &Handler{ 35 | logger: l, 36 | client: c, 37 | }, nil 38 | } 39 | 40 | // HandleLog implements log.Handler. 41 | func (h *Handler) HandleLog(e *log.Entry) error { 42 | switch e.Level { 43 | case log.DebugLevel: 44 | return h.logger.Dbgm(e.Fields, e.Message) 45 | case log.InfoLevel: 46 | return h.logger.Infom(e.Fields, e.Message) 47 | case log.WarnLevel: 48 | return h.logger.Warnm(e.Fields, e.Message) 49 | case log.ErrorLevel: 50 | return h.logger.Errm(e.Fields, e.Message) 51 | case log.FatalLevel: 52 | return h.logger.Critm(e.Fields, e.Message) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Closes connection to server, flushing message queue. 59 | func (h *Handler) Close() error { 60 | return h.client.Close() 61 | } 62 | -------------------------------------------------------------------------------- /handlers/json/json.go: -------------------------------------------------------------------------------- 1 | // Package json implements a JSON handler. 2 | package json 3 | 4 | import ( 5 | j "encoding/json" 6 | "io" 7 | "os" 8 | "sync" 9 | 10 | "github.com/apex/log" 11 | ) 12 | 13 | // Default handler outputting to stderr. 14 | var Default = New(os.Stderr) 15 | 16 | // Handler implementation. 17 | type Handler struct { 18 | *j.Encoder 19 | mu sync.Mutex 20 | } 21 | 22 | // New handler. 23 | func New(w io.Writer) *Handler { 24 | return &Handler{ 25 | Encoder: j.NewEncoder(w), 26 | } 27 | } 28 | 29 | // HandleLog implements log.Handler. 30 | func (h *Handler) HandleLog(e *log.Entry) error { 31 | h.mu.Lock() 32 | defer h.mu.Unlock() 33 | return h.Encoder.Encode(e) 34 | } 35 | -------------------------------------------------------------------------------- /handlers/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/apex/log" 11 | "github.com/apex/log/handlers/json" 12 | ) 13 | 14 | func init() { 15 | log.Now = func() time.Time { 16 | return time.Unix(0, 0).UTC() 17 | } 18 | } 19 | 20 | func Test(t *testing.T) { 21 | var buf bytes.Buffer 22 | 23 | log.SetHandler(json.New(&buf)) 24 | log.WithField("user", "tj").WithField("id", "123").Info("hello") 25 | log.Info("world") 26 | log.Error("boom") 27 | 28 | expected := `{"fields":{"id":"123","user":"tj"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"hello"} 29 | {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"world"} 30 | {"fields":{},"level":"error","timestamp":"1970-01-01T00:00:00Z","message":"boom"} 31 | ` 32 | 33 | assert.Equal(t, expected, buf.String()) 34 | } 35 | -------------------------------------------------------------------------------- /handlers/kinesis/kinesis.go: -------------------------------------------------------------------------------- 1 | package kinesis 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | 7 | "github.com/apex/log" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/kinesis" 11 | "github.com/rogpeppe/fastuuid" 12 | k "github.com/tj/go-kinesis" 13 | ) 14 | 15 | // Handler implementation. 16 | type Handler struct { 17 | appName string 18 | producer *k.Producer 19 | gen *fastuuid.Generator 20 | } 21 | 22 | // New handler sending logs to Kinesis. To configure producer options or pass your 23 | // own AWS Kinesis client use NewConfig instead. 24 | func New(stream string) *Handler { 25 | return NewConfig(k.Config{ 26 | StreamName: stream, 27 | Client: kinesis.New(session.New(aws.NewConfig())), 28 | }) 29 | } 30 | 31 | // NewConfig handler sending logs to Kinesis. The `config` given is passed to the batch 32 | // Kinesis producer, and a random value is used as the partition key for even distribution. 33 | func NewConfig(config k.Config) *Handler { 34 | producer := k.New(config) 35 | producer.Start() 36 | return &Handler{ 37 | producer: producer, 38 | gen: fastuuid.MustNewGenerator(), 39 | } 40 | } 41 | 42 | // HandleLog implements log.Handler. 43 | func (h *Handler) HandleLog(e *log.Entry) error { 44 | b, err := json.Marshal(e) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | uuid := h.gen.Next() 50 | key := base64.StdEncoding.EncodeToString(uuid[:]) 51 | return h.producer.Put(b, key) 52 | } 53 | -------------------------------------------------------------------------------- /handlers/level/level.go: -------------------------------------------------------------------------------- 1 | // Package level implements a level filter handler. 2 | package level 3 | 4 | import "github.com/apex/log" 5 | 6 | // Handler implementation. 7 | type Handler struct { 8 | Level log.Level 9 | Handler log.Handler 10 | } 11 | 12 | // New handler. 13 | func New(h log.Handler, level log.Level) *Handler { 14 | return &Handler{ 15 | Level: level, 16 | Handler: h, 17 | } 18 | } 19 | 20 | // HandleLog implements log.Handler. 21 | func (h *Handler) HandleLog(e *log.Entry) error { 22 | if e.Level < h.Level { 23 | return nil 24 | } 25 | 26 | return h.Handler.HandleLog(e) 27 | } 28 | -------------------------------------------------------------------------------- /handlers/level/level_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/level" 10 | "github.com/apex/log/handlers/memory" 11 | ) 12 | 13 | func Test(t *testing.T) { 14 | h := memory.New() 15 | 16 | ctx := log.Logger{ 17 | Handler: level.New(h, log.ErrorLevel), 18 | Level: log.InfoLevel, 19 | } 20 | 21 | ctx.Info("hello") 22 | ctx.Info("world") 23 | ctx.Error("boom") 24 | 25 | assert.Len(t, h.Entries, 1) 26 | assert.Equal(t, h.Entries[0].Message, "boom") 27 | } 28 | -------------------------------------------------------------------------------- /handlers/logfmt/logfmt.go: -------------------------------------------------------------------------------- 1 | // Package logfmt implements a "logfmt" format handler. 2 | package logfmt 3 | 4 | import ( 5 | "io" 6 | "os" 7 | "sync" 8 | 9 | "github.com/apex/log" 10 | "github.com/go-logfmt/logfmt" 11 | ) 12 | 13 | // Default handler outputting to stderr. 14 | var Default = New(os.Stderr) 15 | 16 | // Handler implementation. 17 | type Handler struct { 18 | mu sync.Mutex 19 | enc *logfmt.Encoder 20 | } 21 | 22 | // New handler. 23 | func New(w io.Writer) *Handler { 24 | return &Handler{ 25 | enc: logfmt.NewEncoder(w), 26 | } 27 | } 28 | 29 | // HandleLog implements log.Handler. 30 | func (h *Handler) HandleLog(e *log.Entry) error { 31 | names := e.Fields.Names() 32 | 33 | h.mu.Lock() 34 | defer h.mu.Unlock() 35 | 36 | h.enc.EncodeKeyval("timestamp", e.Timestamp) 37 | h.enc.EncodeKeyval("level", e.Level.String()) 38 | h.enc.EncodeKeyval("message", e.Message) 39 | 40 | for _, name := range names { 41 | h.enc.EncodeKeyval(name, e.Fields.Get(name)) 42 | } 43 | 44 | h.enc.EndRecord() 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /handlers/logfmt/logfmt_test.go: -------------------------------------------------------------------------------- 1 | package logfmt_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/apex/log" 12 | "github.com/apex/log/handlers/logfmt" 13 | ) 14 | 15 | func init() { 16 | log.Now = func() time.Time { 17 | return time.Unix(0, 0).UTC() 18 | } 19 | } 20 | 21 | func Test(t *testing.T) { 22 | var buf bytes.Buffer 23 | 24 | log.SetHandler(logfmt.New(&buf)) 25 | log.WithField("user", "tj").WithField("id", "123").Info("hello") 26 | log.Info("world") 27 | log.Error("boom") 28 | 29 | expected := `timestamp=1970-01-01T00:00:00Z level=info message=hello id=123 user=tj 30 | timestamp=1970-01-01T00:00:00Z level=info message=world 31 | timestamp=1970-01-01T00:00:00Z level=error message=boom 32 | ` 33 | 34 | assert.Equal(t, expected, buf.String()) 35 | } 36 | 37 | func Benchmark(b *testing.B) { 38 | log.SetHandler(logfmt.New(ioutil.Discard)) 39 | ctx := log.WithField("user", "tj").WithField("id", "123") 40 | 41 | for i := 0; i < b.N; i++ { 42 | ctx.Info("hello") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /handlers/memory/memory.go: -------------------------------------------------------------------------------- 1 | // Package memory implements an in-memory handler useful for testing, as the 2 | // entries can be accessed after writes. 3 | package memory 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/apex/log" 9 | ) 10 | 11 | // Handler implementation. 12 | type Handler struct { 13 | mu sync.Mutex 14 | Entries []*log.Entry 15 | } 16 | 17 | // New handler. 18 | func New() *Handler { 19 | return &Handler{} 20 | } 21 | 22 | // HandleLog implements log.Handler. 23 | func (h *Handler) HandleLog(e *log.Entry) error { 24 | h.mu.Lock() 25 | defer h.mu.Unlock() 26 | h.Entries = append(h.Entries, e) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /handlers/multi/multi.go: -------------------------------------------------------------------------------- 1 | // Package multi implements a handler which invokes a number of handlers. 2 | package multi 3 | 4 | import ( 5 | "github.com/apex/log" 6 | ) 7 | 8 | // Handler implementation. 9 | type Handler struct { 10 | Handlers []log.Handler 11 | } 12 | 13 | // New handler. 14 | func New(h ...log.Handler) *Handler { 15 | return &Handler{ 16 | Handlers: h, 17 | } 18 | } 19 | 20 | // HandleLog implements log.Handler. 21 | func (h *Handler) HandleLog(e *log.Entry) error { 22 | for _, handler := range h.Handlers { 23 | // TODO(tj): maybe just write to stderr here, definitely not ideal 24 | // to miss out logging to a more critical handler if something 25 | // goes wrong 26 | if err := handler.HandleLog(e); err != nil { 27 | return err 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /handlers/multi/multi_test.go: -------------------------------------------------------------------------------- 1 | package multi_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/apex/log" 10 | "github.com/apex/log/handlers/memory" 11 | "github.com/apex/log/handlers/multi" 12 | ) 13 | 14 | func init() { 15 | log.Now = func() time.Time { 16 | return time.Unix(0, 0) 17 | } 18 | } 19 | 20 | func Test(t *testing.T) { 21 | a := memory.New() 22 | b := memory.New() 23 | 24 | log.SetHandler(multi.New(a, b)) 25 | log.WithField("user", "tj").WithField("id", "123").Info("hello") 26 | log.Info("world") 27 | log.Error("boom") 28 | 29 | assert.Len(t, a.Entries, 3) 30 | assert.Len(t, b.Entries, 3) 31 | } 32 | -------------------------------------------------------------------------------- /handlers/papertrail/papertrail.go: -------------------------------------------------------------------------------- 1 | // Package papertrail implements a papertrail logfmt format handler. 2 | package papertrail 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "log/syslog" 8 | "net" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/apex/log" 14 | "github.com/go-logfmt/logfmt" 15 | ) 16 | 17 | // TODO: syslog portion is ad-hoc for my serverless use-case, 18 | // I don't really need hostnames etc, but this should be improved 19 | 20 | // Config for Papertrail. 21 | type Config struct { 22 | // Papertrail settings. 23 | Host string // Host subdomain such as "logs4" 24 | Port int // Port number 25 | 26 | // Application settings 27 | Hostname string // Hostname value 28 | Tag string // Tag value 29 | } 30 | 31 | // Handler implementation. 32 | type Handler struct { 33 | *Config 34 | 35 | mu sync.Mutex 36 | conn net.Conn 37 | } 38 | 39 | // New handler. 40 | func New(config *Config) *Handler { 41 | conn, err := net.Dial("udp", fmt.Sprintf("%s.papertrailapp.com:%d", config.Host, config.Port)) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | return &Handler{ 47 | Config: config, 48 | conn: conn, 49 | } 50 | } 51 | 52 | // HandleLog implements log.Handler. 53 | func (h *Handler) HandleLog(e *log.Entry) error { 54 | ts := time.Now().Format(time.Stamp) 55 | 56 | var buf bytes.Buffer 57 | 58 | enc := logfmt.NewEncoder(&buf) 59 | enc.EncodeKeyval("level", e.Level.String()) 60 | enc.EncodeKeyval("message", e.Message) 61 | 62 | for k, v := range e.Fields { 63 | enc.EncodeKeyval(k, v) 64 | } 65 | 66 | enc.EndRecord() 67 | 68 | msg := []byte(fmt.Sprintf("<%d>%s %s %s[%d]: %s\n", syslog.LOG_KERN, ts, h.Hostname, h.Tag, os.Getpid(), buf.String())) 69 | 70 | h.mu.Lock() 71 | _, err := h.conn.Write(msg) 72 | h.mu.Unlock() 73 | 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /handlers/text/text.go: -------------------------------------------------------------------------------- 1 | // Package text implements a development-friendly textual handler. 2 | package text 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/apex/log" 12 | ) 13 | 14 | // Default handler outputting to stderr. 15 | var Default = New(os.Stderr) 16 | 17 | // start time. 18 | var start = time.Now() 19 | 20 | // colors. 21 | const ( 22 | none = 0 23 | red = 31 24 | green = 32 25 | yellow = 33 26 | blue = 34 27 | gray = 37 28 | ) 29 | 30 | // Colors mapping. 31 | var Colors = [...]int{ 32 | log.DebugLevel: gray, 33 | log.InfoLevel: blue, 34 | log.WarnLevel: yellow, 35 | log.ErrorLevel: red, 36 | log.FatalLevel: red, 37 | } 38 | 39 | // Strings mapping. 40 | var Strings = [...]string{ 41 | log.DebugLevel: "DEBUG", 42 | log.InfoLevel: "INFO", 43 | log.WarnLevel: "WARN", 44 | log.ErrorLevel: "ERROR", 45 | log.FatalLevel: "FATAL", 46 | } 47 | 48 | // Handler implementation. 49 | type Handler struct { 50 | mu sync.Mutex 51 | Writer io.Writer 52 | } 53 | 54 | // New handler. 55 | func New(w io.Writer) *Handler { 56 | return &Handler{ 57 | Writer: w, 58 | } 59 | } 60 | 61 | // HandleLog implements log.Handler. 62 | func (h *Handler) HandleLog(e *log.Entry) error { 63 | color := Colors[e.Level] 64 | level := Strings[e.Level] 65 | names := e.Fields.Names() 66 | 67 | h.mu.Lock() 68 | defer h.mu.Unlock() 69 | 70 | ts := time.Since(start) / time.Second 71 | fmt.Fprintf(h.Writer, "\033[%dm%6s\033[0m[%04d] %-25s", color, level, ts, e.Message) 72 | 73 | for _, name := range names { 74 | fmt.Fprintf(h.Writer, " \033[%dm%s\033[0m=%v", color, name, e.Fields.Get(name)) 75 | } 76 | 77 | fmt.Fprintln(h.Writer) 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /handlers/text/text_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/apex/log" 11 | "github.com/apex/log/handlers/text" 12 | ) 13 | 14 | func init() { 15 | log.Now = func() time.Time { 16 | return time.Unix(0, 0) 17 | } 18 | } 19 | 20 | func Test(t *testing.T) { 21 | var buf bytes.Buffer 22 | 23 | log.SetHandler(text.New(&buf)) 24 | log.WithField("user", "tj").WithField("id", "123").Info("hello") 25 | log.WithField("user", "tj").Info("world") 26 | log.WithField("user", "tj").Error("boom") 27 | 28 | expected := "\x1b[34m INFO\x1b[0m[0000] hello \x1b[34mid\x1b[0m=123 \x1b[34muser\x1b[0m=tj\n\x1b[34m INFO\x1b[0m[0000] world \x1b[34muser\x1b[0m=tj\n\x1b[31m ERROR\x1b[0m[0000] boom \x1b[31muser\x1b[0m=tj\n" 29 | 30 | assert.Equal(t, expected, buf.String()) 31 | } 32 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "time" 4 | 5 | // Interface represents the API of both Logger and Entry. 6 | type Interface interface { 7 | WithFields(Fielder) *Entry 8 | WithField(string, interface{}) *Entry 9 | WithDuration(time.Duration) *Entry 10 | WithError(error) *Entry 11 | Debug(string) 12 | Info(string) 13 | Warn(string) 14 | Error(string) 15 | Fatal(string) 16 | Debugf(string, ...interface{}) 17 | Infof(string, ...interface{}) 18 | Warnf(string, ...interface{}) 19 | Errorf(string, ...interface{}) 20 | Fatalf(string, ...interface{}) 21 | Trace(string) *Entry 22 | } 23 | -------------------------------------------------------------------------------- /levels.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidLevel is returned if the severity level is invalid. 10 | var ErrInvalidLevel = errors.New("invalid level") 11 | 12 | // Level of severity. 13 | type Level int 14 | 15 | // Log levels. 16 | const ( 17 | InvalidLevel Level = iota - 1 18 | DebugLevel 19 | InfoLevel 20 | WarnLevel 21 | ErrorLevel 22 | FatalLevel 23 | ) 24 | 25 | var levelNames = [...]string{ 26 | DebugLevel: "debug", 27 | InfoLevel: "info", 28 | WarnLevel: "warn", 29 | ErrorLevel: "error", 30 | FatalLevel: "fatal", 31 | } 32 | 33 | var levelStrings = map[string]Level{ 34 | "debug": DebugLevel, 35 | "info": InfoLevel, 36 | "warn": WarnLevel, 37 | "warning": WarnLevel, 38 | "error": ErrorLevel, 39 | "fatal": FatalLevel, 40 | } 41 | 42 | // String implementation. 43 | func (l Level) String() string { 44 | return levelNames[l] 45 | } 46 | 47 | // MarshalJSON implementation. 48 | func (l Level) MarshalJSON() ([]byte, error) { 49 | return []byte(`"` + l.String() + `"`), nil 50 | } 51 | 52 | // UnmarshalJSON implementation. 53 | func (l *Level) UnmarshalJSON(b []byte) error { 54 | v, err := ParseLevel(string(bytes.Trim(b, `"`))) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | *l = v 60 | return nil 61 | } 62 | 63 | // ParseLevel parses level string. 64 | func ParseLevel(s string) (Level, error) { 65 | l, ok := levelStrings[strings.ToLower(s)] 66 | if !ok { 67 | return InvalidLevel, ErrInvalidLevel 68 | } 69 | 70 | return l, nil 71 | } 72 | 73 | // MustParseLevel parses level string or panics. 74 | func MustParseLevel(s string) Level { 75 | l, err := ParseLevel(s) 76 | if err != nil { 77 | panic("invalid log level") 78 | } 79 | 80 | return l 81 | } 82 | -------------------------------------------------------------------------------- /levels_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseLevel(t *testing.T) { 11 | cases := []struct { 12 | String string 13 | Level Level 14 | Num int 15 | }{ 16 | {"debug", DebugLevel, 0}, 17 | {"info", InfoLevel, 1}, 18 | {"warn", WarnLevel, 2}, 19 | {"warning", WarnLevel, 3}, 20 | {"error", ErrorLevel, 4}, 21 | {"fatal", FatalLevel, 5}, 22 | } 23 | 24 | for _, c := range cases { 25 | t.Run(c.String, func(t *testing.T) { 26 | l, err := ParseLevel(c.String) 27 | assert.NoError(t, err, "parse") 28 | assert.Equal(t, c.Level, l) 29 | }) 30 | } 31 | 32 | t.Run("invalid", func(t *testing.T) { 33 | l, err := ParseLevel("something") 34 | assert.Equal(t, ErrInvalidLevel, err) 35 | assert.Equal(t, InvalidLevel, l) 36 | }) 37 | } 38 | 39 | func TestLevel_MarshalJSON(t *testing.T) { 40 | e := Entry{ 41 | Level: InfoLevel, 42 | Message: "hello", 43 | Fields: Fields{}, 44 | } 45 | 46 | expect := `{"fields":{},"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` 47 | 48 | b, err := json.Marshal(e) 49 | assert.NoError(t, err) 50 | assert.Equal(t, expect, string(b)) 51 | } 52 | 53 | func TestLevel_UnmarshalJSON(t *testing.T) { 54 | s := `{"fields":{},"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` 55 | e := new(Entry) 56 | 57 | err := json.Unmarshal([]byte(s), e) 58 | assert.NoError(t, err) 59 | assert.Equal(t, InfoLevel, e.Level) 60 | } 61 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | stdlog "log" 5 | "sort" 6 | "time" 7 | ) 8 | 9 | // assert interface compliance. 10 | var _ Interface = (*Logger)(nil) 11 | 12 | // Fielder is an interface for providing fields to custom types. 13 | type Fielder interface { 14 | Fields() Fields 15 | } 16 | 17 | // Fields represents a map of entry level data used for structured logging. 18 | type Fields map[string]interface{} 19 | 20 | // Fields implements Fielder. 21 | func (f Fields) Fields() Fields { 22 | return f 23 | } 24 | 25 | // Get field value by name. 26 | func (f Fields) Get(name string) interface{} { 27 | return f[name] 28 | } 29 | 30 | // Names returns field names sorted. 31 | func (f Fields) Names() (v []string) { 32 | for k := range f { 33 | v = append(v, k) 34 | } 35 | 36 | sort.Strings(v) 37 | return 38 | } 39 | 40 | // The HandlerFunc type is an adapter to allow the use of ordinary functions as 41 | // log handlers. If f is a function with the appropriate signature, 42 | // HandlerFunc(f) is a Handler object that calls f. 43 | type HandlerFunc func(*Entry) error 44 | 45 | // HandleLog calls f(e). 46 | func (f HandlerFunc) HandleLog(e *Entry) error { 47 | return f(e) 48 | } 49 | 50 | // Handler is used to handle log events, outputting them to 51 | // stdio or sending them to remote services. See the "handlers" 52 | // directory for implementations. 53 | // 54 | // It is left up to Handlers to implement thread-safety. 55 | type Handler interface { 56 | HandleLog(*Entry) error 57 | } 58 | 59 | // Logger represents a logger with configurable Level and Handler. 60 | type Logger struct { 61 | Handler Handler 62 | Level Level 63 | } 64 | 65 | // WithFields returns a new entry with `fields` set. 66 | func (l *Logger) WithFields(fields Fielder) *Entry { 67 | return NewEntry(l).WithFields(fields.Fields()) 68 | } 69 | 70 | // WithField returns a new entry with the `key` and `value` set. 71 | // 72 | // Note that the `key` should not have spaces in it - use camel 73 | // case or underscores 74 | func (l *Logger) WithField(key string, value interface{}) *Entry { 75 | return NewEntry(l).WithField(key, value) 76 | } 77 | 78 | // WithDuration returns a new entry with the "duration" field set 79 | // to the given duration in milliseconds. 80 | func (l *Logger) WithDuration(d time.Duration) *Entry { 81 | return NewEntry(l).WithDuration(d) 82 | } 83 | 84 | // WithError returns a new entry with the "error" set to `err`. 85 | func (l *Logger) WithError(err error) *Entry { 86 | return NewEntry(l).WithError(err) 87 | } 88 | 89 | // Debug level message. 90 | func (l *Logger) Debug(msg string) { 91 | NewEntry(l).Debug(msg) 92 | } 93 | 94 | // Info level message. 95 | func (l *Logger) Info(msg string) { 96 | NewEntry(l).Info(msg) 97 | } 98 | 99 | // Warn level message. 100 | func (l *Logger) Warn(msg string) { 101 | NewEntry(l).Warn(msg) 102 | } 103 | 104 | // Error level message. 105 | func (l *Logger) Error(msg string) { 106 | NewEntry(l).Error(msg) 107 | } 108 | 109 | // Fatal level message, followed by an exit. 110 | func (l *Logger) Fatal(msg string) { 111 | NewEntry(l).Fatal(msg) 112 | } 113 | 114 | // Debugf level formatted message. 115 | func (l *Logger) Debugf(msg string, v ...interface{}) { 116 | NewEntry(l).Debugf(msg, v...) 117 | } 118 | 119 | // Infof level formatted message. 120 | func (l *Logger) Infof(msg string, v ...interface{}) { 121 | NewEntry(l).Infof(msg, v...) 122 | } 123 | 124 | // Warnf level formatted message. 125 | func (l *Logger) Warnf(msg string, v ...interface{}) { 126 | NewEntry(l).Warnf(msg, v...) 127 | } 128 | 129 | // Errorf level formatted message. 130 | func (l *Logger) Errorf(msg string, v ...interface{}) { 131 | NewEntry(l).Errorf(msg, v...) 132 | } 133 | 134 | // Fatalf level formatted message, followed by an exit. 135 | func (l *Logger) Fatalf(msg string, v ...interface{}) { 136 | NewEntry(l).Fatalf(msg, v...) 137 | } 138 | 139 | // Trace returns a new entry with a Stop method to fire off 140 | // a corresponding completion log, useful with defer. 141 | func (l *Logger) Trace(msg string) *Entry { 142 | return NewEntry(l).Trace(msg) 143 | } 144 | 145 | // log the message, invoking the handler. We clone the entry here 146 | // to bypass the overhead in Entry methods when the level is not 147 | // met. 148 | func (l *Logger) log(level Level, e *Entry, msg string) { 149 | if level < l.Level { 150 | return 151 | } 152 | 153 | if err := l.Handler.HandleLog(e.finalize(level, msg)); err != nil { 154 | stdlog.Printf("error logging: %s", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/discard" 9 | "github.com/apex/log/handlers/memory" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestLogger_printf(t *testing.T) { 14 | h := memory.New() 15 | 16 | l := &log.Logger{ 17 | Handler: h, 18 | Level: log.InfoLevel, 19 | } 20 | 21 | l.Infof("logged in %s", "Tobi") 22 | 23 | e := h.Entries[0] 24 | assert.Equal(t, e.Message, "logged in Tobi") 25 | assert.Equal(t, e.Level, log.InfoLevel) 26 | } 27 | 28 | func TestLogger_levels(t *testing.T) { 29 | h := memory.New() 30 | 31 | l := &log.Logger{ 32 | Handler: h, 33 | Level: log.InfoLevel, 34 | } 35 | 36 | l.Debug("uploading") 37 | l.Info("upload complete") 38 | 39 | assert.Equal(t, 1, len(h.Entries)) 40 | 41 | e := h.Entries[0] 42 | assert.Equal(t, e.Message, "upload complete") 43 | assert.Equal(t, e.Level, log.InfoLevel) 44 | } 45 | 46 | func TestLogger_WithFields(t *testing.T) { 47 | h := memory.New() 48 | 49 | l := &log.Logger{ 50 | Handler: h, 51 | Level: log.InfoLevel, 52 | } 53 | 54 | ctx := l.WithFields(log.Fields{"file": "sloth.png"}) 55 | ctx.Debug("uploading") 56 | ctx.Info("upload complete") 57 | 58 | assert.Equal(t, 1, len(h.Entries)) 59 | 60 | e := h.Entries[0] 61 | assert.Equal(t, e.Message, "upload complete") 62 | assert.Equal(t, e.Level, log.InfoLevel) 63 | assert.Equal(t, log.Fields{"file": "sloth.png"}, e.Fields) 64 | } 65 | 66 | func TestLogger_WithField(t *testing.T) { 67 | h := memory.New() 68 | 69 | l := &log.Logger{ 70 | Handler: h, 71 | Level: log.InfoLevel, 72 | } 73 | 74 | ctx := l.WithField("file", "sloth.png").WithField("user", "Tobi") 75 | ctx.Debug("uploading") 76 | ctx.Info("upload complete") 77 | 78 | assert.Equal(t, 1, len(h.Entries)) 79 | 80 | e := h.Entries[0] 81 | assert.Equal(t, e.Message, "upload complete") 82 | assert.Equal(t, e.Level, log.InfoLevel) 83 | assert.Equal(t, log.Fields{"file": "sloth.png", "user": "Tobi"}, e.Fields) 84 | } 85 | 86 | func TestLogger_Trace_info(t *testing.T) { 87 | h := memory.New() 88 | 89 | l := &log.Logger{ 90 | Handler: h, 91 | Level: log.InfoLevel, 92 | } 93 | 94 | func() (err error) { 95 | defer l.WithField("file", "sloth.png").Trace("upload").Stop(&err) 96 | return nil 97 | }() 98 | 99 | assert.Equal(t, 2, len(h.Entries)) 100 | 101 | { 102 | e := h.Entries[0] 103 | assert.Equal(t, e.Message, "upload") 104 | assert.Equal(t, e.Level, log.InfoLevel) 105 | assert.Equal(t, log.Fields{"file": "sloth.png"}, e.Fields) 106 | } 107 | 108 | { 109 | e := h.Entries[1] 110 | assert.Equal(t, e.Message, "upload") 111 | assert.Equal(t, e.Level, log.InfoLevel) 112 | assert.Equal(t, "sloth.png", e.Fields["file"]) 113 | assert.IsType(t, int64(0), e.Fields["duration"]) 114 | } 115 | } 116 | 117 | func TestLogger_Trace_error(t *testing.T) { 118 | h := memory.New() 119 | 120 | l := &log.Logger{ 121 | Handler: h, 122 | Level: log.InfoLevel, 123 | } 124 | 125 | func() (err error) { 126 | defer l.WithField("file", "sloth.png").Trace("upload").Stop(&err) 127 | return fmt.Errorf("boom") 128 | }() 129 | 130 | assert.Equal(t, 2, len(h.Entries)) 131 | 132 | { 133 | e := h.Entries[0] 134 | assert.Equal(t, e.Message, "upload") 135 | assert.Equal(t, e.Level, log.InfoLevel) 136 | assert.Equal(t, "sloth.png", e.Fields["file"]) 137 | } 138 | 139 | { 140 | e := h.Entries[1] 141 | assert.Equal(t, e.Message, "upload") 142 | assert.Equal(t, e.Level, log.ErrorLevel) 143 | assert.Equal(t, "sloth.png", e.Fields["file"]) 144 | assert.Equal(t, "boom", e.Fields["error"]) 145 | assert.IsType(t, int64(0), e.Fields["duration"]) 146 | } 147 | } 148 | 149 | func TestLogger_Trace_nil(t *testing.T) { 150 | h := memory.New() 151 | 152 | l := &log.Logger{ 153 | Handler: h, 154 | Level: log.InfoLevel, 155 | } 156 | 157 | func() { 158 | defer l.WithField("file", "sloth.png").Trace("upload").Stop(nil) 159 | }() 160 | 161 | assert.Equal(t, 2, len(h.Entries)) 162 | 163 | { 164 | e := h.Entries[0] 165 | assert.Equal(t, e.Message, "upload") 166 | assert.Equal(t, e.Level, log.InfoLevel) 167 | assert.Equal(t, log.Fields{"file": "sloth.png"}, e.Fields) 168 | } 169 | 170 | { 171 | e := h.Entries[1] 172 | assert.Equal(t, e.Message, "upload") 173 | assert.Equal(t, e.Level, log.InfoLevel) 174 | assert.Equal(t, "sloth.png", e.Fields["file"]) 175 | assert.IsType(t, int64(0), e.Fields["duration"]) 176 | } 177 | } 178 | 179 | func TestLogger_HandlerFunc(t *testing.T) { 180 | h := memory.New() 181 | f := func(e *log.Entry) error { 182 | return h.HandleLog(e) 183 | } 184 | 185 | l := &log.Logger{ 186 | Handler: log.HandlerFunc(f), 187 | Level: log.InfoLevel, 188 | } 189 | 190 | l.Infof("logged in %s", "Tobi") 191 | 192 | e := h.Entries[0] 193 | assert.Equal(t, e.Message, "logged in Tobi") 194 | assert.Equal(t, e.Level, log.InfoLevel) 195 | } 196 | 197 | func BenchmarkLogger_small(b *testing.B) { 198 | l := &log.Logger{ 199 | Handler: discard.New(), 200 | Level: log.InfoLevel, 201 | } 202 | 203 | for i := 0; i < b.N; i++ { 204 | l.Info("login") 205 | } 206 | } 207 | 208 | func BenchmarkLogger_medium(b *testing.B) { 209 | l := &log.Logger{ 210 | Handler: discard.New(), 211 | Level: log.InfoLevel, 212 | } 213 | 214 | for i := 0; i < b.N; i++ { 215 | l.WithFields(log.Fields{ 216 | "file": "sloth.png", 217 | "type": "image/png", 218 | "size": 1 << 20, 219 | }).Info("upload") 220 | } 221 | } 222 | 223 | func BenchmarkLogger_large(b *testing.B) { 224 | l := &log.Logger{ 225 | Handler: discard.New(), 226 | Level: log.InfoLevel, 227 | } 228 | 229 | err := fmt.Errorf("boom") 230 | 231 | for i := 0; i < b.N; i++ { 232 | l.WithFields(log.Fields{ 233 | "file": "sloth.png", 234 | "type": "image/png", 235 | "size": 1 << 20, 236 | }). 237 | WithFields(log.Fields{ 238 | "some": "more", 239 | "data": "here", 240 | "whatever": "blah blah", 241 | "more": "stuff", 242 | "context": "such useful", 243 | "much": "fun", 244 | }). 245 | WithError(err).Error("upload failed") 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /pkg.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "time" 4 | 5 | // singletons ftw? 6 | var Log Interface = &Logger{ 7 | Handler: HandlerFunc(handleStdLog), 8 | Level: InfoLevel, 9 | } 10 | 11 | // SetHandler sets the handler. This is not thread-safe. 12 | // The default handler outputs to the stdlib log. 13 | func SetHandler(h Handler) { 14 | if logger, ok := Log.(*Logger); ok { 15 | logger.Handler = h 16 | } 17 | } 18 | 19 | // SetLevel sets the log level. This is not thread-safe. 20 | func SetLevel(l Level) { 21 | if logger, ok := Log.(*Logger); ok { 22 | logger.Level = l 23 | } 24 | } 25 | 26 | // SetLevelFromString sets the log level from a string, panicing when invalid. This is not thread-safe. 27 | func SetLevelFromString(s string) { 28 | if logger, ok := Log.(*Logger); ok { 29 | logger.Level = MustParseLevel(s) 30 | } 31 | } 32 | 33 | // WithFields returns a new entry with `fields` set. 34 | func WithFields(fields Fielder) *Entry { 35 | return Log.WithFields(fields) 36 | } 37 | 38 | // WithField returns a new entry with the `key` and `value` set. 39 | func WithField(key string, value interface{}) *Entry { 40 | return Log.WithField(key, value) 41 | } 42 | 43 | // WithDuration returns a new entry with the "duration" field set 44 | // to the given duration in milliseconds. 45 | func WithDuration(d time.Duration) *Entry { 46 | return Log.WithDuration(d) 47 | } 48 | 49 | // WithError returns a new entry with the "error" set to `err`. 50 | func WithError(err error) *Entry { 51 | return Log.WithError(err) 52 | } 53 | 54 | // Debug level message. 55 | func Debug(msg string) { 56 | Log.Debug(msg) 57 | } 58 | 59 | // Info level message. 60 | func Info(msg string) { 61 | Log.Info(msg) 62 | } 63 | 64 | // Warn level message. 65 | func Warn(msg string) { 66 | Log.Warn(msg) 67 | } 68 | 69 | // Error level message. 70 | func Error(msg string) { 71 | Log.Error(msg) 72 | } 73 | 74 | // Fatal level message, followed by an exit. 75 | func Fatal(msg string) { 76 | Log.Fatal(msg) 77 | } 78 | 79 | // Debugf level formatted message. 80 | func Debugf(msg string, v ...interface{}) { 81 | Log.Debugf(msg, v...) 82 | } 83 | 84 | // Infof level formatted message. 85 | func Infof(msg string, v ...interface{}) { 86 | Log.Infof(msg, v...) 87 | } 88 | 89 | // Warnf level formatted message. 90 | func Warnf(msg string, v ...interface{}) { 91 | Log.Warnf(msg, v...) 92 | } 93 | 94 | // Errorf level formatted message. 95 | func Errorf(msg string, v ...interface{}) { 96 | Log.Errorf(msg, v...) 97 | } 98 | 99 | // Fatalf level formatted message, followed by an exit. 100 | func Fatalf(msg string, v ...interface{}) { 101 | Log.Fatalf(msg, v...) 102 | } 103 | 104 | // Trace returns a new entry with a Stop method to fire off 105 | // a corresponding completion log, useful with defer. 106 | func Trace(msg string) *Entry { 107 | return Log.Trace(msg) 108 | } 109 | -------------------------------------------------------------------------------- /pkg_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/memory" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type Pet struct { 13 | Name string 14 | Age int 15 | } 16 | 17 | func (p *Pet) Fields() log.Fields { 18 | return log.Fields{ 19 | "name": p.Name, 20 | "age": p.Age, 21 | } 22 | } 23 | 24 | func TestInfo(t *testing.T) { 25 | h := memory.New() 26 | log.SetHandler(h) 27 | 28 | log.Infof("logged in %s", "Tobi") 29 | 30 | e := h.Entries[0] 31 | assert.Equal(t, e.Message, "logged in Tobi") 32 | assert.Equal(t, e.Level, log.InfoLevel) 33 | } 34 | 35 | func TestFielder(t *testing.T) { 36 | h := memory.New() 37 | log.SetHandler(h) 38 | 39 | pet := &Pet{"Tobi", 3} 40 | log.WithFields(pet).Info("add pet") 41 | 42 | e := h.Entries[0] 43 | assert.Equal(t, log.Fields{"name": "Tobi", "age": 3}, e.Fields) 44 | } 45 | 46 | // Unstructured logging is supported, but not recommended since it is hard to query. 47 | func Example_unstructured() { 48 | log.Infof("%s logged in", "Tobi") 49 | } 50 | 51 | // Structured logging is supported with fields, and is recommended over the formatted message variants. 52 | func Example_structured() { 53 | log.WithField("user", "Tobo").Info("logged in") 54 | } 55 | 56 | // Errors are passed to WithError(), populating the "error" field. 57 | func Example_errors() { 58 | err := errors.New("boom") 59 | log.WithError(err).Error("upload failed") 60 | } 61 | 62 | // Multiple fields can be set, via chaining, or WithFields(). 63 | func Example_multipleFields() { 64 | log.WithFields(log.Fields{ 65 | "user": "Tobi", 66 | "file": "sloth.png", 67 | "type": "image/png", 68 | }).Info("upload") 69 | } 70 | 71 | // Trace can be used to simplify logging of start and completion events, 72 | // for example an upload which may fail. 73 | func Example_trace() { 74 | fn := func() (err error) { 75 | defer log.Trace("upload").Stop(&err) 76 | return 77 | } 78 | 79 | fn() 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/pkg/errors" 4 | 5 | // stackTracer interface. 6 | type stackTracer interface { 7 | StackTrace() errors.StackTrace 8 | } 9 | --------------------------------------------------------------------------------