├── .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 | 
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 | [](https://semaphoreci.com/tj/log)
57 | [](https://godoc.org/github.com/apex/log)
58 | 
59 | 
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 |
--------------------------------------------------------------------------------