├── .gitignore
├── ci
├── go.mod
├── mage.go
├── go.sum
└── magefile.go
├── terminal_check_js.go
├── terminal_check_wasi.go
├── terminal_check_wasip1.go
├── terminal_check_appengine.go
├── terminal_check_no_terminal.go
├── travis
├── cross_build.sh
└── install.sh
├── appveyor.yml
├── terminal_check_solaris.go
├── go.mod
├── terminal_check_notappengine.go
├── terminal_check_bsd.go
├── .travis.yml
├── terminal_check_unix.go
├── terminal_check_windows.go
├── example_function_test.go
├── doc.go
├── hooks
├── syslog
│ ├── syslog_test.go
│ ├── syslog.go
│ └── README.md
├── writer
│ ├── writer.go
│ ├── writer_test.go
│ └── README.md
└── test
│ ├── test.go
│ └── test_test.go
├── example_custom_caller_test.go
├── example_default_field_value_test.go
├── buffer_pool.go
├── example_global_hook_test.go
├── LICENSE
├── hooks.go
├── example_hook_test.go
├── internal
└── testutils
│ └── testutils.go
├── .golangci.yml
├── level_test.go
├── .github
└── workflows
│ └── ci.yaml
├── go.sum
├── logger_bench_test.go
├── example_basic_test.go
├── logger_test.go
├── formatter.go
├── formatter_bench_test.go
├── alt_exit.go
├── writer_test.go
├── writer.go
├── alt_exit_test.go
├── json_formatter.go
├── hook_test.go
├── logrus.go
├── exported.go
├── CHANGELOG.md
├── entry_test.go
├── json_formatter_test.go
├── text_formatter.go
├── logger.go
├── entry.go
├── text_formatter_test.go
├── README.md
└── logrus_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | logrus
2 | vendor
3 |
4 | .idea/
5 |
--------------------------------------------------------------------------------
/ci/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sirupsen/logrus/ci
2 |
3 | go 1.17
4 |
5 | require github.com/magefile/mage v1.15.0
6 |
--------------------------------------------------------------------------------
/terminal_check_js.go:
--------------------------------------------------------------------------------
1 | // +build js
2 |
3 | package logrus
4 |
5 | func isTerminal(fd int) bool {
6 | return false
7 | }
8 |
--------------------------------------------------------------------------------
/terminal_check_wasi.go:
--------------------------------------------------------------------------------
1 | //go:build wasi
2 | // +build wasi
3 |
4 | package logrus
5 |
6 | func isTerminal(fd int) bool {
7 | return false
8 | }
9 |
--------------------------------------------------------------------------------
/terminal_check_wasip1.go:
--------------------------------------------------------------------------------
1 | //go:build wasip1
2 | // +build wasip1
3 |
4 | package logrus
5 |
6 | func isTerminal(fd int) bool {
7 | return false
8 | }
9 |
--------------------------------------------------------------------------------
/ci/mage.go:
--------------------------------------------------------------------------------
1 | // +build ignore
2 |
3 | package main
4 |
5 | import (
6 | "github.com/magefile/mage/mage"
7 | "os"
8 | )
9 |
10 | func main() { os.Exit(mage.Main()) }
11 |
--------------------------------------------------------------------------------
/ci/go.sum:
--------------------------------------------------------------------------------
1 | github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
2 | github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
3 |
--------------------------------------------------------------------------------
/terminal_check_appengine.go:
--------------------------------------------------------------------------------
1 | // +build appengine
2 |
3 | package logrus
4 |
5 | import (
6 | "io"
7 | )
8 |
9 | func checkIfTerminal(w io.Writer) bool {
10 | return true
11 | }
12 |
--------------------------------------------------------------------------------
/terminal_check_no_terminal.go:
--------------------------------------------------------------------------------
1 | // +build js nacl plan9
2 |
3 | package logrus
4 |
5 | import (
6 | "io"
7 | )
8 |
9 | func checkIfTerminal(w io.Writer) bool {
10 | return false
11 | }
12 |
--------------------------------------------------------------------------------
/travis/cross_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$GO111MODULE" == "on" ]]; then
4 | $(go env GOPATH)/bin/gox -build-lib
5 | fi
6 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # Minimal stub to satisfy AppVeyor CI
2 | version: 1.0.{build}
3 | platform: x64
4 | shallow_clone: true
5 |
6 | branches:
7 | only:
8 | - master
9 | - main
10 |
11 | build_script:
12 | - echo "No-op build to satisfy AppVeyor CI"
13 |
--------------------------------------------------------------------------------
/travis/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # Install golanci 1.32.2
6 | if [[ "$TRAVIS_GO_VERSION" =~ ^1\.15\. ]]; then
7 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.32.2
8 | fi
9 |
--------------------------------------------------------------------------------
/terminal_check_solaris.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "golang.org/x/sys/unix"
5 | )
6 |
7 | // IsTerminal returns true if the given file descriptor is a terminal.
8 | func isTerminal(fd int) bool {
9 | _, err := unix.IoctlGetTermio(fd, unix.TCGETA)
10 | return err == nil
11 | }
12 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sirupsen/logrus
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/stretchr/testify v1.10.0
7 | golang.org/x/sys v0.13.0
8 | )
9 |
10 | require (
11 | github.com/davecgh/go-spew v1.1.1 // indirect
12 | github.com/pmezard/go-difflib v1.0.0 // indirect
13 | gopkg.in/yaml.v3 v3.0.1 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/terminal_check_notappengine.go:
--------------------------------------------------------------------------------
1 | // +build !appengine,!js,!windows,!nacl,!plan9
2 |
3 | package logrus
4 |
5 | import (
6 | "io"
7 | "os"
8 | )
9 |
10 | func checkIfTerminal(w io.Writer) bool {
11 | switch v := w.(type) {
12 | case *os.File:
13 | return isTerminal(int(v.Fd()))
14 | default:
15 | return false
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/terminal_check_bsd.go:
--------------------------------------------------------------------------------
1 | // +build darwin dragonfly freebsd netbsd openbsd hurd
2 | // +build !js
3 |
4 | package logrus
5 |
6 | import "golang.org/x/sys/unix"
7 |
8 | const ioctlReadTermios = unix.TIOCGETA
9 |
10 | func isTerminal(fd int) bool {
11 | _, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
12 | return err == nil
13 | }
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go_import_path: github.com/sirupsen/logrus
3 | git:
4 | depth: 1
5 | env:
6 | - GO111MODULE=on
7 | go: 1.15.x
8 | os: linux
9 | install:
10 | - ./travis/install.sh
11 | script:
12 | - cd ci
13 | - go run mage.go -v -w ../ crossBuild
14 | - go run mage.go -v -w ../ lint
15 | - go run mage.go -v -w ../ test
16 |
--------------------------------------------------------------------------------
/terminal_check_unix.go:
--------------------------------------------------------------------------------
1 | //go:build (linux || aix || zos) && !js && !wasi
2 | // +build linux aix zos
3 | // +build !js
4 | // +build !wasi
5 |
6 | package logrus
7 |
8 | import "golang.org/x/sys/unix"
9 |
10 | const ioctlReadTermios = unix.TCGETS
11 |
12 | func isTerminal(fd int) bool {
13 | _, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
14 | return err == nil
15 | }
16 |
--------------------------------------------------------------------------------
/terminal_check_windows.go:
--------------------------------------------------------------------------------
1 | // +build !appengine,!js,windows
2 |
3 | package logrus
4 |
5 | import (
6 | "io"
7 | "os"
8 |
9 | "golang.org/x/sys/windows"
10 | )
11 |
12 | func checkIfTerminal(w io.Writer) bool {
13 | switch v := w.(type) {
14 | case *os.File:
15 | handle := windows.Handle(v.Fd())
16 | var mode uint32
17 | if err := windows.GetConsoleMode(handle, &mode); err != nil {
18 | return false
19 | }
20 | mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
21 | if err := windows.SetConsoleMode(handle, mode); err != nil {
22 | return false
23 | }
24 | return true
25 | }
26 | return false
27 | }
28 |
--------------------------------------------------------------------------------
/example_function_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "testing"
5 |
6 | log "github.com/sirupsen/logrus"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestLogger_LogFn(t *testing.T) {
11 | log.SetFormatter(&log.JSONFormatter{})
12 | log.SetLevel(log.WarnLevel)
13 |
14 | notCalled := 0
15 | log.InfoFn(func() []interface{} {
16 | notCalled++
17 | return []interface{}{
18 | "Hello",
19 | }
20 | })
21 | assert.Equal(t, 0, notCalled)
22 |
23 | called := 0
24 | log.ErrorFn(func() []interface{} {
25 | called++
26 | return []interface{}{
27 | "Oopsi",
28 | }
29 | })
30 | assert.Equal(t, 1, called)
31 | }
32 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
3 |
4 |
5 | The simplest way to use Logrus is simply the package-level exported logger:
6 |
7 | package main
8 |
9 | import (
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | func main() {
14 | log.WithFields(log.Fields{
15 | "animal": "walrus",
16 | "number": 1,
17 | "size": 10,
18 | }).Info("A walrus appears")
19 | }
20 |
21 | Output:
22 | time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
23 |
24 | For a full guide visit https://github.com/sirupsen/logrus
25 | */
26 | package logrus
27 |
--------------------------------------------------------------------------------
/hooks/syslog/syslog_test.go:
--------------------------------------------------------------------------------
1 | // +build !windows,!nacl,!plan9
2 |
3 | package syslog
4 |
5 | import (
6 | "log/syslog"
7 | "testing"
8 |
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | func TestLocalhostAddAndPrint(t *testing.T) {
13 | log := logrus.New()
14 | hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
15 |
16 | if err != nil {
17 | t.Errorf("Unable to connect to local syslog.")
18 | }
19 |
20 | log.Hooks.Add(hook)
21 |
22 | for _, level := range hook.Levels() {
23 | if len(log.Hooks[level]) != 1 {
24 | t.Errorf("SyslogHook was not added. The length of log.Hooks[%v]: %v", level, len(log.Hooks[level]))
25 | }
26 | }
27 |
28 | log.Info("Congratulations!")
29 | }
30 |
--------------------------------------------------------------------------------
/hooks/writer/writer.go:
--------------------------------------------------------------------------------
1 | package writer
2 |
3 | import (
4 | "io"
5 |
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | // Hook is a hook that writes logs of specified LogLevels to specified Writer
10 | type Hook struct {
11 | Writer io.Writer
12 | LogLevels []log.Level
13 | }
14 |
15 | // Fire will be called when some logging function is called with current hook
16 | // It will format log entry to string and write it to appropriate writer
17 | func (hook *Hook) Fire(entry *log.Entry) error {
18 | line, err := entry.Bytes()
19 | if err != nil {
20 | return err
21 | }
22 | _, err = hook.Writer.Write(line)
23 | return err
24 | }
25 |
26 | // Levels define on which log levels this hook would trigger
27 | func (hook *Hook) Levels() []log.Level {
28 | return hook.LogLevels
29 | }
30 |
--------------------------------------------------------------------------------
/example_custom_caller_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "os"
5 | "path"
6 | "runtime"
7 | "strings"
8 |
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | func ExampleJSONFormatter_CallerPrettyfier() {
13 | l := logrus.New()
14 | l.SetReportCaller(true)
15 | l.Out = os.Stdout
16 | l.Formatter = &logrus.JSONFormatter{
17 | DisableTimestamp: true,
18 | CallerPrettyfier: func(f *runtime.Frame) (string, string) {
19 | s := strings.Split(f.Function, ".")
20 | funcname := s[len(s)-1]
21 | _, filename := path.Split(f.File)
22 | return funcname, filename
23 | },
24 | }
25 | l.Info("example of custom format caller")
26 | // Output:
27 | // {"file":"example_custom_caller_test.go","func":"ExampleJSONFormatter_CallerPrettyfier","level":"info","msg":"example of custom format caller"}
28 | }
29 |
--------------------------------------------------------------------------------
/example_default_field_value_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | type DefaultFieldHook struct {
10 | GetValue func() string
11 | }
12 |
13 | func (h *DefaultFieldHook) Levels() []logrus.Level {
14 | return logrus.AllLevels
15 | }
16 |
17 | func (h *DefaultFieldHook) Fire(e *logrus.Entry) error {
18 | e.Data["aDefaultField"] = h.GetValue()
19 | return nil
20 | }
21 |
22 | func ExampleDefaultFieldHook() {
23 | l := logrus.New()
24 | l.Out = os.Stdout
25 | l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
26 |
27 | l.AddHook(&DefaultFieldHook{GetValue: func() string { return "with its default value" }})
28 | l.Info("first log")
29 | // Output:
30 | // level=info msg="first log" aDefaultField="with its default value"
31 | }
32 |
--------------------------------------------------------------------------------
/buffer_pool.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "sync"
6 | )
7 |
8 | var (
9 | bufferPool BufferPool
10 | )
11 |
12 | type BufferPool interface {
13 | Put(*bytes.Buffer)
14 | Get() *bytes.Buffer
15 | }
16 |
17 | type defaultPool struct {
18 | pool *sync.Pool
19 | }
20 |
21 | func (p *defaultPool) Put(buf *bytes.Buffer) {
22 | p.pool.Put(buf)
23 | }
24 |
25 | func (p *defaultPool) Get() *bytes.Buffer {
26 | return p.pool.Get().(*bytes.Buffer)
27 | }
28 |
29 | // SetBufferPool allows to replace the default logrus buffer pool
30 | // to better meets the specific needs of an application.
31 | func SetBufferPool(bp BufferPool) {
32 | bufferPool = bp
33 | }
34 |
35 | func init() {
36 | SetBufferPool(&defaultPool{
37 | pool: &sync.Pool{
38 | New: func() interface{} {
39 | return new(bytes.Buffer)
40 | },
41 | },
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/example_global_hook_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | var (
10 | mystring string
11 | )
12 |
13 | type GlobalHook struct {
14 | }
15 |
16 | func (h *GlobalHook) Levels() []logrus.Level {
17 | return logrus.AllLevels
18 | }
19 |
20 | func (h *GlobalHook) Fire(e *logrus.Entry) error {
21 | e.Data["mystring"] = mystring
22 | return nil
23 | }
24 |
25 | func ExampleGlobalHook() {
26 | l := logrus.New()
27 | l.Out = os.Stdout
28 | l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
29 | l.AddHook(&GlobalHook{})
30 | mystring = "first value"
31 | l.Info("first log")
32 | mystring = "another value"
33 | l.Info("second log")
34 | // Output:
35 | // level=info msg="first log" mystring="first value"
36 | // level=info msg="second log" mystring="another value"
37 | }
38 |
--------------------------------------------------------------------------------
/hooks/writer/writer_test.go:
--------------------------------------------------------------------------------
1 | package writer
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "testing"
7 |
8 | log "github.com/sirupsen/logrus"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestDifferentLevelsGoToDifferentWriters(t *testing.T) {
13 | var a, b bytes.Buffer
14 |
15 | log.SetFormatter(&log.TextFormatter{
16 | DisableTimestamp: true,
17 | DisableColors: true,
18 | })
19 | log.SetOutput(io.Discard) // Send all logs to nowhere by default
20 |
21 | log.AddHook(&Hook{
22 | Writer: &a,
23 | LogLevels: []log.Level{
24 | log.WarnLevel,
25 | },
26 | })
27 | log.AddHook(&Hook{ // Send info and debug logs to stdout
28 | Writer: &b,
29 | LogLevels: []log.Level{
30 | log.InfoLevel,
31 | },
32 | })
33 | log.Warn("send to a")
34 | log.Info("send to b")
35 |
36 | assert.Equal(t, "level=warning msg=\"send to a\"\n", a.String())
37 | assert.Equal(t, "level=info msg=\"send to b\"\n", b.String())
38 | }
39 |
--------------------------------------------------------------------------------
/hooks/writer/README.md:
--------------------------------------------------------------------------------
1 | # Writer Hooks for Logrus
2 |
3 | Send logs of given levels to any object with `io.Writer` interface.
4 |
5 | ## Usage
6 |
7 | If you want for example send high level logs to `Stderr` and
8 | logs of normal execution to `Stdout`, you could do it like this:
9 |
10 | ```go
11 | package main
12 |
13 | import (
14 | "io/ioutil"
15 | "os"
16 |
17 | log "github.com/sirupsen/logrus"
18 | "github.com/sirupsen/logrus/hooks/writer"
19 | )
20 |
21 | func main() {
22 | log.SetOutput(ioutil.Discard) // Send all logs to nowhere by default
23 |
24 | log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr
25 | Writer: os.Stderr,
26 | LogLevels: []log.Level{
27 | log.PanicLevel,
28 | log.FatalLevel,
29 | log.ErrorLevel,
30 | log.WarnLevel,
31 | },
32 | })
33 | log.AddHook(&writer.Hook{ // Send info and debug logs to stdout
34 | Writer: os.Stdout,
35 | LogLevels: []log.Level{
36 | log.InfoLevel,
37 | log.DebugLevel,
38 | },
39 | })
40 | log.Info("This will go to stdout")
41 | log.Warn("This will go to stderr")
42 | }
43 | ```
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Simon Eskildsen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/hooks.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | // Hook describes hooks to be fired when logging on the logging levels returned from
4 | // [Hook.Levels] on your implementation of the interface. Note that this is not
5 | // fired in a goroutine or a channel with workers, you should handle such
6 | // functionality yourself if your call is non-blocking, and you don't wish for
7 | // the logging calls for levels returned from `Levels()` to block.
8 | type Hook interface {
9 | Levels() []Level
10 | Fire(*Entry) error
11 | }
12 |
13 | // LevelHooks is an internal type for storing the hooks on a logger instance.
14 | type LevelHooks map[Level][]Hook
15 |
16 | // Add a hook to an instance of logger. This is called with
17 | // `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
18 | func (hooks LevelHooks) Add(hook Hook) {
19 | for _, level := range hook.Levels() {
20 | hooks[level] = append(hooks[level], hook)
21 | }
22 | }
23 |
24 | // Fire all the hooks for the passed level. Used by `entry.log` to fire
25 | // appropriate hooks for a log entry.
26 | func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
27 | for _, hook := range hooks[level] {
28 | if err := hook.Fire(entry); err != nil {
29 | return err
30 | }
31 | }
32 |
33 | return nil
34 | }
35 |
--------------------------------------------------------------------------------
/example_hook_test.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package logrus_test
4 |
5 | import (
6 | "log/syslog"
7 | "os"
8 |
9 | "github.com/sirupsen/logrus"
10 | slhooks "github.com/sirupsen/logrus/hooks/syslog"
11 | )
12 |
13 | // An example on how to use a hook
14 | func Example_hook() {
15 | var log = logrus.New()
16 | log.Formatter = new(logrus.TextFormatter) // default
17 | log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
18 | log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
19 | if sl, err := slhooks.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, ""); err == nil {
20 | log.Hooks.Add(sl)
21 | }
22 | log.Out = os.Stdout
23 |
24 | log.WithFields(logrus.Fields{
25 | "animal": "walrus",
26 | "size": 10,
27 | }).Info("A group of walrus emerges from the ocean")
28 |
29 | log.WithFields(logrus.Fields{
30 | "omg": true,
31 | "number": 122,
32 | }).Warn("The group's number increased tremendously!")
33 |
34 | log.WithFields(logrus.Fields{
35 | "omg": true,
36 | "number": 100,
37 | }).Error("The ice breaks!")
38 |
39 | // Output:
40 | // level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
41 | // level=warning msg="The group's number increased tremendously!" number=122 omg=true
42 | // level=error msg="The ice breaks!" number=100 omg=true
43 | }
44 |
--------------------------------------------------------------------------------
/internal/testutils/testutils.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "strconv"
7 | "strings"
8 | "testing"
9 |
10 | . "github.com/sirupsen/logrus" //nolint:staticcheck
11 |
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) {
16 | var buffer bytes.Buffer
17 | var fields Fields
18 |
19 | logger := New()
20 | logger.Out = &buffer
21 | logger.Formatter = new(JSONFormatter)
22 |
23 | log(logger)
24 |
25 | err := json.Unmarshal(buffer.Bytes(), &fields)
26 | require.NoError(t, err)
27 |
28 | assertions(fields)
29 | }
30 |
31 | func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) {
32 | var buffer bytes.Buffer
33 |
34 | logger := New()
35 | logger.Out = &buffer
36 | logger.Formatter = &TextFormatter{
37 | DisableColors: true,
38 | }
39 |
40 | log(logger)
41 |
42 | fields := make(map[string]string)
43 | for _, kv := range strings.Split(strings.TrimRight(buffer.String(), "\n"), " ") {
44 | if !strings.Contains(kv, "=") {
45 | continue
46 | }
47 | kvArr := strings.Split(kv, "=")
48 | key := strings.TrimSpace(kvArr[0])
49 | val := kvArr[1]
50 | if kvArr[1][0] == '"' {
51 | var err error
52 | val, err = strconv.Unquote(val)
53 | require.NoError(t, err)
54 | }
55 | fields[key] = val
56 | }
57 | assertions(fields)
58 | }
59 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | run:
3 | tests: false
4 | linters:
5 | enable:
6 | - asasalint
7 | - asciicheck
8 | - bidichk
9 | - bodyclose
10 | - contextcheck
11 | - durationcheck
12 | - errchkjson
13 | - errorlint
14 | - exhaustive
15 | - gocheckcompilerdirectives
16 | - gochecksumtype
17 | - gosec
18 | - gosmopolitan
19 | - loggercheck
20 | - makezero
21 | - musttag
22 | - nilerr
23 | - nilnesserr
24 | - noctx
25 | - protogetter
26 | - reassign
27 | - recvcheck
28 | - rowserrcheck
29 | - spancheck
30 | - sqlclosecheck
31 | - testifylint
32 | - unparam
33 | - zerologlint
34 | disable:
35 | - prealloc
36 | settings:
37 | errcheck:
38 | check-type-assertions: false
39 | check-blank: false
40 | lll:
41 | line-length: 100
42 | tab-width: 4
43 | prealloc:
44 | simple: false
45 | range-loops: false
46 | for-loops: false
47 | whitespace:
48 | multi-if: false
49 | multi-func: false
50 | exclusions:
51 | generated: lax
52 | presets:
53 | - comments
54 | - common-false-positives
55 | - legacy
56 | - std-error-handling
57 | paths:
58 | - third_party$
59 | - builtin$
60 | - examples$
61 | formatters:
62 | exclusions:
63 | generated: lax
64 | paths:
65 | - third_party$
66 | - builtin$
67 | - examples$
68 |
--------------------------------------------------------------------------------
/level_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "testing"
7 |
8 | "github.com/sirupsen/logrus"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestLevelJsonEncoding(t *testing.T) {
13 | type X struct {
14 | Level logrus.Level
15 | }
16 |
17 | var x X
18 | x.Level = logrus.WarnLevel
19 | var buf bytes.Buffer
20 | enc := json.NewEncoder(&buf)
21 | require.NoError(t, enc.Encode(x))
22 | dec := json.NewDecoder(&buf)
23 | var y X
24 | require.NoError(t, dec.Decode(&y))
25 | }
26 |
27 | func TestLevelUnmarshalText(t *testing.T) {
28 | var u logrus.Level
29 | for _, level := range logrus.AllLevels {
30 | t.Run(level.String(), func(t *testing.T) {
31 | require.NoError(t, u.UnmarshalText([]byte(level.String())))
32 | require.Equal(t, level, u)
33 | })
34 | }
35 | t.Run("invalid", func(t *testing.T) {
36 | require.Error(t, u.UnmarshalText([]byte("invalid")))
37 | })
38 | }
39 |
40 | func TestLevelMarshalText(t *testing.T) {
41 | levelStrings := []string{
42 | "panic",
43 | "fatal",
44 | "error",
45 | "warning",
46 | "info",
47 | "debug",
48 | "trace",
49 | }
50 | for idx, val := range logrus.AllLevels {
51 | level := val
52 | t.Run(level.String(), func(t *testing.T) {
53 | var cmp logrus.Level
54 | b, err := level.MarshalText()
55 | require.NoError(t, err)
56 | require.Equal(t, levelStrings[idx], string(b))
57 | err = cmp.UnmarshalText(b)
58 | require.NoError(t, err)
59 | require.Equal(t, level, cmp)
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/hooks/syslog/syslog.go:
--------------------------------------------------------------------------------
1 | // +build !windows,!nacl,!plan9
2 |
3 | package syslog
4 |
5 | import (
6 | "fmt"
7 | "log/syslog"
8 | "os"
9 |
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | // SyslogHook to send logs via syslog.
14 | type SyslogHook struct {
15 | Writer *syslog.Writer
16 | SyslogNetwork string
17 | SyslogRaddr string
18 | }
19 |
20 | // Creates a hook to be added to an instance of logger. This is called with
21 | // `hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_DEBUG, "")`
22 | // `if err == nil { log.Hooks.Add(hook) }`
23 | func NewSyslogHook(network, raddr string, priority syslog.Priority, tag string) (*SyslogHook, error) {
24 | w, err := syslog.Dial(network, raddr, priority, tag)
25 | return &SyslogHook{w, network, raddr}, err
26 | }
27 |
28 | func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
29 | line, err := entry.String()
30 | if err != nil {
31 | fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
32 | return err
33 | }
34 |
35 | switch entry.Level {
36 | case logrus.PanicLevel:
37 | return hook.Writer.Crit(line)
38 | case logrus.FatalLevel:
39 | return hook.Writer.Crit(line)
40 | case logrus.ErrorLevel:
41 | return hook.Writer.Err(line)
42 | case logrus.WarnLevel:
43 | return hook.Writer.Warning(line)
44 | case logrus.InfoLevel:
45 | return hook.Writer.Info(line)
46 | case logrus.DebugLevel, logrus.TraceLevel:
47 | return hook.Writer.Debug(line)
48 | default:
49 | return nil
50 | }
51 | }
52 |
53 | func (hook *SyslogHook) Levels() []logrus.Level {
54 | return logrus.AllLevels
55 | }
56 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | env:
12 | GOTOOLCHAIN: local
13 |
14 | jobs:
15 |
16 | lint:
17 | name: Golang-CI Lint
18 | timeout-minutes: 10
19 | strategy:
20 | matrix:
21 | platform: [ubuntu-latest]
22 | runs-on: ${{ matrix.platform }}
23 | steps:
24 | - name: Install Go
25 | uses: actions/setup-go@v5
26 | with:
27 | go-version: stable
28 | - uses: actions/checkout@v4
29 | - uses: golangci/golangci-lint-action@v8
30 | cross:
31 | name: Cross
32 | timeout-minutes: 10
33 | strategy:
34 | matrix:
35 | go-version: [stable]
36 | platform: [ubuntu-latest]
37 | runs-on: ${{ matrix.platform }}
38 | steps:
39 | - name: Install Go
40 | uses: actions/setup-go@v5
41 | with:
42 | go-version: ${{ matrix.go-version }}
43 | - name: Checkout code
44 | uses: actions/checkout@v4
45 | - name: Cross
46 | working-directory: ci
47 | run: go run mage.go -v -w ../ crossBuild
48 |
49 | test:
50 | name: Unit test
51 | timeout-minutes: 10
52 | strategy:
53 | matrix:
54 | go-version: [stable, oldstable, 1.17.x]
55 | platform: [ubuntu-latest, windows-latest, macos-latest]
56 | runs-on: ${{ matrix.platform }}
57 | steps:
58 | - name: Install Go
59 | uses: actions/setup-go@v5
60 | with:
61 | go-version: ${{ matrix.go-version }}
62 | - name: Checkout code
63 | uses: actions/checkout@v4
64 | - name: Test
65 | run: go test -race -v ./...
66 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
9 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
13 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
14 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
15 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
16 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
22 |
--------------------------------------------------------------------------------
/logger_bench_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "io"
5 | "os"
6 | "testing"
7 | )
8 |
9 | func BenchmarkDummyLogger(b *testing.B) {
10 | nullf, err := os.OpenFile("/dev/null", os.O_WRONLY, 0666)
11 | if err != nil {
12 | b.Fatalf("%v", err)
13 | }
14 | defer nullf.Close()
15 | doLoggerBenchmark(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
16 | }
17 |
18 | func BenchmarkDummyLoggerNoLock(b *testing.B) {
19 | nullf, err := os.OpenFile("/dev/null", os.O_WRONLY|os.O_APPEND, 0666)
20 | if err != nil {
21 | b.Fatalf("%v", err)
22 | }
23 | defer nullf.Close()
24 | doLoggerBenchmarkNoLock(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
25 | }
26 |
27 | func doLoggerBenchmark(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
28 | logger := Logger{
29 | Out: out,
30 | Level: InfoLevel,
31 | Formatter: formatter,
32 | }
33 | entry := logger.WithFields(fields)
34 | b.RunParallel(func(pb *testing.PB) {
35 | for pb.Next() {
36 | entry.Info("aaa")
37 | }
38 | })
39 | }
40 |
41 | func doLoggerBenchmarkNoLock(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
42 | logger := Logger{
43 | Out: out,
44 | Level: InfoLevel,
45 | Formatter: formatter,
46 | }
47 | logger.SetNoLock()
48 | entry := logger.WithFields(fields)
49 | b.RunParallel(func(pb *testing.PB) {
50 | for pb.Next() {
51 | entry.Info("aaa")
52 | }
53 | })
54 | }
55 |
56 | func BenchmarkLoggerJSONFormatter(b *testing.B) {
57 | doLoggerBenchmarkWithFormatter(b, &JSONFormatter{})
58 | }
59 |
60 | func BenchmarkLoggerTextFormatter(b *testing.B) {
61 | doLoggerBenchmarkWithFormatter(b, &TextFormatter{})
62 | }
63 |
64 | func doLoggerBenchmarkWithFormatter(b *testing.B, f Formatter) {
65 | b.SetParallelism(100)
66 | log := New()
67 | log.Formatter = f
68 | log.Out = io.Discard
69 | b.RunParallel(func(pb *testing.PB) {
70 | for pb.Next() {
71 | log.
72 | WithField("foo1", "bar1").
73 | WithField("foo2", "bar2").
74 | Info("this is a dummy log")
75 | }
76 | })
77 | }
78 |
--------------------------------------------------------------------------------
/hooks/syslog/README.md:
--------------------------------------------------------------------------------
1 | # Syslog Hooks for Logrus
2 |
3 | ## Usage
4 |
5 | ```go
6 | import (
7 | "log/syslog"
8 | "github.com/sirupsen/logrus"
9 | lSyslog "github.com/sirupsen/logrus/hooks/syslog"
10 | )
11 |
12 | func main() {
13 | log := logrus.New()
14 | hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
15 |
16 | if err == nil {
17 | log.Hooks.Add(hook)
18 | }
19 | }
20 | ```
21 |
22 | If you want to connect to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). Just assign empty string to the first two parameters of `NewSyslogHook`. It should look like the following.
23 |
24 | ```go
25 | import (
26 | "log/syslog"
27 | "github.com/sirupsen/logrus"
28 | lSyslog "github.com/sirupsen/logrus/hooks/syslog"
29 | )
30 |
31 | func main() {
32 | log := logrus.New()
33 | hook, err := lSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "")
34 |
35 | if err == nil {
36 | log.Hooks.Add(hook)
37 | }
38 | }
39 | ```
40 |
41 | ### Different log levels for local and remote logging
42 |
43 | By default `NewSyslogHook()` sends logs through the hook for all log levels. If you want to have
44 | different log levels between local logging and syslog logging (i.e. respect the `priority` argument
45 | passed to `NewSyslogHook()`), you need to implement the `logrus_syslog.SyslogHook` interface
46 | overriding `Levels()` to return only the log levels you're interested on.
47 |
48 | The following example shows how to log at **DEBUG** level for local logging and **WARN** level for
49 | syslog logging:
50 |
51 | ```go
52 | package main
53 |
54 | import (
55 | "log/syslog"
56 |
57 | log "github.com/sirupsen/logrus"
58 | logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
59 | )
60 |
61 | type customHook struct {
62 | *logrus_syslog.SyslogHook
63 | }
64 |
65 | func (h *customHook) Levels() []log.Level {
66 | return []log.Level{log.WarnLevel}
67 | }
68 |
69 | func main() {
70 | log.SetLevel(log.DebugLevel)
71 |
72 | hook, err := logrus_syslog.NewSyslogHook("tcp", "localhost:5140", syslog.LOG_WARNING, "myTag")
73 | if err != nil {
74 | panic(err)
75 | }
76 |
77 | log.AddHook(&customHook{hook})
78 |
79 | //...
80 | }
81 | ```
82 |
--------------------------------------------------------------------------------
/hooks/test/test.go:
--------------------------------------------------------------------------------
1 | // The Test package is used for testing logrus.
2 | // It provides a simple hooks which register logged messages.
3 | package test
4 |
5 | import (
6 | "io"
7 | "sync"
8 |
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | // Hook is a hook designed for dealing with logs in test scenarios.
13 | type Hook struct {
14 | // Entries is an array of all entries that have been received by this hook.
15 | // For safe access, use the AllEntries() method, rather than reading this
16 | // value directly.
17 | Entries []logrus.Entry
18 | mu sync.RWMutex
19 | }
20 |
21 | // NewGlobal installs a test hook for the global logger.
22 | func NewGlobal() *Hook {
23 |
24 | hook := new(Hook)
25 | logrus.AddHook(hook)
26 |
27 | return hook
28 |
29 | }
30 |
31 | // NewLocal installs a test hook for a given local logger.
32 | func NewLocal(logger *logrus.Logger) *Hook {
33 |
34 | hook := new(Hook)
35 | logger.AddHook(hook)
36 |
37 | return hook
38 |
39 | }
40 |
41 | // NewNullLogger creates a discarding logger and installs the test hook.
42 | func NewNullLogger() (*logrus.Logger, *Hook) {
43 |
44 | logger := logrus.New()
45 | logger.Out = io.Discard
46 |
47 | return logger, NewLocal(logger)
48 |
49 | }
50 |
51 | func (t *Hook) Fire(e *logrus.Entry) error {
52 | t.mu.Lock()
53 | defer t.mu.Unlock()
54 | t.Entries = append(t.Entries, *e)
55 | return nil
56 | }
57 |
58 | func (t *Hook) Levels() []logrus.Level {
59 | return logrus.AllLevels
60 | }
61 |
62 | // LastEntry returns the last entry that was logged or nil.
63 | func (t *Hook) LastEntry() *logrus.Entry {
64 | t.mu.RLock()
65 | defer t.mu.RUnlock()
66 | i := len(t.Entries) - 1
67 | if i < 0 {
68 | return nil
69 | }
70 | return &t.Entries[i]
71 | }
72 |
73 | // AllEntries returns all entries that were logged.
74 | func (t *Hook) AllEntries() []*logrus.Entry {
75 | t.mu.RLock()
76 | defer t.mu.RUnlock()
77 | // Make a copy so the returned value won't race with future log requests
78 | entries := make([]*logrus.Entry, len(t.Entries))
79 | for i := 0; i < len(t.Entries); i++ {
80 | // Make a copy, for safety
81 | entries[i] = &t.Entries[i]
82 | }
83 | return entries
84 | }
85 |
86 | // Reset removes all Entries from this test hook.
87 | func (t *Hook) Reset() {
88 | t.mu.Lock()
89 | defer t.mu.Unlock()
90 | t.Entries = make([]logrus.Entry, 0)
91 | }
92 |
--------------------------------------------------------------------------------
/example_basic_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | func Example_basic() {
10 | var log = logrus.New()
11 | log.Formatter = new(logrus.JSONFormatter)
12 | log.Formatter = new(logrus.TextFormatter) //default
13 | log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
14 | log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
15 | log.Level = logrus.TraceLevel
16 | log.Out = os.Stdout
17 |
18 | // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
19 | // if err == nil {
20 | // log.Out = file
21 | // } else {
22 | // log.Info("Failed to log to file, using default stderr")
23 | // }
24 |
25 | defer func() {
26 | err := recover()
27 | if err != nil {
28 | entry := err.(*logrus.Entry)
29 | log.WithFields(logrus.Fields{
30 | "omg": true,
31 | "err_animal": entry.Data["animal"],
32 | "err_size": entry.Data["size"],
33 | "err_level": entry.Level,
34 | "err_message": entry.Message,
35 | "number": 100,
36 | }).Error("The ice breaks!") // or use Fatal() to force the process to exit with a nonzero code
37 | }
38 | }()
39 |
40 | log.WithFields(logrus.Fields{
41 | "animal": "walrus",
42 | "number": 0,
43 | }).Trace("Went to the beach")
44 |
45 | log.WithFields(logrus.Fields{
46 | "animal": "walrus",
47 | "number": 8,
48 | }).Debug("Started observing beach")
49 |
50 | log.WithFields(logrus.Fields{
51 | "animal": "walrus",
52 | "size": 10,
53 | }).Info("A group of walrus emerges from the ocean")
54 |
55 | log.WithFields(logrus.Fields{
56 | "omg": true,
57 | "number": 122,
58 | }).Warn("The group's number increased tremendously!")
59 |
60 | log.WithFields(logrus.Fields{
61 | "temperature": -4,
62 | }).Debug("Temperature changes")
63 |
64 | log.WithFields(logrus.Fields{
65 | "animal": "orca",
66 | "size": 9009,
67 | }).Panic("It's over 9000!")
68 |
69 | // Output:
70 | // level=trace msg="Went to the beach" animal=walrus number=0
71 | // level=debug msg="Started observing beach" animal=walrus number=8
72 | // level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
73 | // level=warning msg="The group's number increased tremendously!" number=122 omg=true
74 | // level=debug msg="Temperature changes" temperature=-4
75 | // level=panic msg="It's over 9000!" animal=orca size=9009
76 | // level=error msg="The ice breaks!" err_animal=orca err_level=panic err_message="It's over 9000!" err_size=9009 number=100 omg=true
77 | }
78 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestFieldValueError(t *testing.T) {
14 | buf := &bytes.Buffer{}
15 | l := &Logger{
16 | Out: buf,
17 | Formatter: new(JSONFormatter),
18 | Hooks: make(LevelHooks),
19 | Level: DebugLevel,
20 | }
21 | l.WithField("func", func() {}).Info("test")
22 | fmt.Println(buf.String())
23 | var data map[string]interface{}
24 | if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
25 | t.Error("unexpected error", err)
26 | }
27 | _, ok := data[FieldKeyLogrusError]
28 | require.True(t, ok, `cannot found expected "logrus_error" field: %v`, data)
29 | }
30 |
31 | func TestNoFieldValueError(t *testing.T) {
32 | buf := &bytes.Buffer{}
33 | l := &Logger{
34 | Out: buf,
35 | Formatter: new(JSONFormatter),
36 | Hooks: make(LevelHooks),
37 | Level: DebugLevel,
38 | }
39 | l.WithField("str", "str").Info("test")
40 | fmt.Println(buf.String())
41 | var data map[string]interface{}
42 | if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
43 | t.Error("unexpected error", err)
44 | }
45 | _, ok := data[FieldKeyLogrusError]
46 | require.False(t, ok)
47 | }
48 |
49 | func TestWarninglnNotEqualToWarning(t *testing.T) {
50 | buf := &bytes.Buffer{}
51 | bufln := &bytes.Buffer{}
52 |
53 | formatter := new(TextFormatter)
54 | formatter.DisableTimestamp = true
55 | formatter.DisableLevelTruncation = true
56 |
57 | l := &Logger{
58 | Out: buf,
59 | Formatter: formatter,
60 | Hooks: make(LevelHooks),
61 | Level: DebugLevel,
62 | }
63 | l.Warning("hello,", "world")
64 |
65 | l.SetOutput(bufln)
66 | l.Warningln("hello,", "world")
67 |
68 | assert.NotEqual(t, buf.String(), bufln.String(), "Warning() and Wantingln() should not be equal")
69 | }
70 |
71 | type testBufferPool struct {
72 | buffers []*bytes.Buffer
73 | get int
74 | }
75 |
76 | func (p *testBufferPool) Get() *bytes.Buffer {
77 | p.get++
78 | return new(bytes.Buffer)
79 | }
80 |
81 | func (p *testBufferPool) Put(buf *bytes.Buffer) {
82 | p.buffers = append(p.buffers, buf)
83 | }
84 |
85 | func TestLogger_SetBufferPool(t *testing.T) {
86 | out := &bytes.Buffer{}
87 | l := New()
88 | l.SetOutput(out)
89 |
90 | pool := new(testBufferPool)
91 | l.SetBufferPool(pool)
92 |
93 | l.Info("test")
94 |
95 | assert.Equal(t, 1, pool.get, "Logger.SetBufferPool(): The BufferPool.Get() must be called")
96 | assert.Len(t, pool.buffers, 1, "Logger.SetBufferPool(): The BufferPool.Put() must be called")
97 | }
98 |
--------------------------------------------------------------------------------
/formatter.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import "time"
4 |
5 | // Default key names for the default fields
6 | const (
7 | defaultTimestampFormat = time.RFC3339
8 | FieldKeyMsg = "msg"
9 | FieldKeyLevel = "level"
10 | FieldKeyTime = "time"
11 | FieldKeyLogrusError = "logrus_error"
12 | FieldKeyFunc = "func"
13 | FieldKeyFile = "file"
14 | )
15 |
16 | // The Formatter interface is used to implement a custom Formatter. It takes an
17 | // `Entry`. It exposes all the fields, including the default ones:
18 | //
19 | // * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
20 | // * `entry.Data["time"]`. The timestamp.
21 | // * `entry.Data["level"]. The level the entry was logged at.
22 | //
23 | // Any additional fields added with `WithField` or `WithFields` are also in
24 | // `entry.Data`. Format is expected to return an array of bytes which are then
25 | // logged to `logger.Out`.
26 | type Formatter interface {
27 | Format(*Entry) ([]byte, error)
28 | }
29 |
30 | // This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
31 | // dumping it. If this code wasn't there doing:
32 | //
33 | // logrus.WithField("level", 1).Info("hello")
34 | //
35 | // Would just silently drop the user provided level. Instead with this code
36 | // it'll logged as:
37 | //
38 | // {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
39 | //
40 | // It's not exported because it's still using Data in an opinionated way. It's to
41 | // avoid code duplication between the two default formatters.
42 | func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
43 | timeKey := fieldMap.resolve(FieldKeyTime)
44 | if t, ok := data[timeKey]; ok {
45 | data["fields."+timeKey] = t
46 | delete(data, timeKey)
47 | }
48 |
49 | msgKey := fieldMap.resolve(FieldKeyMsg)
50 | if m, ok := data[msgKey]; ok {
51 | data["fields."+msgKey] = m
52 | delete(data, msgKey)
53 | }
54 |
55 | levelKey := fieldMap.resolve(FieldKeyLevel)
56 | if l, ok := data[levelKey]; ok {
57 | data["fields."+levelKey] = l
58 | delete(data, levelKey)
59 | }
60 |
61 | logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
62 | if l, ok := data[logrusErrKey]; ok {
63 | data["fields."+logrusErrKey] = l
64 | delete(data, logrusErrKey)
65 | }
66 |
67 | // If reportCaller is not set, 'func' will not conflict.
68 | if reportCaller {
69 | funcKey := fieldMap.resolve(FieldKeyFunc)
70 | if l, ok := data[funcKey]; ok {
71 | data["fields."+funcKey] = l
72 | }
73 | fileKey := fieldMap.resolve(FieldKeyFile)
74 | if l, ok := data[fileKey]; ok {
75 | data["fields."+fileKey] = l
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/formatter_bench_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | // smallFields is a small size data set for benchmarking
10 | var smallFields = Fields{
11 | "foo": "bar",
12 | "baz": "qux",
13 | "one": "two",
14 | "three": "four",
15 | }
16 |
17 | // largeFields is a large size data set for benchmarking
18 | var largeFields = Fields{
19 | "foo": "bar",
20 | "baz": "qux",
21 | "one": "two",
22 | "three": "four",
23 | "five": "six",
24 | "seven": "eight",
25 | "nine": "ten",
26 | "eleven": "twelve",
27 | "thirteen": "fourteen",
28 | "fifteen": "sixteen",
29 | "seventeen": "eighteen",
30 | "nineteen": "twenty",
31 | "a": "b",
32 | "c": "d",
33 | "e": "f",
34 | "g": "h",
35 | "i": "j",
36 | "k": "l",
37 | "m": "n",
38 | "o": "p",
39 | "q": "r",
40 | "s": "t",
41 | "u": "v",
42 | "w": "x",
43 | "y": "z",
44 | "this": "will",
45 | "make": "thirty",
46 | "entries": "yeah",
47 | }
48 |
49 | var errorFields = Fields{
50 | "foo": fmt.Errorf("bar"),
51 | "baz": fmt.Errorf("qux"),
52 | }
53 |
54 | func BenchmarkErrorTextFormatter(b *testing.B) {
55 | doBenchmark(b, &TextFormatter{DisableColors: true}, errorFields)
56 | }
57 |
58 | func BenchmarkSmallTextFormatter(b *testing.B) {
59 | doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields)
60 | }
61 |
62 | func BenchmarkLargeTextFormatter(b *testing.B) {
63 | doBenchmark(b, &TextFormatter{DisableColors: true}, largeFields)
64 | }
65 |
66 | func BenchmarkSmallColoredTextFormatter(b *testing.B) {
67 | doBenchmark(b, &TextFormatter{ForceColors: true}, smallFields)
68 | }
69 |
70 | func BenchmarkLargeColoredTextFormatter(b *testing.B) {
71 | doBenchmark(b, &TextFormatter{ForceColors: true}, largeFields)
72 | }
73 |
74 | func BenchmarkSmallJSONFormatter(b *testing.B) {
75 | doBenchmark(b, &JSONFormatter{}, smallFields)
76 | }
77 |
78 | func BenchmarkLargeJSONFormatter(b *testing.B) {
79 | doBenchmark(b, &JSONFormatter{}, largeFields)
80 | }
81 |
82 | func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
83 | logger := New()
84 |
85 | entry := &Entry{
86 | Time: time.Time{},
87 | Level: InfoLevel,
88 | Message: "message",
89 | Data: fields,
90 | Logger: logger,
91 | }
92 | var d []byte
93 | var err error
94 | for i := 0; i < b.N; i++ {
95 | d, err = formatter.Format(entry)
96 | if err != nil {
97 | b.Fatal(err)
98 | }
99 | b.SetBytes(int64(len(d)))
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/hooks/test/test_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "math/rand"
5 | "sync"
6 | "testing"
7 | "time"
8 |
9 | "github.com/sirupsen/logrus"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestAllHooks(t *testing.T) {
14 | assert := assert.New(t)
15 |
16 | logger, hook := NewNullLogger()
17 | assert.Nil(hook.LastEntry())
18 | assert.Equal(0, len(hook.Entries))
19 |
20 | logger.Error("Hello error")
21 | assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
22 | assert.Equal("Hello error", hook.LastEntry().Message)
23 | assert.Equal(1, len(hook.Entries))
24 |
25 | logger.Warn("Hello warning")
26 | assert.Equal(logrus.WarnLevel, hook.LastEntry().Level)
27 | assert.Equal("Hello warning", hook.LastEntry().Message)
28 | assert.Equal(2, len(hook.Entries))
29 |
30 | hook.Reset()
31 | assert.Nil(hook.LastEntry())
32 | assert.Equal(0, len(hook.Entries))
33 |
34 | hook = NewGlobal()
35 |
36 | logrus.Error("Hello error")
37 | assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
38 | assert.Equal("Hello error", hook.LastEntry().Message)
39 | assert.Equal(1, len(hook.Entries))
40 | }
41 |
42 | func TestLoggingWithHooksRace(t *testing.T) {
43 |
44 | r := rand.New(rand.NewSource(time.Now().UnixNano()))
45 | unlocker := r.Intn(100)
46 |
47 | assert := assert.New(t)
48 | logger, hook := NewNullLogger()
49 |
50 | var wgOne, wgAll sync.WaitGroup
51 | wgOne.Add(1)
52 | wgAll.Add(100)
53 |
54 | for i := 0; i < 100; i++ {
55 | go func(i int) {
56 | logger.Info("info")
57 | wgAll.Done()
58 | if i == unlocker {
59 | wgOne.Done()
60 | }
61 | }(i)
62 | }
63 |
64 | wgOne.Wait()
65 |
66 | assert.Equal(logrus.InfoLevel, hook.LastEntry().Level)
67 | assert.Equal("info", hook.LastEntry().Message)
68 |
69 | wgAll.Wait()
70 |
71 | entries := hook.AllEntries()
72 | assert.Equal(100, len(entries))
73 | }
74 |
75 | func TestFatalWithAlternateExit(t *testing.T) {
76 | assert := assert.New(t)
77 |
78 | logger, hook := NewNullLogger()
79 | logger.ExitFunc = func(code int) {}
80 |
81 | logger.Fatal("something went very wrong")
82 | assert.Equal(logrus.FatalLevel, hook.LastEntry().Level)
83 | assert.Equal("something went very wrong", hook.LastEntry().Message)
84 | assert.Equal(1, len(hook.Entries))
85 | }
86 |
87 | func TestNewLocal(t *testing.T) {
88 | assert := assert.New(t)
89 | logger := logrus.New()
90 |
91 | var wg sync.WaitGroup
92 | defer wg.Wait()
93 |
94 | wg.Add(10)
95 | for i := 0; i < 10; i++ {
96 | go func(i int) {
97 | logger.Info("info")
98 | wg.Done()
99 | }(i)
100 | }
101 |
102 | hook := NewLocal(logger)
103 | assert.NotNil(hook)
104 | }
105 |
--------------------------------------------------------------------------------
/alt_exit.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | // The following code was sourced and modified from the
4 | // https://github.com/tebeka/atexit package governed by the following license:
5 | //
6 | // Copyright (c) 2012 Miki Tebeka .
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | // this software and associated documentation files (the "Software"), to deal in
10 | // the Software without restriction, including without limitation the rights to
11 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | // the Software, and to permit persons to whom the Software is furnished to do so,
13 | // subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 |
25 | import (
26 | "fmt"
27 | "os"
28 | )
29 |
30 | var handlers = []func(){}
31 |
32 | func runHandler(handler func()) {
33 | defer func() {
34 | if err := recover(); err != nil {
35 | fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
36 | }
37 | }()
38 |
39 | handler()
40 | }
41 |
42 | func runHandlers() {
43 | for _, handler := range handlers {
44 | runHandler(handler)
45 | }
46 | }
47 |
48 | // Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
49 | func Exit(code int) {
50 | runHandlers()
51 | os.Exit(code)
52 | }
53 |
54 | // RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
55 | // call logrus.Exit to invoke all handlers. The handlers will also be invoked when
56 | // any Fatal log entry is made.
57 | //
58 | // This method is useful when a caller wishes to use logrus to log a fatal
59 | // message but also needs to gracefully shutdown. An example usecase could be
60 | // closing database connections, or sending a alert that the application is
61 | // closing.
62 | func RegisterExitHandler(handler func()) {
63 | handlers = append(handlers, handler)
64 | }
65 |
66 | // DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
67 | // call logrus.Exit to invoke all handlers. The handlers will also be invoked when
68 | // any Fatal log entry is made.
69 | //
70 | // This method is useful when a caller wishes to use logrus to log a fatal
71 | // message but also needs to gracefully shutdown. An example usecase could be
72 | // closing database connections, or sending a alert that the application is
73 | // closing.
74 | func DeferExitHandler(handler func()) {
75 | handlers = append([]func(){handler}, handlers...)
76 | }
77 |
--------------------------------------------------------------------------------
/ci/magefile.go:
--------------------------------------------------------------------------------
1 | //go:build mage
2 |
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "path"
10 | "sort"
11 |
12 | "github.com/magefile/mage/mg"
13 | "github.com/magefile/mage/sh"
14 | )
15 |
16 | func intersect(a, b []string) []string {
17 | sort.Strings(a)
18 | sort.Strings(b)
19 |
20 | res := make([]string, 0, func() int {
21 | if len(a) < len(b) {
22 | return len(a)
23 | }
24 | return len(b)
25 | }())
26 |
27 | for _, v := range a {
28 | idx := sort.SearchStrings(b, v)
29 | if idx < len(b) && b[idx] == v {
30 | res = append(res, v)
31 | }
32 | }
33 | return res
34 | }
35 |
36 | // getBuildMatrix returns the build matrix from the current version of the go compiler
37 | func getFullBuildMatrix() (map[string][]string, error) {
38 | jsonData, err := sh.Output("go", "tool", "dist", "list", "-json")
39 | if err != nil {
40 | return nil, err
41 | }
42 | var data []struct {
43 | Goos string
44 | Goarch string
45 | }
46 | if err := json.Unmarshal([]byte(jsonData), &data); err != nil {
47 | return nil, err
48 | }
49 |
50 | matrix := map[string][]string{}
51 | for _, v := range data {
52 | if val, ok := matrix[v.Goos]; ok {
53 | matrix[v.Goos] = append(val, v.Goarch)
54 | } else {
55 | matrix[v.Goos] = []string{v.Goarch}
56 | }
57 | }
58 |
59 | return matrix, nil
60 | }
61 |
62 | func getBuildMatrix() (map[string][]string, error) {
63 | minimalMatrix := map[string][]string{
64 | "linux": []string{"amd64"},
65 | "darwin": []string{"amd64", "arm64"},
66 | "freebsd": []string{"amd64"},
67 | "js": []string{"wasm"},
68 | "solaris": []string{"amd64"},
69 | "windows": []string{"amd64", "arm64"},
70 | }
71 |
72 | fullMatrix, err := getFullBuildMatrix()
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | for os, arches := range minimalMatrix {
78 | if fullV, ok := fullMatrix[os]; !ok {
79 | delete(minimalMatrix, os)
80 | } else {
81 | minimalMatrix[os] = intersect(arches, fullV)
82 | }
83 | }
84 | return minimalMatrix, nil
85 | }
86 |
87 | func CrossBuild() error {
88 | matrix, err := getBuildMatrix()
89 | if err != nil {
90 | return err
91 | }
92 |
93 | for os, arches := range matrix {
94 | for _, arch := range arches {
95 | env := map[string]string{
96 | "GOOS": os,
97 | "GOARCH": arch,
98 | }
99 | if mg.Verbose() {
100 | fmt.Printf("Building for GOOS=%s GOARCH=%s\n", os, arch)
101 | }
102 | if err := sh.RunWith(env, "go", "build", "./..."); err != nil {
103 | return err
104 | }
105 | }
106 | }
107 | return nil
108 | }
109 |
110 | func Lint() error {
111 | gopath := os.Getenv("GOPATH")
112 | if gopath == "" {
113 | return fmt.Errorf("cannot retrieve GOPATH")
114 | }
115 |
116 | return sh.Run(path.Join(gopath, "bin", "golangci-lint"), "run", "./...")
117 | }
118 |
119 | // Run the test suite
120 | func Test() error {
121 | return sh.RunWith(map[string]string{"GORACE": "halt_on_error=1"},
122 | "go", "test", "-race", "-v", "./...")
123 | }
124 |
--------------------------------------------------------------------------------
/writer_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "log"
7 | "net/http"
8 | "strings"
9 | "sync"
10 | "testing"
11 | "time"
12 |
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 |
16 | "github.com/sirupsen/logrus"
17 | )
18 |
19 | func ExampleLogger_Writer_httpServer() {
20 | logger := logrus.New()
21 | w := logger.Writer()
22 | defer w.Close()
23 |
24 | srv := http.Server{
25 | // create a stdlib log.Logger that writes to
26 | // logrus.Logger.
27 | ErrorLog: log.New(w, "", 0),
28 | }
29 |
30 | if err := srv.ListenAndServe(); err != nil {
31 | logger.Fatal(err)
32 | }
33 | }
34 |
35 | func ExampleLogger_Writer_stdlib() {
36 | logger := logrus.New()
37 | logger.Formatter = &logrus.JSONFormatter{}
38 |
39 | // Use logrus for standard log output
40 | // Note that `log` here references stdlib's log
41 | // Not logrus imported under the name `log`.
42 | log.SetOutput(logger.Writer())
43 | }
44 |
45 | type bufferWithMu struct {
46 | buf *bytes.Buffer
47 | mu sync.RWMutex
48 | }
49 |
50 | func (b *bufferWithMu) Write(p []byte) (int, error) {
51 | b.mu.Lock()
52 | defer b.mu.Unlock()
53 | return b.buf.Write(p)
54 | }
55 |
56 | func (b *bufferWithMu) Read(p []byte) (int, error) {
57 | b.mu.RLock()
58 | defer b.mu.RUnlock()
59 | return b.buf.Read(p)
60 | }
61 |
62 | func (b *bufferWithMu) String() string {
63 | b.mu.RLock()
64 | defer b.mu.RUnlock()
65 | return b.buf.String()
66 | }
67 |
68 | func TestWriterSplitNewlines(t *testing.T) {
69 | buf := &bufferWithMu{
70 | buf: bytes.NewBuffer(nil),
71 | }
72 | logger := logrus.New()
73 | logger.Formatter = &logrus.TextFormatter{
74 | DisableColors: true,
75 | DisableTimestamp: true,
76 | }
77 | logger.SetOutput(buf)
78 | writer := logger.Writer()
79 |
80 | const logNum = 10
81 |
82 | for i := 0; i < logNum; i++ {
83 | _, err := writer.Write([]byte("bar\nfoo\n"))
84 | require.NoError(t, err, "writer.Write failed")
85 | }
86 | writer.Close()
87 | // Test is flaky because it writes in another goroutine,
88 | // we need to make sure to wait a bit so all write are done.
89 | time.Sleep(500 * time.Millisecond)
90 |
91 | lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
92 | assert.Len(t, lines, logNum*2, "logger printed incorrect number of lines")
93 | }
94 |
95 | func TestWriterSplitsMax64KB(t *testing.T) {
96 | buf := &bufferWithMu{
97 | buf: bytes.NewBuffer(nil),
98 | }
99 | logger := logrus.New()
100 | logger.Formatter = &logrus.TextFormatter{
101 | DisableColors: true,
102 | DisableTimestamp: true,
103 | }
104 | logger.SetOutput(buf)
105 | writer := logger.Writer()
106 |
107 | // write more than 64KB
108 | const bigWriteLen = bufio.MaxScanTokenSize + 100
109 | output := make([]byte, bigWriteLen)
110 | // lets not write zero bytes
111 | for i := 0; i < bigWriteLen; i++ {
112 | output[i] = 'A'
113 | }
114 |
115 | for i := 0; i < 3; i++ {
116 | len, err := writer.Write(output)
117 | require.NoError(t, err, "writer.Write failed")
118 | assert.Equal(t, bigWriteLen, len, "bytes written")
119 | }
120 | writer.Close()
121 | // Test is flaky because it writes in another goroutine,
122 | // we need to make sure to wait a bit so all write are done.
123 | time.Sleep(500 * time.Millisecond)
124 |
125 | lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
126 | // we should have 4 lines because we wrote more than 64 KB each time
127 | assert.Len(t, lines, 4, "logger printed incorrect number of lines")
128 | }
129 |
--------------------------------------------------------------------------------
/writer.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bufio"
5 | "io"
6 | "runtime"
7 | "strings"
8 | )
9 |
10 | // Writer at INFO level. See WriterLevel for details.
11 | func (logger *Logger) Writer() *io.PipeWriter {
12 | return logger.WriterLevel(InfoLevel)
13 | }
14 |
15 | // WriterLevel returns an io.Writer that can be used to write arbitrary text to
16 | // the logger at the given log level. Each line written to the writer will be
17 | // printed in the usual way using formatters and hooks. The writer is part of an
18 | // io.Pipe and it is the callers responsibility to close the writer when done.
19 | // This can be used to override the standard library logger easily.
20 | func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
21 | return NewEntry(logger).WriterLevel(level)
22 | }
23 |
24 | // Writer returns an io.Writer that writes to the logger at the info log level
25 | func (entry *Entry) Writer() *io.PipeWriter {
26 | return entry.WriterLevel(InfoLevel)
27 | }
28 |
29 | // WriterLevel returns an io.Writer that writes to the logger at the given log level
30 | func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
31 | reader, writer := io.Pipe()
32 |
33 | var printFunc func(args ...interface{})
34 |
35 | // Determine which log function to use based on the specified log level
36 | switch level {
37 | case TraceLevel:
38 | printFunc = entry.Trace
39 | case DebugLevel:
40 | printFunc = entry.Debug
41 | case InfoLevel:
42 | printFunc = entry.Info
43 | case WarnLevel:
44 | printFunc = entry.Warn
45 | case ErrorLevel:
46 | printFunc = entry.Error
47 | case FatalLevel:
48 | printFunc = entry.Fatal
49 | case PanicLevel:
50 | printFunc = entry.Panic
51 | default:
52 | printFunc = entry.Print
53 | }
54 |
55 | // Start a new goroutine to scan the input and write it to the logger using the specified print function.
56 | // It splits the input into chunks of up to 64KB to avoid buffer overflows.
57 | go entry.writerScanner(reader, printFunc)
58 |
59 | // Set a finalizer function to close the writer when it is garbage collected
60 | runtime.SetFinalizer(writer, writerFinalizer)
61 |
62 | return writer
63 | }
64 |
65 | // writerScanner scans the input from the reader and writes it to the logger
66 | func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
67 | scanner := bufio.NewScanner(reader)
68 |
69 | // Set the buffer size to the maximum token size to avoid buffer overflows
70 | scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize)
71 |
72 | // Define a split function to split the input into chunks of up to 64KB
73 | chunkSize := bufio.MaxScanTokenSize // 64KB
74 | splitFunc := func(data []byte, atEOF bool) (int, []byte, error) {
75 | if len(data) >= chunkSize {
76 | return chunkSize, data[:chunkSize], nil
77 | }
78 |
79 | return bufio.ScanLines(data, atEOF)
80 | }
81 |
82 | // Use the custom split function to split the input
83 | scanner.Split(splitFunc)
84 |
85 | // Scan the input and write it to the logger using the specified print function
86 | for scanner.Scan() {
87 | printFunc(strings.TrimRight(scanner.Text(), "\r\n"))
88 | }
89 |
90 | // If there was an error while scanning the input, log an error
91 | if err := scanner.Err(); err != nil {
92 | entry.Errorf("Error while reading from Writer: %s", err)
93 | }
94 |
95 | // Close the reader when we are done
96 | reader.Close()
97 | }
98 |
99 | // WriterFinalizer is a finalizer function that closes then given writer when it is garbage collected
100 | func writerFinalizer(writer *io.PipeWriter) {
101 | writer.Close()
102 | }
103 |
--------------------------------------------------------------------------------
/alt_exit_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "log"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "runtime"
9 | "strings"
10 | "testing"
11 | "time"
12 | )
13 |
14 | func TestRegister(t *testing.T) {
15 | current := len(handlers)
16 |
17 | var results []string
18 |
19 | h1 := func() { results = append(results, "first") }
20 | h2 := func() { results = append(results, "second") }
21 |
22 | RegisterExitHandler(h1)
23 | RegisterExitHandler(h2)
24 |
25 | if len(handlers) != current+2 {
26 | t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
27 | }
28 |
29 | runHandlers()
30 |
31 | if len(results) != 2 {
32 | t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
33 | }
34 |
35 | if results[0] != "first" {
36 | t.Fatal("expected handler h1 to be run first, but it wasn't")
37 | }
38 |
39 | if results[1] != "second" {
40 | t.Fatal("expected handler h2 to be run second, but it wasn't")
41 | }
42 | }
43 |
44 | func TestDefer(t *testing.T) {
45 | current := len(handlers)
46 |
47 | var results []string
48 |
49 | h1 := func() { results = append(results, "first") }
50 | h2 := func() { results = append(results, "second") }
51 |
52 | DeferExitHandler(h1)
53 | DeferExitHandler(h2)
54 |
55 | if len(handlers) != current+2 {
56 | t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
57 | }
58 |
59 | runHandlers()
60 |
61 | if len(results) != 2 {
62 | t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
63 | }
64 |
65 | if results[0] != "second" {
66 | t.Fatal("expected handler h2 to be run first, but it wasn't")
67 | }
68 |
69 | if results[1] != "first" {
70 | t.Fatal("expected handler h1 to be run second, but it wasn't")
71 | }
72 | }
73 |
74 | func TestHandler(t *testing.T) {
75 | testprog := testprogleader
76 | testprog = append(testprog, getPackage()...)
77 | testprog = append(testprog, testprogtrailer...)
78 | tempDir, err := os.MkdirTemp("", "test_handler")
79 | if err != nil {
80 | log.Fatalf("can't create temp dir. %q", err)
81 | }
82 | defer os.RemoveAll(tempDir)
83 |
84 | gofile := filepath.Join(tempDir, "gofile.go")
85 | if err := os.WriteFile(gofile, testprog, 0666); err != nil {
86 | t.Fatalf("can't create go file. %q", err)
87 | }
88 |
89 | outfile := filepath.Join(tempDir, "outfile.out")
90 | arg := time.Now().UTC().String()
91 | err = exec.Command("go", "run", gofile, outfile, arg).Run()
92 | if err == nil {
93 | t.Fatalf("completed normally, should have failed")
94 | }
95 |
96 | data, err := os.ReadFile(outfile)
97 | if err != nil {
98 | t.Fatalf("can't read output file %s. %q", outfile, err)
99 | }
100 |
101 | if string(data) != arg {
102 | t.Fatalf("bad data. Expected %q, got %q", data, arg)
103 | }
104 | }
105 |
106 | // getPackage returns the name of the current package, which makes running this
107 | // test in a fork simpler
108 | func getPackage() []byte {
109 | pc, _, _, _ := runtime.Caller(0)
110 | fullFuncName := runtime.FuncForPC(pc).Name()
111 | idx := strings.LastIndex(fullFuncName, ".")
112 | return []byte(fullFuncName[:idx]) // trim off function details
113 | }
114 |
115 | var testprogleader = []byte(`
116 | // Test program for atexit, gets output file and data as arguments and writes
117 | // data to output file in atexit handler.
118 | package main
119 |
120 | import (
121 | "`)
122 | var testprogtrailer = []byte(
123 | `"
124 | "flag"
125 | "fmt"
126 | "io/ioutil"
127 | )
128 |
129 | var outfile = ""
130 | var data = ""
131 |
132 | func handler() {
133 | ioutil.WriteFile(outfile, []byte(data), 0666)
134 | }
135 |
136 | func badHandler() {
137 | n := 0
138 | fmt.Println(1/n)
139 | }
140 |
141 | func main() {
142 | flag.Parse()
143 | outfile = flag.Arg(0)
144 | data = flag.Arg(1)
145 |
146 | logrus.RegisterExitHandler(handler)
147 | logrus.RegisterExitHandler(badHandler)
148 | logrus.Fatal("Bye bye")
149 | }
150 | `)
151 |
--------------------------------------------------------------------------------
/json_formatter.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "runtime"
8 | )
9 |
10 | type fieldKey string
11 |
12 | // FieldMap allows customization of the key names for default fields.
13 | type FieldMap map[fieldKey]string
14 |
15 | func (f FieldMap) resolve(key fieldKey) string {
16 | if k, ok := f[key]; ok {
17 | return k
18 | }
19 |
20 | return string(key)
21 | }
22 |
23 | // JSONFormatter formats logs into parsable json
24 | type JSONFormatter struct {
25 | // TimestampFormat sets the format used for marshaling timestamps.
26 | // The format to use is the same than for time.Format or time.Parse from the standard
27 | // library.
28 | // The standard Library already provides a set of predefined format.
29 | TimestampFormat string
30 |
31 | // DisableTimestamp allows disabling automatic timestamps in output
32 | DisableTimestamp bool
33 |
34 | // DisableHTMLEscape allows disabling html escaping in output
35 | DisableHTMLEscape bool
36 |
37 | // DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
38 | DataKey string
39 |
40 | // FieldMap allows users to customize the names of keys for default fields.
41 | // As an example:
42 | // formatter := &JSONFormatter{
43 | // FieldMap: FieldMap{
44 | // FieldKeyTime: "@timestamp",
45 | // FieldKeyLevel: "@level",
46 | // FieldKeyMsg: "@message",
47 | // FieldKeyFunc: "@caller",
48 | // },
49 | // }
50 | FieldMap FieldMap
51 |
52 | // CallerPrettyfier can be set by the user to modify the content
53 | // of the function and file keys in the json data when ReportCaller is
54 | // activated. If any of the returned value is the empty string the
55 | // corresponding key will be removed from json fields.
56 | CallerPrettyfier func(*runtime.Frame) (function string, file string)
57 |
58 | // PrettyPrint will indent all json logs
59 | PrettyPrint bool
60 | }
61 |
62 | // Format renders a single log entry
63 | func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
64 | data := make(Fields, len(entry.Data)+4)
65 | for k, v := range entry.Data {
66 | switch v := v.(type) {
67 | case error:
68 | // Otherwise errors are ignored by `encoding/json`
69 | // https://github.com/sirupsen/logrus/issues/137
70 | data[k] = v.Error()
71 | default:
72 | data[k] = v
73 | }
74 | }
75 |
76 | if f.DataKey != "" {
77 | newData := make(Fields, 4)
78 | newData[f.DataKey] = data
79 | data = newData
80 | }
81 |
82 | prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
83 |
84 | timestampFormat := f.TimestampFormat
85 | if timestampFormat == "" {
86 | timestampFormat = defaultTimestampFormat
87 | }
88 |
89 | if entry.err != "" {
90 | data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
91 | }
92 | if !f.DisableTimestamp {
93 | data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
94 | }
95 | data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
96 | data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
97 | if entry.HasCaller() {
98 | funcVal := entry.Caller.Function
99 | fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
100 | if f.CallerPrettyfier != nil {
101 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
102 | }
103 | if funcVal != "" {
104 | data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
105 | }
106 | if fileVal != "" {
107 | data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
108 | }
109 | }
110 |
111 | var b *bytes.Buffer
112 | if entry.Buffer != nil {
113 | b = entry.Buffer
114 | } else {
115 | b = &bytes.Buffer{}
116 | }
117 |
118 | encoder := json.NewEncoder(b)
119 | encoder.SetEscapeHTML(!f.DisableHTMLEscape)
120 | if f.PrettyPrint {
121 | encoder.SetIndent("", " ")
122 | }
123 | if err := encoder.Encode(data); err != nil {
124 | return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
125 | }
126 |
127 | return b.Bytes(), nil
128 | }
129 |
--------------------------------------------------------------------------------
/hook_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "sync"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | . "github.com/sirupsen/logrus"
14 | "github.com/sirupsen/logrus/hooks/test"
15 | . "github.com/sirupsen/logrus/internal/testutils"
16 | )
17 |
18 | type TestHook struct {
19 | Fired bool
20 | }
21 |
22 | func (hook *TestHook) Fire(entry *Entry) error {
23 | hook.Fired = true
24 | return nil
25 | }
26 |
27 | func (hook *TestHook) Levels() []Level {
28 | return []Level{
29 | TraceLevel,
30 | DebugLevel,
31 | InfoLevel,
32 | WarnLevel,
33 | ErrorLevel,
34 | FatalLevel,
35 | PanicLevel,
36 | }
37 | }
38 |
39 | func TestHookFires(t *testing.T) {
40 | hook := new(TestHook)
41 |
42 | LogAndAssertJSON(t, func(log *Logger) {
43 | log.Hooks.Add(hook)
44 | assert.False(t, hook.Fired)
45 |
46 | log.Print("test")
47 | }, func(fields Fields) {
48 | assert.True(t, hook.Fired)
49 | })
50 | }
51 |
52 | type ModifyHook struct {
53 | }
54 |
55 | func (hook *ModifyHook) Fire(entry *Entry) error {
56 | entry.Data["wow"] = "whale"
57 | return nil
58 | }
59 |
60 | func (hook *ModifyHook) Levels() []Level {
61 | return []Level{
62 | TraceLevel,
63 | DebugLevel,
64 | InfoLevel,
65 | WarnLevel,
66 | ErrorLevel,
67 | FatalLevel,
68 | PanicLevel,
69 | }
70 | }
71 |
72 | func TestHookCanModifyEntry(t *testing.T) {
73 | hook := new(ModifyHook)
74 |
75 | LogAndAssertJSON(t, func(log *Logger) {
76 | log.Hooks.Add(hook)
77 | log.WithField("wow", "elephant").Print("test")
78 | }, func(fields Fields) {
79 | assert.Equal(t, "whale", fields["wow"])
80 | })
81 | }
82 |
83 | func TestCanFireMultipleHooks(t *testing.T) {
84 | hook1 := new(ModifyHook)
85 | hook2 := new(TestHook)
86 |
87 | LogAndAssertJSON(t, func(log *Logger) {
88 | log.Hooks.Add(hook1)
89 | log.Hooks.Add(hook2)
90 |
91 | log.WithField("wow", "elephant").Print("test")
92 | }, func(fields Fields) {
93 | assert.Equal(t, "whale", fields["wow"])
94 | assert.True(t, hook2.Fired)
95 | })
96 | }
97 |
98 | type SingleLevelModifyHook struct {
99 | ModifyHook
100 | }
101 |
102 | func (h *SingleLevelModifyHook) Levels() []Level {
103 | return []Level{InfoLevel}
104 | }
105 |
106 | func TestHookEntryIsPristine(t *testing.T) {
107 | l := New()
108 | b := &bytes.Buffer{}
109 | l.Formatter = &JSONFormatter{}
110 | l.Out = b
111 | l.AddHook(&SingleLevelModifyHook{})
112 |
113 | l.Error("error message")
114 | data := map[string]string{}
115 | err := json.Unmarshal(b.Bytes(), &data)
116 | require.NoError(t, err)
117 | _, ok := data["wow"]
118 | require.False(t, ok)
119 | b.Reset()
120 |
121 | l.Info("error message")
122 | data = map[string]string{}
123 | err = json.Unmarshal(b.Bytes(), &data)
124 | require.NoError(t, err)
125 | _, ok = data["wow"]
126 | require.True(t, ok)
127 | b.Reset()
128 |
129 | l.Error("error message")
130 | data = map[string]string{}
131 | err = json.Unmarshal(b.Bytes(), &data)
132 | require.NoError(t, err)
133 | _, ok = data["wow"]
134 | require.False(t, ok)
135 | b.Reset()
136 | }
137 |
138 | type ErrorHook struct {
139 | Fired bool
140 | }
141 |
142 | func (hook *ErrorHook) Fire(entry *Entry) error {
143 | hook.Fired = true
144 | return nil
145 | }
146 |
147 | func (hook *ErrorHook) Levels() []Level {
148 | return []Level{
149 | ErrorLevel,
150 | }
151 | }
152 |
153 | func TestErrorHookShouldntFireOnInfo(t *testing.T) {
154 | hook := new(ErrorHook)
155 |
156 | LogAndAssertJSON(t, func(log *Logger) {
157 | log.Hooks.Add(hook)
158 | log.Info("test")
159 | }, func(fields Fields) {
160 | assert.False(t, hook.Fired)
161 | })
162 | }
163 |
164 | func TestErrorHookShouldFireOnError(t *testing.T) {
165 | hook := new(ErrorHook)
166 |
167 | LogAndAssertJSON(t, func(log *Logger) {
168 | log.Hooks.Add(hook)
169 | log.Error("test")
170 | }, func(fields Fields) {
171 | assert.True(t, hook.Fired)
172 | })
173 | }
174 |
175 | func TestAddHookRace(t *testing.T) {
176 | var wg sync.WaitGroup
177 | wg.Add(2)
178 | hook := new(ErrorHook)
179 | LogAndAssertJSON(t, func(log *Logger) {
180 | go func() {
181 | defer wg.Done()
182 | log.AddHook(hook)
183 | }()
184 | go func() {
185 | defer wg.Done()
186 | log.Error("test")
187 | }()
188 | wg.Wait()
189 | }, func(fields Fields) {
190 | // the line may have been logged
191 | // before the hook was added, so we can't
192 | // actually assert on the hook
193 | })
194 | }
195 |
196 | func TestAddHookRace2(t *testing.T) {
197 | t.Parallel()
198 |
199 | for i := 0; i < 3; i++ {
200 | testname := fmt.Sprintf("Test %d", i)
201 | t.Run(testname, func(t *testing.T) {
202 | t.Parallel()
203 |
204 | _ = test.NewGlobal()
205 | Info(testname)
206 | })
207 | }
208 | }
209 |
210 | type HookCallFunc struct {
211 | F func()
212 | }
213 |
214 | func (h *HookCallFunc) Levels() []Level {
215 | return AllLevels
216 | }
217 |
218 | func (h *HookCallFunc) Fire(e *Entry) error {
219 | h.F()
220 | return nil
221 | }
222 |
223 | func TestHookFireOrder(t *testing.T) {
224 | checkers := []string{}
225 | h := LevelHooks{}
226 | h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "first hook") }})
227 | h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "second hook") }})
228 | h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "third hook") }})
229 |
230 | if err := h.Fire(InfoLevel, &Entry{}); err != nil {
231 | t.Error("unexpected error:", err)
232 | }
233 | require.Equal(t, []string{"first hook", "second hook", "third hook"}, checkers)
234 | }
235 |
--------------------------------------------------------------------------------
/logrus.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 | )
8 |
9 | // Fields type, used to pass to [WithFields].
10 | type Fields map[string]interface{}
11 |
12 | // Level type
13 | //
14 | //nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver.
15 | type Level uint32
16 |
17 | // Convert the Level to a string. E.g. [PanicLevel] becomes "panic".
18 | func (level Level) String() string {
19 | if b, err := level.MarshalText(); err == nil {
20 | return string(b)
21 | } else {
22 | return "unknown"
23 | }
24 | }
25 |
26 | // ParseLevel takes a string level and returns the Logrus log level constant.
27 | func ParseLevel(lvl string) (Level, error) {
28 | switch strings.ToLower(lvl) {
29 | case "panic":
30 | return PanicLevel, nil
31 | case "fatal":
32 | return FatalLevel, nil
33 | case "error":
34 | return ErrorLevel, nil
35 | case "warn", "warning":
36 | return WarnLevel, nil
37 | case "info":
38 | return InfoLevel, nil
39 | case "debug":
40 | return DebugLevel, nil
41 | case "trace":
42 | return TraceLevel, nil
43 | }
44 |
45 | var l Level
46 | return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
47 | }
48 |
49 | // UnmarshalText implements encoding.TextUnmarshaler.
50 | func (level *Level) UnmarshalText(text []byte) error {
51 | l, err := ParseLevel(string(text))
52 | if err != nil {
53 | return err
54 | }
55 |
56 | *level = l
57 |
58 | return nil
59 | }
60 |
61 | func (level Level) MarshalText() ([]byte, error) {
62 | switch level {
63 | case TraceLevel:
64 | return []byte("trace"), nil
65 | case DebugLevel:
66 | return []byte("debug"), nil
67 | case InfoLevel:
68 | return []byte("info"), nil
69 | case WarnLevel:
70 | return []byte("warning"), nil
71 | case ErrorLevel:
72 | return []byte("error"), nil
73 | case FatalLevel:
74 | return []byte("fatal"), nil
75 | case PanicLevel:
76 | return []byte("panic"), nil
77 | }
78 |
79 | return nil, fmt.Errorf("not a valid logrus level %d", level)
80 | }
81 |
82 | // AllLevels exposing all logging levels.
83 | var AllLevels = []Level{
84 | PanicLevel,
85 | FatalLevel,
86 | ErrorLevel,
87 | WarnLevel,
88 | InfoLevel,
89 | DebugLevel,
90 | TraceLevel,
91 | }
92 |
93 | // These are the different logging levels. You can set the logging level to log
94 | // on your instance of logger, obtained with `logrus.New()`.
95 | const (
96 | // PanicLevel level, highest level of severity. Logs and then calls panic with the
97 | // message passed to Debug, Info, ...
98 | PanicLevel Level = iota
99 | // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
100 | // logging level is set to Panic.
101 | FatalLevel
102 | // ErrorLevel level. Logs. Used for errors that should definitely be noted.
103 | // Commonly used for hooks to send errors to an error tracking service.
104 | ErrorLevel
105 | // WarnLevel level. Non-critical entries that deserve eyes.
106 | WarnLevel
107 | // InfoLevel level. General operational entries about what's going on inside the
108 | // application.
109 | InfoLevel
110 | // DebugLevel level. Usually only enabled when debugging. Very verbose logging.
111 | DebugLevel
112 | // TraceLevel level. Designates finer-grained informational events than the Debug.
113 | TraceLevel
114 | )
115 |
116 | // Won't compile if StdLogger can't be realized by a log.Logger
117 | var (
118 | _ StdLogger = &log.Logger{}
119 | _ StdLogger = &Entry{}
120 | _ StdLogger = &Logger{}
121 | )
122 |
123 | // StdLogger is what your logrus-enabled library should take, that way
124 | // it'll accept a stdlib logger ([log.Logger]) and a logrus logger.
125 | // There's no standard interface, so this is the closest we get, unfortunately.
126 | type StdLogger interface {
127 | Print(...interface{})
128 | Printf(string, ...interface{})
129 | Println(...interface{})
130 |
131 | Fatal(...interface{})
132 | Fatalf(string, ...interface{})
133 | Fatalln(...interface{})
134 |
135 | Panic(...interface{})
136 | Panicf(string, ...interface{})
137 | Panicln(...interface{})
138 | }
139 |
140 | // FieldLogger extends the [StdLogger] interface, generalizing
141 | // the [Entry] and [Logger] types.
142 | type FieldLogger interface {
143 | WithField(key string, value interface{}) *Entry
144 | WithFields(fields Fields) *Entry
145 | WithError(err error) *Entry
146 |
147 | Debugf(format string, args ...interface{})
148 | Infof(format string, args ...interface{})
149 | Printf(format string, args ...interface{})
150 | Warnf(format string, args ...interface{})
151 | Warningf(format string, args ...interface{})
152 | Errorf(format string, args ...interface{})
153 | Fatalf(format string, args ...interface{})
154 | Panicf(format string, args ...interface{})
155 |
156 | Debug(args ...interface{})
157 | Info(args ...interface{})
158 | Print(args ...interface{})
159 | Warn(args ...interface{})
160 | Warning(args ...interface{})
161 | Error(args ...interface{})
162 | Fatal(args ...interface{})
163 | Panic(args ...interface{})
164 |
165 | Debugln(args ...interface{})
166 | Infoln(args ...interface{})
167 | Println(args ...interface{})
168 | Warnln(args ...interface{})
169 | Warningln(args ...interface{})
170 | Errorln(args ...interface{})
171 | Fatalln(args ...interface{})
172 | Panicln(args ...interface{})
173 |
174 | // IsDebugEnabled() bool
175 | // IsInfoEnabled() bool
176 | // IsWarnEnabled() bool
177 | // IsErrorEnabled() bool
178 | // IsFatalEnabled() bool
179 | // IsPanicEnabled() bool
180 | }
181 |
182 | // Ext1FieldLogger (the first extension to [FieldLogger]) is superfluous, it is
183 | // here for consistency. Do not use. Use [FieldLogger], [Logger] or [Entry]
184 | // instead.
185 | type Ext1FieldLogger interface {
186 | FieldLogger
187 | Tracef(format string, args ...interface{})
188 | Trace(args ...interface{})
189 | Traceln(args ...interface{})
190 | }
191 |
--------------------------------------------------------------------------------
/exported.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "context"
5 | "io"
6 | "time"
7 | )
8 |
9 | var (
10 | // std is the name of the standard logger in stdlib `log`
11 | std = New()
12 | )
13 |
14 | func StandardLogger() *Logger {
15 | return std
16 | }
17 |
18 | // SetOutput sets the standard logger output.
19 | func SetOutput(out io.Writer) {
20 | std.SetOutput(out)
21 | }
22 |
23 | // SetFormatter sets the standard logger formatter.
24 | func SetFormatter(formatter Formatter) {
25 | std.SetFormatter(formatter)
26 | }
27 |
28 | // SetReportCaller sets whether the standard logger will include the calling
29 | // method as a field.
30 | func SetReportCaller(include bool) {
31 | std.SetReportCaller(include)
32 | }
33 |
34 | // SetLevel sets the standard logger level.
35 | func SetLevel(level Level) {
36 | std.SetLevel(level)
37 | }
38 |
39 | // GetLevel returns the standard logger level.
40 | func GetLevel() Level {
41 | return std.GetLevel()
42 | }
43 |
44 | // IsLevelEnabled checks if the log level of the standard logger is greater than the level param
45 | func IsLevelEnabled(level Level) bool {
46 | return std.IsLevelEnabled(level)
47 | }
48 |
49 | // AddHook adds a hook to the standard logger hooks.
50 | func AddHook(hook Hook) {
51 | std.AddHook(hook)
52 | }
53 |
54 | // WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
55 | func WithError(err error) *Entry {
56 | return std.WithField(ErrorKey, err)
57 | }
58 |
59 | // WithContext creates an entry from the standard logger and adds a context to it.
60 | func WithContext(ctx context.Context) *Entry {
61 | return std.WithContext(ctx)
62 | }
63 |
64 | // WithField creates an entry from the standard logger and adds a field to
65 | // it. If you want multiple fields, use `WithFields`.
66 | //
67 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
68 | // or Panic on the Entry it returns.
69 | func WithField(key string, value interface{}) *Entry {
70 | return std.WithField(key, value)
71 | }
72 |
73 | // WithFields creates an entry from the standard logger and adds multiple
74 | // fields to it. This is simply a helper for `WithField`, invoking it
75 | // once for each field.
76 | //
77 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
78 | // or Panic on the Entry it returns.
79 | func WithFields(fields Fields) *Entry {
80 | return std.WithFields(fields)
81 | }
82 |
83 | // WithTime creates an entry from the standard logger and overrides the time of
84 | // logs generated with it.
85 | //
86 | // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
87 | // or Panic on the Entry it returns.
88 | func WithTime(t time.Time) *Entry {
89 | return std.WithTime(t)
90 | }
91 |
92 | // Trace logs a message at level Trace on the standard logger.
93 | func Trace(args ...interface{}) {
94 | std.Trace(args...)
95 | }
96 |
97 | // Debug logs a message at level Debug on the standard logger.
98 | func Debug(args ...interface{}) {
99 | std.Debug(args...)
100 | }
101 |
102 | // Print logs a message at level Info on the standard logger.
103 | func Print(args ...interface{}) {
104 | std.Print(args...)
105 | }
106 |
107 | // Info logs a message at level Info on the standard logger.
108 | func Info(args ...interface{}) {
109 | std.Info(args...)
110 | }
111 |
112 | // Warn logs a message at level Warn on the standard logger.
113 | func Warn(args ...interface{}) {
114 | std.Warn(args...)
115 | }
116 |
117 | // Warning logs a message at level Warn on the standard logger.
118 | func Warning(args ...interface{}) {
119 | std.Warning(args...)
120 | }
121 |
122 | // Error logs a message at level Error on the standard logger.
123 | func Error(args ...interface{}) {
124 | std.Error(args...)
125 | }
126 |
127 | // Panic logs a message at level Panic on the standard logger.
128 | func Panic(args ...interface{}) {
129 | std.Panic(args...)
130 | }
131 |
132 | // Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
133 | func Fatal(args ...interface{}) {
134 | std.Fatal(args...)
135 | }
136 |
137 | // TraceFn logs a message from a func at level Trace on the standard logger.
138 | func TraceFn(fn LogFunction) {
139 | std.TraceFn(fn)
140 | }
141 |
142 | // DebugFn logs a message from a func at level Debug on the standard logger.
143 | func DebugFn(fn LogFunction) {
144 | std.DebugFn(fn)
145 | }
146 |
147 | // PrintFn logs a message from a func at level Info on the standard logger.
148 | func PrintFn(fn LogFunction) {
149 | std.PrintFn(fn)
150 | }
151 |
152 | // InfoFn logs a message from a func at level Info on the standard logger.
153 | func InfoFn(fn LogFunction) {
154 | std.InfoFn(fn)
155 | }
156 |
157 | // WarnFn logs a message from a func at level Warn on the standard logger.
158 | func WarnFn(fn LogFunction) {
159 | std.WarnFn(fn)
160 | }
161 |
162 | // WarningFn logs a message from a func at level Warn on the standard logger.
163 | func WarningFn(fn LogFunction) {
164 | std.WarningFn(fn)
165 | }
166 |
167 | // ErrorFn logs a message from a func at level Error on the standard logger.
168 | func ErrorFn(fn LogFunction) {
169 | std.ErrorFn(fn)
170 | }
171 |
172 | // PanicFn logs a message from a func at level Panic on the standard logger.
173 | func PanicFn(fn LogFunction) {
174 | std.PanicFn(fn)
175 | }
176 |
177 | // FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
178 | func FatalFn(fn LogFunction) {
179 | std.FatalFn(fn)
180 | }
181 |
182 | // Tracef logs a message at level Trace on the standard logger.
183 | func Tracef(format string, args ...interface{}) {
184 | std.Tracef(format, args...)
185 | }
186 |
187 | // Debugf logs a message at level Debug on the standard logger.
188 | func Debugf(format string, args ...interface{}) {
189 | std.Debugf(format, args...)
190 | }
191 |
192 | // Printf logs a message at level Info on the standard logger.
193 | func Printf(format string, args ...interface{}) {
194 | std.Printf(format, args...)
195 | }
196 |
197 | // Infof logs a message at level Info on the standard logger.
198 | func Infof(format string, args ...interface{}) {
199 | std.Infof(format, args...)
200 | }
201 |
202 | // Warnf logs a message at level Warn on the standard logger.
203 | func Warnf(format string, args ...interface{}) {
204 | std.Warnf(format, args...)
205 | }
206 |
207 | // Warningf logs a message at level Warn on the standard logger.
208 | func Warningf(format string, args ...interface{}) {
209 | std.Warningf(format, args...)
210 | }
211 |
212 | // Errorf logs a message at level Error on the standard logger.
213 | func Errorf(format string, args ...interface{}) {
214 | std.Errorf(format, args...)
215 | }
216 |
217 | // Panicf logs a message at level Panic on the standard logger.
218 | func Panicf(format string, args ...interface{}) {
219 | std.Panicf(format, args...)
220 | }
221 |
222 | // Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
223 | func Fatalf(format string, args ...interface{}) {
224 | std.Fatalf(format, args...)
225 | }
226 |
227 | // Traceln logs a message at level Trace on the standard logger.
228 | func Traceln(args ...interface{}) {
229 | std.Traceln(args...)
230 | }
231 |
232 | // Debugln logs a message at level Debug on the standard logger.
233 | func Debugln(args ...interface{}) {
234 | std.Debugln(args...)
235 | }
236 |
237 | // Println logs a message at level Info on the standard logger.
238 | func Println(args ...interface{}) {
239 | std.Println(args...)
240 | }
241 |
242 | // Infoln logs a message at level Info on the standard logger.
243 | func Infoln(args ...interface{}) {
244 | std.Infoln(args...)
245 | }
246 |
247 | // Warnln logs a message at level Warn on the standard logger.
248 | func Warnln(args ...interface{}) {
249 | std.Warnln(args...)
250 | }
251 |
252 | // Warningln logs a message at level Warn on the standard logger.
253 | func Warningln(args ...interface{}) {
254 | std.Warningln(args...)
255 | }
256 |
257 | // Errorln logs a message at level Error on the standard logger.
258 | func Errorln(args ...interface{}) {
259 | std.Errorln(args...)
260 | }
261 |
262 | // Panicln logs a message at level Panic on the standard logger.
263 | func Panicln(args ...interface{}) {
264 | std.Panicln(args...)
265 | }
266 |
267 | // Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
268 | func Fatalln(args ...interface{}) {
269 | std.Fatalln(args...)
270 | }
271 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.8.1
2 | Code quality:
3 | * move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
4 | * improve timestamp format documentation
5 |
6 | Fixes:
7 | * fix race condition on logger hooks
8 |
9 |
10 | # 1.8.0
11 |
12 | Correct versioning number replacing v1.7.1.
13 |
14 | # 1.7.1
15 |
16 | Beware this release has introduced a new public API and its semver is therefore incorrect.
17 |
18 | Code quality:
19 | * use go 1.15 in travis
20 | * use magefile as task runner
21 |
22 | Fixes:
23 | * small fixes about new go 1.13 error formatting system
24 | * Fix for long time race condiction with mutating data hooks
25 |
26 | Features:
27 | * build support for zos
28 |
29 | # 1.7.0
30 | Fixes:
31 | * the dependency toward a windows terminal library has been removed
32 |
33 | Features:
34 | * a new buffer pool management API has been added
35 | * a set of `Fn()` functions have been added
36 |
37 | # 1.6.0
38 | Fixes:
39 | * end of line cleanup
40 | * revert the entry concurrency bug fix which leads to deadlock under some circumstances
41 | * update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
42 |
43 | Features:
44 | * add an option to the `TextFormatter` to completely disable fields quoting
45 |
46 | # 1.5.0
47 | Code quality:
48 | * add golangci linter run on travis
49 |
50 | Fixes:
51 | * add mutex for hooks concurrent access on `Entry` data
52 | * caller function field for go1.14
53 | * fix build issue for gopherjs target
54 |
55 | Feature:
56 | * add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
57 | * add a `DisableHTMLEscape` option in the `JSONFormatter`
58 | * add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
59 |
60 | # 1.4.2
61 | * Fixes build break for plan9, nacl, solaris
62 | # 1.4.1
63 | This new release introduces:
64 | * Enhance TextFormatter to not print caller information when they are empty (#944)
65 | * Remove dependency on golang.org/x/crypto (#932, #943)
66 |
67 | Fixes:
68 | * Fix Entry.WithContext method to return a copy of the initial entry (#941)
69 |
70 | # 1.4.0
71 | This new release introduces:
72 | * Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
73 | * Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
74 | * Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
75 |
76 | Fixes:
77 | * Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
78 | * Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
79 | * Fix infinite recursion on unknown `Level.String()` (#907)
80 | * Fix race condition in `getCaller` (#916).
81 |
82 |
83 | # 1.3.0
84 | This new release introduces:
85 | * Log, Logf, Logln functions for Logger and Entry that take a Level
86 |
87 | Fixes:
88 | * Building prometheus node_exporter on AIX (#840)
89 | * Race condition in TextFormatter (#468)
90 | * Travis CI import path (#868)
91 | * Remove coloured output on Windows (#862)
92 | * Pointer to func as field in JSONFormatter (#870)
93 | * Properly marshal Levels (#873)
94 |
95 | # 1.2.0
96 | This new release introduces:
97 | * A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
98 | * A new trace level named `Trace` whose level is below `Debug`
99 | * A configurable exit function to be called upon a Fatal trace
100 | * The `Level` object now implements `encoding.TextUnmarshaler` interface
101 |
102 | # 1.1.1
103 | This is a bug fix release.
104 | * fix the build break on Solaris
105 | * don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
106 |
107 | # 1.1.0
108 | This new release introduces:
109 | * several fixes:
110 | * a fix for a race condition on entry formatting
111 | * proper cleanup of previously used entries before putting them back in the pool
112 | * the extra new line at the end of message in text formatter has been removed
113 | * a new global public API to check if a level is activated: IsLevelEnabled
114 | * the following methods have been added to the Logger object
115 | * IsLevelEnabled
116 | * SetFormatter
117 | * SetOutput
118 | * ReplaceHooks
119 | * introduction of go module
120 | * an indent configuration for the json formatter
121 | * output colour support for windows
122 | * the field sort function is now configurable for text formatter
123 | * the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
124 |
125 | # 1.0.6
126 |
127 | This new release introduces:
128 | * a new api WithTime which allows to easily force the time of the log entry
129 | which is mostly useful for logger wrapper
130 | * a fix reverting the immutability of the entry given as parameter to the hooks
131 | a new configuration field of the json formatter in order to put all the fields
132 | in a nested dictionary
133 | * a new SetOutput method in the Logger
134 | * a new configuration of the textformatter to configure the name of the default keys
135 | * a new configuration of the text formatter to disable the level truncation
136 |
137 | # 1.0.5
138 |
139 | * Fix hooks race (#707)
140 | * Fix panic deadlock (#695)
141 |
142 | # 1.0.4
143 |
144 | * Fix race when adding hooks (#612)
145 | * Fix terminal check in AppEngine (#635)
146 |
147 | # 1.0.3
148 |
149 | * Replace example files with testable examples
150 |
151 | # 1.0.2
152 |
153 | * bug: quote non-string values in text formatter (#583)
154 | * Make (*Logger) SetLevel a public method
155 |
156 | # 1.0.1
157 |
158 | * bug: fix escaping in text formatter (#575)
159 |
160 | # 1.0.0
161 |
162 | * Officially changed name to lower-case
163 | * bug: colors on Windows 10 (#541)
164 | * bug: fix race in accessing level (#512)
165 |
166 | # 0.11.5
167 |
168 | * feature: add writer and writerlevel to entry (#372)
169 |
170 | # 0.11.4
171 |
172 | * bug: fix undefined variable on solaris (#493)
173 |
174 | # 0.11.3
175 |
176 | * formatter: configure quoting of empty values (#484)
177 | * formatter: configure quoting character (default is `"`) (#484)
178 | * bug: fix not importing io correctly in non-linux environments (#481)
179 |
180 | # 0.11.2
181 |
182 | * bug: fix windows terminal detection (#476)
183 |
184 | # 0.11.1
185 |
186 | * bug: fix tty detection with custom out (#471)
187 |
188 | # 0.11.0
189 |
190 | * performance: Use bufferpool to allocate (#370)
191 | * terminal: terminal detection for app-engine (#343)
192 | * feature: exit handler (#375)
193 |
194 | # 0.10.0
195 |
196 | * feature: Add a test hook (#180)
197 | * feature: `ParseLevel` is now case-insensitive (#326)
198 | * feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
199 | * performance: avoid re-allocations on `WithFields` (#335)
200 |
201 | # 0.9.0
202 |
203 | * logrus/text_formatter: don't emit empty msg
204 | * logrus/hooks/airbrake: move out of main repository
205 | * logrus/hooks/sentry: move out of main repository
206 | * logrus/hooks/papertrail: move out of main repository
207 | * logrus/hooks/bugsnag: move out of main repository
208 | * logrus/core: run tests with `-race`
209 | * logrus/core: detect TTY based on `stderr`
210 | * logrus/core: support `WithError` on logger
211 | * logrus/core: Solaris support
212 |
213 | # 0.8.7
214 |
215 | * logrus/core: fix possible race (#216)
216 | * logrus/doc: small typo fixes and doc improvements
217 |
218 |
219 | # 0.8.6
220 |
221 | * hooks/raven: allow passing an initialized client
222 |
223 | # 0.8.5
224 |
225 | * logrus/core: revert #208
226 |
227 | # 0.8.4
228 |
229 | * formatter/text: fix data race (#218)
230 |
231 | # 0.8.3
232 |
233 | * logrus/core: fix entry log level (#208)
234 | * logrus/core: improve performance of text formatter by 40%
235 | * logrus/core: expose `LevelHooks` type
236 | * logrus/core: add support for DragonflyBSD and NetBSD
237 | * formatter/text: print structs more verbosely
238 |
239 | # 0.8.2
240 |
241 | * logrus: fix more Fatal family functions
242 |
243 | # 0.8.1
244 |
245 | * logrus: fix not exiting on `Fatalf` and `Fatalln`
246 |
247 | # 0.8.0
248 |
249 | * logrus: defaults to stderr instead of stdout
250 | * hooks/sentry: add special field for `*http.Request`
251 | * formatter/text: ignore Windows for colors
252 |
253 | # 0.7.3
254 |
255 | * formatter/\*: allow configuration of timestamp layout
256 |
257 | # 0.7.2
258 |
259 | * formatter/text: Add configuration option for time format (#158)
260 |
--------------------------------------------------------------------------------
/entry_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "testing"
8 | "time"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | type contextKeyType string
14 |
15 | func TestEntryWithError(t *testing.T) {
16 |
17 | assert := assert.New(t)
18 |
19 | defer func() {
20 | ErrorKey = "error"
21 | }()
22 |
23 | err := fmt.Errorf("kaboom at layer %d", 4711)
24 |
25 | assert.Equal(err, WithError(err).Data["error"])
26 |
27 | logger := New()
28 | logger.Out = &bytes.Buffer{}
29 | entry := NewEntry(logger)
30 |
31 | assert.Equal(err, entry.WithError(err).Data["error"])
32 |
33 | ErrorKey = "err"
34 |
35 | assert.Equal(err, entry.WithError(err).Data["err"])
36 |
37 | }
38 |
39 | func TestEntryWithContext(t *testing.T) {
40 | assert := assert.New(t)
41 | var contextKey contextKeyType = "foo"
42 | ctx := context.WithValue(context.Background(), contextKey, "bar")
43 |
44 | assert.Equal(ctx, WithContext(ctx).Context)
45 |
46 | logger := New()
47 | logger.Out = &bytes.Buffer{}
48 | entry := NewEntry(logger)
49 |
50 | assert.Equal(ctx, entry.WithContext(ctx).Context)
51 | }
52 |
53 | func TestEntryWithContextCopiesData(t *testing.T) {
54 | assert := assert.New(t)
55 |
56 | // Initialize a parent Entry object with a key/value set in its Data map
57 | logger := New()
58 | logger.Out = &bytes.Buffer{}
59 | parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
60 |
61 | // Create two children Entry objects from the parent in different contexts
62 | var contextKey1 contextKeyType = "foo"
63 | ctx1 := context.WithValue(context.Background(), contextKey1, "bar")
64 | childEntry1 := parentEntry.WithContext(ctx1)
65 | assert.Equal(ctx1, childEntry1.Context)
66 |
67 | var contextKey2 contextKeyType = "bar"
68 | ctx2 := context.WithValue(context.Background(), contextKey2, "baz")
69 | childEntry2 := parentEntry.WithContext(ctx2)
70 | assert.Equal(ctx2, childEntry2.Context)
71 | assert.NotEqual(ctx1, ctx2)
72 |
73 | // Ensure that data set in the parent Entry are preserved to both children
74 | assert.Equal("parentValue", childEntry1.Data["parentKey"])
75 | assert.Equal("parentValue", childEntry2.Data["parentKey"])
76 |
77 | // Modify data stored in the child entry
78 | childEntry1.Data["childKey"] = "childValue"
79 |
80 | // Verify that data is successfully stored in the child it was set on
81 | val, exists := childEntry1.Data["childKey"]
82 | assert.True(exists)
83 | assert.Equal("childValue", val)
84 |
85 | // Verify that the data change to child 1 has not affected its sibling
86 | val, exists = childEntry2.Data["childKey"]
87 | assert.False(exists)
88 | assert.Empty(val)
89 |
90 | // Verify that the data change to child 1 has not affected its parent
91 | val, exists = parentEntry.Data["childKey"]
92 | assert.False(exists)
93 | assert.Empty(val)
94 | }
95 |
96 | func TestEntryWithTimeCopiesData(t *testing.T) {
97 | assert := assert.New(t)
98 |
99 | // Initialize a parent Entry object with a key/value set in its Data map
100 | logger := New()
101 | logger.Out = &bytes.Buffer{}
102 | parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
103 |
104 | // Create two children Entry objects from the parent with two different times
105 | childEntry1 := parentEntry.WithTime(time.Now().AddDate(0, 0, 1))
106 | childEntry2 := parentEntry.WithTime(time.Now().AddDate(0, 0, 2))
107 |
108 | // Ensure that data set in the parent Entry are preserved to both children
109 | assert.Equal("parentValue", childEntry1.Data["parentKey"])
110 | assert.Equal("parentValue", childEntry2.Data["parentKey"])
111 |
112 | // Modify data stored in the child entry
113 | childEntry1.Data["childKey"] = "childValue"
114 |
115 | // Verify that data is successfully stored in the child it was set on
116 | val, exists := childEntry1.Data["childKey"]
117 | assert.True(exists)
118 | assert.Equal("childValue", val)
119 |
120 | // Verify that the data change to child 1 has not affected its sibling
121 | val, exists = childEntry2.Data["childKey"]
122 | assert.False(exists)
123 | assert.Empty(val)
124 |
125 | // Verify that the data change to child 1 has not affected its parent
126 | val, exists = parentEntry.Data["childKey"]
127 | assert.False(exists)
128 | assert.Empty(val)
129 | }
130 |
131 | func TestEntryPanicln(t *testing.T) {
132 | errBoom := fmt.Errorf("boom time")
133 |
134 | defer func() {
135 | p := recover()
136 | assert.NotNil(t, p)
137 |
138 | switch pVal := p.(type) {
139 | case *Entry:
140 | assert.Equal(t, "kaboom", pVal.Message)
141 | assert.Equal(t, errBoom, pVal.Data["err"])
142 | default:
143 | t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
144 | }
145 | }()
146 |
147 | logger := New()
148 | logger.Out = &bytes.Buffer{}
149 | entry := NewEntry(logger)
150 | entry.WithField("err", errBoom).Panicln("kaboom")
151 | }
152 |
153 | func TestEntryPanicf(t *testing.T) {
154 | errBoom := fmt.Errorf("boom again")
155 |
156 | defer func() {
157 | p := recover()
158 | assert.NotNil(t, p)
159 |
160 | switch pVal := p.(type) {
161 | case *Entry:
162 | assert.Equal(t, "kaboom true", pVal.Message)
163 | assert.Equal(t, errBoom, pVal.Data["err"])
164 | default:
165 | t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
166 | }
167 | }()
168 |
169 | logger := New()
170 | logger.Out = &bytes.Buffer{}
171 | entry := NewEntry(logger)
172 | entry.WithField("err", errBoom).Panicf("kaboom %v", true)
173 | }
174 |
175 | func TestEntryPanic(t *testing.T) {
176 | errBoom := fmt.Errorf("boom again")
177 |
178 | defer func() {
179 | p := recover()
180 | assert.NotNil(t, p)
181 |
182 | switch pVal := p.(type) {
183 | case *Entry:
184 | assert.Equal(t, "kaboom", pVal.Message)
185 | assert.Equal(t, errBoom, pVal.Data["err"])
186 | default:
187 | t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
188 | }
189 | }()
190 |
191 | logger := New()
192 | logger.Out = &bytes.Buffer{}
193 | entry := NewEntry(logger)
194 | entry.WithField("err", errBoom).Panic("kaboom")
195 | }
196 |
197 | const (
198 | badMessage = "this is going to panic"
199 | panicMessage = "this is broken"
200 | )
201 |
202 | type panickyHook struct{}
203 |
204 | func (p *panickyHook) Levels() []Level {
205 | return []Level{InfoLevel}
206 | }
207 |
208 | func (p *panickyHook) Fire(entry *Entry) error {
209 | if entry.Message == badMessage {
210 | panic(panicMessage)
211 | }
212 |
213 | return nil
214 | }
215 |
216 | func TestEntryHooksPanic(t *testing.T) {
217 | logger := New()
218 | logger.Out = &bytes.Buffer{}
219 | logger.Level = InfoLevel
220 | logger.Hooks.Add(&panickyHook{})
221 |
222 | defer func() {
223 | p := recover()
224 | assert.NotNil(t, p)
225 | assert.Equal(t, panicMessage, p)
226 |
227 | entry := NewEntry(logger)
228 | entry.Info("another message")
229 | }()
230 |
231 | entry := NewEntry(logger)
232 | entry.Info(badMessage)
233 | }
234 |
235 | func TestEntryWithIncorrectField(t *testing.T) {
236 | assert := assert.New(t)
237 |
238 | fn := func() {}
239 |
240 | e := Entry{Logger: New()}
241 | eWithFunc := e.WithFields(Fields{"func": fn})
242 | eWithFuncPtr := e.WithFields(Fields{"funcPtr": &fn})
243 |
244 | assert.Equal(`can not add field "func"`, eWithFunc.err)
245 | assert.Equal(`can not add field "funcPtr"`, eWithFuncPtr.err)
246 |
247 | eWithFunc = eWithFunc.WithField("not_a_func", "it is a string")
248 | eWithFuncPtr = eWithFuncPtr.WithField("not_a_func", "it is a string")
249 |
250 | assert.Equal(`can not add field "func"`, eWithFunc.err)
251 | assert.Equal(`can not add field "funcPtr"`, eWithFuncPtr.err)
252 |
253 | eWithFunc = eWithFunc.WithTime(time.Now())
254 | eWithFuncPtr = eWithFuncPtr.WithTime(time.Now())
255 |
256 | assert.Equal(`can not add field "func"`, eWithFunc.err)
257 | assert.Equal(`can not add field "funcPtr"`, eWithFuncPtr.err)
258 | }
259 |
260 | func TestEntryLogfLevel(t *testing.T) {
261 | logger := New()
262 | buffer := &bytes.Buffer{}
263 | logger.Out = buffer
264 | logger.SetLevel(InfoLevel)
265 | entry := NewEntry(logger)
266 |
267 | entry.Logf(DebugLevel, "%s", "debug")
268 | assert.NotContains(t, buffer.String(), "debug")
269 |
270 | entry.Logf(WarnLevel, "%s", "warn")
271 | assert.Contains(t, buffer.String(), "warn")
272 | }
273 |
274 | func TestEntryReportCallerRace(t *testing.T) {
275 | logger := New()
276 | entry := NewEntry(logger)
277 |
278 | // logging before SetReportCaller has the highest chance of causing a race condition
279 | // to be detected, but doing it twice just to increase the likelihood of detecting the race
280 | go func() {
281 | entry.Info("should not race")
282 | }()
283 | go func() {
284 | logger.SetReportCaller(true)
285 | }()
286 | go func() {
287 | entry.Info("should not race")
288 | }()
289 | }
290 |
291 | func TestEntryFormatterRace(t *testing.T) {
292 | logger := New()
293 | entry := NewEntry(logger)
294 |
295 | // logging before SetReportCaller has the highest chance of causing a race condition
296 | // to be detected, but doing it twice just to increase the likelihood of detecting the race
297 | go func() {
298 | entry.Info("should not race")
299 | }()
300 | go func() {
301 | logger.SetFormatter(&TextFormatter{})
302 | }()
303 | go func() {
304 | entry.Info("should not race")
305 | }()
306 | }
307 |
--------------------------------------------------------------------------------
/json_formatter_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "runtime"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestErrorNotLost(t *testing.T) {
13 | formatter := &JSONFormatter{}
14 |
15 | b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
16 | if err != nil {
17 | t.Fatal("Unable to format entry: ", err)
18 | }
19 |
20 | entry := make(map[string]interface{})
21 | err = json.Unmarshal(b, &entry)
22 | if err != nil {
23 | t.Fatal("Unable to unmarshal formatted entry: ", err)
24 | }
25 |
26 | if entry["error"] != "wild walrus" {
27 | t.Fatal("Error field not set")
28 | }
29 | }
30 |
31 | func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
32 | formatter := &JSONFormatter{}
33 |
34 | b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
35 | if err != nil {
36 | t.Fatal("Unable to format entry: ", err)
37 | }
38 |
39 | entry := make(map[string]interface{})
40 | err = json.Unmarshal(b, &entry)
41 | if err != nil {
42 | t.Fatal("Unable to unmarshal formatted entry: ", err)
43 | }
44 |
45 | if entry["omg"] != "wild walrus" {
46 | t.Fatal("Error field not set")
47 | }
48 | }
49 |
50 | func TestFieldClashWithTime(t *testing.T) {
51 | formatter := &JSONFormatter{}
52 |
53 | b, err := formatter.Format(WithField("time", "right now!"))
54 | if err != nil {
55 | t.Fatal("Unable to format entry: ", err)
56 | }
57 |
58 | entry := make(map[string]interface{})
59 | err = json.Unmarshal(b, &entry)
60 | if err != nil {
61 | t.Fatal("Unable to unmarshal formatted entry: ", err)
62 | }
63 |
64 | if entry["fields.time"] != "right now!" {
65 | t.Fatal("fields.time not set to original time field")
66 | }
67 |
68 | if entry["time"] != "0001-01-01T00:00:00Z" {
69 | t.Fatal("time field not set to current time, was: ", entry["time"])
70 | }
71 | }
72 |
73 | func TestFieldClashWithMsg(t *testing.T) {
74 | formatter := &JSONFormatter{}
75 |
76 | b, err := formatter.Format(WithField("msg", "something"))
77 | if err != nil {
78 | t.Fatal("Unable to format entry: ", err)
79 | }
80 |
81 | entry := make(map[string]interface{})
82 | err = json.Unmarshal(b, &entry)
83 | if err != nil {
84 | t.Fatal("Unable to unmarshal formatted entry: ", err)
85 | }
86 |
87 | if entry["fields.msg"] != "something" {
88 | t.Fatal("fields.msg not set to original msg field")
89 | }
90 | }
91 |
92 | func TestFieldClashWithLevel(t *testing.T) {
93 | formatter := &JSONFormatter{}
94 |
95 | b, err := formatter.Format(WithField("level", "something"))
96 | if err != nil {
97 | t.Fatal("Unable to format entry: ", err)
98 | }
99 |
100 | entry := make(map[string]interface{})
101 | err = json.Unmarshal(b, &entry)
102 | if err != nil {
103 | t.Fatal("Unable to unmarshal formatted entry: ", err)
104 | }
105 |
106 | if entry["fields.level"] != "something" {
107 | t.Fatal("fields.level not set to original level field")
108 | }
109 | }
110 |
111 | func TestFieldClashWithRemappedFields(t *testing.T) {
112 | formatter := &JSONFormatter{
113 | FieldMap: FieldMap{
114 | FieldKeyTime: "@timestamp",
115 | FieldKeyLevel: "@level",
116 | FieldKeyMsg: "@message",
117 | },
118 | }
119 |
120 | b, err := formatter.Format(WithFields(Fields{
121 | "@timestamp": "@timestamp",
122 | "@level": "@level",
123 | "@message": "@message",
124 | "timestamp": "timestamp",
125 | "level": "level",
126 | "msg": "msg",
127 | }))
128 | if err != nil {
129 | t.Fatal("Unable to format entry: ", err)
130 | }
131 |
132 | entry := make(map[string]interface{})
133 | err = json.Unmarshal(b, &entry)
134 | if err != nil {
135 | t.Fatal("Unable to unmarshal formatted entry: ", err)
136 | }
137 |
138 | for _, field := range []string{"timestamp", "level", "msg"} {
139 | if entry[field] != field {
140 | t.Errorf("Expected field %v to be untouched; got %v", field, entry[field])
141 | }
142 |
143 | remappedKey := fmt.Sprintf("fields.%s", field)
144 | if remapped, ok := entry[remappedKey]; ok {
145 | t.Errorf("Expected %s to be empty; got %v", remappedKey, remapped)
146 | }
147 | }
148 |
149 | for _, field := range []string{"@timestamp", "@level", "@message"} {
150 | if entry[field] == field {
151 | t.Errorf("Expected field %v to be mapped to an Entry value", field)
152 | }
153 |
154 | remappedKey := fmt.Sprintf("fields.%s", field)
155 | if remapped, ok := entry[remappedKey]; ok {
156 | if remapped != field {
157 | t.Errorf("Expected field %v to be copied to %s; got %v", field, remappedKey, remapped)
158 | }
159 | } else {
160 | t.Errorf("Expected field %v to be copied to %s; was absent", field, remappedKey)
161 | }
162 | }
163 | }
164 |
165 | func TestFieldsInNestedDictionary(t *testing.T) {
166 | formatter := &JSONFormatter{
167 | DataKey: "args",
168 | }
169 |
170 | logEntry := WithFields(Fields{
171 | "level": "level",
172 | "test": "test",
173 | })
174 | logEntry.Level = InfoLevel
175 |
176 | b, err := formatter.Format(logEntry)
177 | if err != nil {
178 | t.Fatal("Unable to format entry: ", err)
179 | }
180 |
181 | entry := make(map[string]interface{})
182 | err = json.Unmarshal(b, &entry)
183 | if err != nil {
184 | t.Fatal("Unable to unmarshal formatted entry: ", err)
185 | }
186 |
187 | args := entry["args"].(map[string]interface{})
188 |
189 | for _, field := range []string{"test", "level"} {
190 | if value, present := args[field]; !present || value != field {
191 | t.Errorf("Expected field %v to be present under 'args'; untouched", field)
192 | }
193 | }
194 |
195 | for _, field := range []string{"test", "fields.level"} {
196 | if _, present := entry[field]; present {
197 | t.Errorf("Expected field %v not to be present at top level", field)
198 | }
199 | }
200 |
201 | // with nested object, "level" shouldn't clash
202 | if entry["level"] != "info" {
203 | t.Errorf("Expected 'level' field to contain 'info'")
204 | }
205 | }
206 |
207 | func TestJSONEntryEndsWithNewline(t *testing.T) {
208 | formatter := &JSONFormatter{}
209 |
210 | b, err := formatter.Format(WithField("level", "something"))
211 | if err != nil {
212 | t.Fatal("Unable to format entry: ", err)
213 | }
214 |
215 | if b[len(b)-1] != '\n' {
216 | t.Fatal("Expected JSON log entry to end with a newline")
217 | }
218 | }
219 |
220 | func TestJSONMessageKey(t *testing.T) {
221 | formatter := &JSONFormatter{
222 | FieldMap: FieldMap{
223 | FieldKeyMsg: "message",
224 | },
225 | }
226 |
227 | b, err := formatter.Format(&Entry{Message: "oh hai"})
228 | if err != nil {
229 | t.Fatal("Unable to format entry: ", err)
230 | }
231 | s := string(b)
232 | if !(strings.Contains(s, "message") && strings.Contains(s, "oh hai")) {
233 | t.Fatal("Expected JSON to format message key")
234 | }
235 | }
236 |
237 | func TestJSONLevelKey(t *testing.T) {
238 | formatter := &JSONFormatter{
239 | FieldMap: FieldMap{
240 | FieldKeyLevel: "somelevel",
241 | },
242 | }
243 |
244 | b, err := formatter.Format(WithField("level", "something"))
245 | if err != nil {
246 | t.Fatal("Unable to format entry: ", err)
247 | }
248 | s := string(b)
249 | if !strings.Contains(s, "somelevel") {
250 | t.Fatal("Expected JSON to format level key")
251 | }
252 | }
253 |
254 | func TestJSONTimeKey(t *testing.T) {
255 | formatter := &JSONFormatter{
256 | FieldMap: FieldMap{
257 | FieldKeyTime: "timeywimey",
258 | },
259 | }
260 |
261 | b, err := formatter.Format(WithField("level", "something"))
262 | if err != nil {
263 | t.Fatal("Unable to format entry: ", err)
264 | }
265 | s := string(b)
266 | if !strings.Contains(s, "timeywimey") {
267 | t.Fatal("Expected JSON to format time key")
268 | }
269 | }
270 |
271 | func TestFieldDoesNotClashWithCaller(t *testing.T) {
272 | SetReportCaller(false)
273 | formatter := &JSONFormatter{}
274 |
275 | b, err := formatter.Format(WithField("func", "howdy pardner"))
276 | if err != nil {
277 | t.Fatal("Unable to format entry: ", err)
278 | }
279 |
280 | entry := make(map[string]interface{})
281 | err = json.Unmarshal(b, &entry)
282 | if err != nil {
283 | t.Fatal("Unable to unmarshal formatted entry: ", err)
284 | }
285 |
286 | if entry["func"] != "howdy pardner" {
287 | t.Fatal("func field replaced when ReportCaller=false")
288 | }
289 | }
290 |
291 | func TestFieldClashWithCaller(t *testing.T) {
292 | SetReportCaller(true)
293 | formatter := &JSONFormatter{}
294 | e := WithField("func", "howdy pardner")
295 | e.Caller = &runtime.Frame{Function: "somefunc"}
296 | b, err := formatter.Format(e)
297 | if err != nil {
298 | t.Fatal("Unable to format entry: ", err)
299 | }
300 |
301 | entry := make(map[string]interface{})
302 | err = json.Unmarshal(b, &entry)
303 | if err != nil {
304 | t.Fatal("Unable to unmarshal formatted entry: ", err)
305 | }
306 |
307 | if entry["fields.func"] != "howdy pardner" {
308 | t.Fatalf("fields.func not set to original func field when ReportCaller=true (got '%s')",
309 | entry["fields.func"])
310 | }
311 |
312 | if entry["func"] != "somefunc" {
313 | t.Fatalf("func not set as expected when ReportCaller=true (got '%s')",
314 | entry["func"])
315 | }
316 |
317 | SetReportCaller(false) // return to default value
318 | }
319 |
320 | func TestJSONDisableTimestamp(t *testing.T) {
321 | formatter := &JSONFormatter{
322 | DisableTimestamp: true,
323 | }
324 |
325 | b, err := formatter.Format(WithField("level", "something"))
326 | if err != nil {
327 | t.Fatal("Unable to format entry: ", err)
328 | }
329 | s := string(b)
330 | if strings.Contains(s, FieldKeyTime) {
331 | t.Error("Did not prevent timestamp", s)
332 | }
333 | }
334 |
335 | func TestJSONEnableTimestamp(t *testing.T) {
336 | formatter := &JSONFormatter{}
337 |
338 | b, err := formatter.Format(WithField("level", "something"))
339 | if err != nil {
340 | t.Fatal("Unable to format entry: ", err)
341 | }
342 | s := string(b)
343 | if !strings.Contains(s, FieldKeyTime) {
344 | t.Error("Timestamp not present", s)
345 | }
346 | }
347 |
348 | func TestJSONDisableHTMLEscape(t *testing.T) {
349 | formatter := &JSONFormatter{DisableHTMLEscape: true}
350 |
351 | b, err := formatter.Format(&Entry{Message: "& < >"})
352 | if err != nil {
353 | t.Fatal("Unable to format entry: ", err)
354 | }
355 | s := string(b)
356 | if !strings.Contains(s, "& < >") {
357 | t.Error("Message should not be HTML escaped", s)
358 | }
359 | }
360 |
361 | func TestJSONEnableHTMLEscape(t *testing.T) {
362 | formatter := &JSONFormatter{}
363 |
364 | b, err := formatter.Format(&Entry{Message: "& < >"})
365 | if err != nil {
366 | t.Fatal("Unable to format entry: ", err)
367 | }
368 | s := string(b)
369 | if !(strings.Contains(s, "u0026") && strings.Contains(s, "u003e") && strings.Contains(s, "u003c")) {
370 | t.Error("Message should be HTML escaped", s)
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/text_formatter.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "runtime"
8 | "sort"
9 | "strconv"
10 | "strings"
11 | "sync"
12 | "time"
13 | "unicode/utf8"
14 | )
15 |
16 | const (
17 | red = 31
18 | yellow = 33
19 | blue = 36
20 | gray = 37
21 | )
22 |
23 | var baseTimestamp time.Time
24 |
25 | func init() {
26 | baseTimestamp = time.Now()
27 | }
28 |
29 | // TextFormatter formats logs into text
30 | type TextFormatter struct {
31 | // Set to true to bypass checking for a TTY before outputting colors.
32 | ForceColors bool
33 |
34 | // Force disabling colors.
35 | DisableColors bool
36 |
37 | // Force quoting of all values
38 | ForceQuote bool
39 |
40 | // DisableQuote disables quoting for all values.
41 | // DisableQuote will have a lower priority than ForceQuote.
42 | // If both of them are set to true, quote will be forced on all values.
43 | DisableQuote bool
44 |
45 | // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
46 | EnvironmentOverrideColors bool
47 |
48 | // Disable timestamp logging. useful when output is redirected to logging
49 | // system that already adds timestamps.
50 | DisableTimestamp bool
51 |
52 | // Enable logging the full timestamp when a TTY is attached instead of just
53 | // the time passed since beginning of execution.
54 | FullTimestamp bool
55 |
56 | // TimestampFormat to use for display when a full timestamp is printed.
57 | // The format to use is the same than for time.Format or time.Parse from the standard
58 | // library.
59 | // The standard Library already provides a set of predefined format.
60 | TimestampFormat string
61 |
62 | // The fields are sorted by default for a consistent output. For applications
63 | // that log extremely frequently and don't use the JSON formatter this may not
64 | // be desired.
65 | DisableSorting bool
66 |
67 | // The keys sorting function, when uninitialized it uses sort.Strings.
68 | SortingFunc func([]string)
69 |
70 | // Disables the truncation of the level text to 4 characters.
71 | DisableLevelTruncation bool
72 |
73 | // PadLevelText Adds padding the level text so that all the levels output at the same length
74 | // PadLevelText is a superset of the DisableLevelTruncation option
75 | PadLevelText bool
76 |
77 | // QuoteEmptyFields will wrap empty fields in quotes if true
78 | QuoteEmptyFields bool
79 |
80 | // Whether the logger's out is to a terminal
81 | isTerminal bool
82 |
83 | // FieldMap allows users to customize the names of keys for default fields.
84 | // As an example:
85 | // formatter := &TextFormatter{
86 | // FieldMap: FieldMap{
87 | // FieldKeyTime: "@timestamp",
88 | // FieldKeyLevel: "@level",
89 | // FieldKeyMsg: "@message"}}
90 | FieldMap FieldMap
91 |
92 | // CallerPrettyfier can be set by the user to modify the content
93 | // of the function and file keys in the data when ReportCaller is
94 | // activated. If any of the returned value is the empty string the
95 | // corresponding key will be removed from fields.
96 | CallerPrettyfier func(*runtime.Frame) (function string, file string)
97 |
98 | terminalInitOnce sync.Once
99 |
100 | // The max length of the level text, generated dynamically on init
101 | levelTextMaxLength int
102 | }
103 |
104 | func (f *TextFormatter) init(entry *Entry) {
105 | if entry.Logger != nil {
106 | f.isTerminal = checkIfTerminal(entry.Logger.Out)
107 | }
108 | // Get the max length of the level text
109 | for _, level := range AllLevels {
110 | levelTextLength := utf8.RuneCount([]byte(level.String()))
111 | if levelTextLength > f.levelTextMaxLength {
112 | f.levelTextMaxLength = levelTextLength
113 | }
114 | }
115 | }
116 |
117 | func (f *TextFormatter) isColored() bool {
118 | isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
119 |
120 | if f.EnvironmentOverrideColors {
121 | switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
122 | case ok && force != "0":
123 | isColored = true
124 | case ok && force == "0", os.Getenv("CLICOLOR") == "0":
125 | isColored = false
126 | }
127 | }
128 |
129 | return isColored && !f.DisableColors
130 | }
131 |
132 | // Format renders a single log entry
133 | func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
134 | data := make(Fields)
135 | for k, v := range entry.Data {
136 | data[k] = v
137 | }
138 | prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
139 | keys := make([]string, 0, len(data))
140 | for k := range data {
141 | keys = append(keys, k)
142 | }
143 |
144 | var funcVal, fileVal string
145 |
146 | fixedKeys := make([]string, 0, 4+len(data))
147 | if !f.DisableTimestamp {
148 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
149 | }
150 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
151 | if entry.Message != "" {
152 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
153 | }
154 | if entry.err != "" {
155 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
156 | }
157 | if entry.HasCaller() {
158 | if f.CallerPrettyfier != nil {
159 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
160 | } else {
161 | funcVal = entry.Caller.Function
162 | fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
163 | }
164 |
165 | if funcVal != "" {
166 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
167 | }
168 | if fileVal != "" {
169 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
170 | }
171 | }
172 |
173 | if !f.DisableSorting {
174 | if f.SortingFunc == nil {
175 | sort.Strings(keys)
176 | fixedKeys = append(fixedKeys, keys...)
177 | } else {
178 | if !f.isColored() {
179 | fixedKeys = append(fixedKeys, keys...)
180 | f.SortingFunc(fixedKeys)
181 | } else {
182 | f.SortingFunc(keys)
183 | }
184 | }
185 | } else {
186 | fixedKeys = append(fixedKeys, keys...)
187 | }
188 |
189 | var b *bytes.Buffer
190 | if entry.Buffer != nil {
191 | b = entry.Buffer
192 | } else {
193 | b = &bytes.Buffer{}
194 | }
195 |
196 | f.terminalInitOnce.Do(func() { f.init(entry) })
197 |
198 | timestampFormat := f.TimestampFormat
199 | if timestampFormat == "" {
200 | timestampFormat = defaultTimestampFormat
201 | }
202 | if f.isColored() {
203 | f.printColored(b, entry, keys, data, timestampFormat)
204 | } else {
205 |
206 | for _, key := range fixedKeys {
207 | var value interface{}
208 | switch {
209 | case key == f.FieldMap.resolve(FieldKeyTime):
210 | value = entry.Time.Format(timestampFormat)
211 | case key == f.FieldMap.resolve(FieldKeyLevel):
212 | value = entry.Level.String()
213 | case key == f.FieldMap.resolve(FieldKeyMsg):
214 | value = entry.Message
215 | case key == f.FieldMap.resolve(FieldKeyLogrusError):
216 | value = entry.err
217 | case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
218 | value = funcVal
219 | case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
220 | value = fileVal
221 | default:
222 | value = data[key]
223 | }
224 | f.appendKeyValue(b, key, value)
225 | }
226 | }
227 |
228 | b.WriteByte('\n')
229 | return b.Bytes(), nil
230 | }
231 |
232 | func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
233 | var levelColor int
234 | switch entry.Level {
235 | case DebugLevel, TraceLevel:
236 | levelColor = gray
237 | case WarnLevel:
238 | levelColor = yellow
239 | case ErrorLevel, FatalLevel, PanicLevel:
240 | levelColor = red
241 | case InfoLevel:
242 | levelColor = blue
243 | default:
244 | levelColor = blue
245 | }
246 |
247 | levelText := strings.ToUpper(entry.Level.String())
248 | if !f.DisableLevelTruncation && !f.PadLevelText {
249 | levelText = levelText[0:4]
250 | }
251 | if f.PadLevelText {
252 | // Generates the format string used in the next line, for example "%-6s" or "%-7s".
253 | // Based on the max level text length.
254 | formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
255 | // Formats the level text by appending spaces up to the max length, for example:
256 | // - "INFO "
257 | // - "WARNING"
258 | levelText = fmt.Sprintf(formatString, levelText)
259 | }
260 |
261 | // Remove a single newline if it already exists in the message to keep
262 | // the behavior of logrus text_formatter the same as the stdlib log package
263 | entry.Message = strings.TrimSuffix(entry.Message, "\n")
264 |
265 | caller := ""
266 | if entry.HasCaller() {
267 | funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
268 | fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
269 |
270 | if f.CallerPrettyfier != nil {
271 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
272 | }
273 |
274 | if fileVal == "" {
275 | caller = funcVal
276 | } else if funcVal == "" {
277 | caller = fileVal
278 | } else {
279 | caller = fileVal + " " + funcVal
280 | }
281 | }
282 |
283 | switch {
284 | case f.DisableTimestamp:
285 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
286 | case !f.FullTimestamp:
287 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
288 | default:
289 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
290 | }
291 | for _, k := range keys {
292 | v := data[k]
293 | fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
294 | f.appendValue(b, v)
295 | }
296 | }
297 |
298 | func (f *TextFormatter) needsQuoting(text string) bool {
299 | if f.ForceQuote {
300 | return true
301 | }
302 | if f.QuoteEmptyFields && len(text) == 0 {
303 | return true
304 | }
305 | if f.DisableQuote {
306 | return false
307 | }
308 | for _, ch := range text {
309 | //nolint:staticcheck // QF1001: could apply De Morgan's law
310 | if !((ch >= 'a' && ch <= 'z') ||
311 | (ch >= 'A' && ch <= 'Z') ||
312 | (ch >= '0' && ch <= '9') ||
313 | ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
314 | return true
315 | }
316 | }
317 | return false
318 | }
319 |
320 | func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
321 | if b.Len() > 0 {
322 | b.WriteByte(' ')
323 | }
324 | b.WriteString(key)
325 | b.WriteByte('=')
326 | f.appendValue(b, value)
327 | }
328 |
329 | func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
330 | stringVal, ok := value.(string)
331 | if !ok {
332 | stringVal = fmt.Sprint(value)
333 | }
334 |
335 | if !f.needsQuoting(stringVal) {
336 | b.WriteString(stringVal)
337 | } else {
338 | fmt.Fprintf(b, "%q", stringVal)
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "context"
5 | "io"
6 | "os"
7 | "sync"
8 | "sync/atomic"
9 | "time"
10 | )
11 |
12 | // LogFunction For big messages, it can be more efficient to pass a function
13 | // and only call it if the log level is actually enables rather than
14 | // generating the log message and then checking if the level is enabled
15 | type LogFunction func() []interface{}
16 |
17 | type Logger struct {
18 | // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
19 | // file, or leave it default which is `os.Stderr`. You can also set this to
20 | // something more adventurous, such as logging to Kafka.
21 | Out io.Writer
22 | // Hooks for the logger instance. These allow firing events based on logging
23 | // levels and log entries. For example, to send errors to an error tracking
24 | // service, log to StatsD or dump the core on fatal errors.
25 | Hooks LevelHooks
26 | // All log entries pass through the formatter before logged to Out. The
27 | // included formatters are `TextFormatter` and `JSONFormatter` for which
28 | // TextFormatter is the default. In development (when a TTY is attached) it
29 | // logs with colors, but to a file it wouldn't. You can easily implement your
30 | // own that implements the `Formatter` interface, see the `README` or included
31 | // formatters for examples.
32 | Formatter Formatter
33 |
34 | // Flag for whether to log caller info (off by default)
35 | ReportCaller bool
36 |
37 | // The logging level the logger should log at. This is typically (and defaults
38 | // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
39 | // logged.
40 | Level Level
41 | // Used to sync writing to the log. Locking is enabled by Default
42 | mu MutexWrap
43 | // Reusable empty entry
44 | entryPool sync.Pool
45 | // Function to exit the application, defaults to `os.Exit()`
46 | ExitFunc exitFunc
47 | // The buffer pool used to format the log. If it is nil, the default global
48 | // buffer pool will be used.
49 | BufferPool BufferPool
50 | }
51 |
52 | type exitFunc func(int)
53 |
54 | type MutexWrap struct {
55 | lock sync.Mutex
56 | disabled bool
57 | }
58 |
59 | func (mw *MutexWrap) Lock() {
60 | if !mw.disabled {
61 | mw.lock.Lock()
62 | }
63 | }
64 |
65 | func (mw *MutexWrap) Unlock() {
66 | if !mw.disabled {
67 | mw.lock.Unlock()
68 | }
69 | }
70 |
71 | func (mw *MutexWrap) Disable() {
72 | mw.disabled = true
73 | }
74 |
75 | // New Creates a new logger. Configuration should be set by changing [Formatter],
76 | // Out and Hooks directly on the default Logger instance. You can also just
77 | // instantiate your own:
78 | //
79 | // var log = &logrus.Logger{
80 | // Out: os.Stderr,
81 | // Formatter: new(logrus.TextFormatter),
82 | // Hooks: make(logrus.LevelHooks),
83 | // Level: logrus.DebugLevel,
84 | // }
85 | //
86 | // It's recommended to make this a global instance called `log`.
87 | func New() *Logger {
88 | return &Logger{
89 | Out: os.Stderr,
90 | Formatter: new(TextFormatter),
91 | Hooks: make(LevelHooks),
92 | Level: InfoLevel,
93 | ExitFunc: os.Exit,
94 | ReportCaller: false,
95 | }
96 | }
97 |
98 | func (logger *Logger) newEntry() *Entry {
99 | entry, ok := logger.entryPool.Get().(*Entry)
100 | if ok {
101 | return entry
102 | }
103 | return NewEntry(logger)
104 | }
105 |
106 | func (logger *Logger) releaseEntry(entry *Entry) {
107 | entry.Data = map[string]interface{}{}
108 | logger.entryPool.Put(entry)
109 | }
110 |
111 | // WithField allocates a new entry and adds a field to it.
112 | // Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
113 | // this new returned entry.
114 | // If you want multiple fields, use `WithFields`.
115 | func (logger *Logger) WithField(key string, value interface{}) *Entry {
116 | entry := logger.newEntry()
117 | defer logger.releaseEntry(entry)
118 | return entry.WithField(key, value)
119 | }
120 |
121 | // WithFields adds a struct of fields to the log entry. It calls [Entry.WithField]
122 | // for each Field.
123 | func (logger *Logger) WithFields(fields Fields) *Entry {
124 | entry := logger.newEntry()
125 | defer logger.releaseEntry(entry)
126 | return entry.WithFields(fields)
127 | }
128 |
129 | // WithError adds an error as single field to the log entry. It calls
130 | // [Entry.WithError] for the given error.
131 | func (logger *Logger) WithError(err error) *Entry {
132 | entry := logger.newEntry()
133 | defer logger.releaseEntry(entry)
134 | return entry.WithError(err)
135 | }
136 |
137 | // WithContext add a context to the log entry.
138 | func (logger *Logger) WithContext(ctx context.Context) *Entry {
139 | entry := logger.newEntry()
140 | defer logger.releaseEntry(entry)
141 | return entry.WithContext(ctx)
142 | }
143 |
144 | // WithTime overrides the time of the log entry.
145 | func (logger *Logger) WithTime(t time.Time) *Entry {
146 | entry := logger.newEntry()
147 | defer logger.releaseEntry(entry)
148 | return entry.WithTime(t)
149 | }
150 |
151 | func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
152 | if logger.IsLevelEnabled(level) {
153 | entry := logger.newEntry()
154 | entry.Logf(level, format, args...)
155 | logger.releaseEntry(entry)
156 | }
157 | }
158 |
159 | func (logger *Logger) Tracef(format string, args ...interface{}) {
160 | logger.Logf(TraceLevel, format, args...)
161 | }
162 |
163 | func (logger *Logger) Debugf(format string, args ...interface{}) {
164 | logger.Logf(DebugLevel, format, args...)
165 | }
166 |
167 | func (logger *Logger) Infof(format string, args ...interface{}) {
168 | logger.Logf(InfoLevel, format, args...)
169 | }
170 |
171 | func (logger *Logger) Printf(format string, args ...interface{}) {
172 | entry := logger.newEntry()
173 | entry.Printf(format, args...)
174 | logger.releaseEntry(entry)
175 | }
176 |
177 | func (logger *Logger) Warnf(format string, args ...interface{}) {
178 | logger.Logf(WarnLevel, format, args...)
179 | }
180 |
181 | func (logger *Logger) Warningf(format string, args ...interface{}) {
182 | logger.Warnf(format, args...)
183 | }
184 |
185 | func (logger *Logger) Errorf(format string, args ...interface{}) {
186 | logger.Logf(ErrorLevel, format, args...)
187 | }
188 |
189 | func (logger *Logger) Fatalf(format string, args ...interface{}) {
190 | logger.Logf(FatalLevel, format, args...)
191 | logger.Exit(1)
192 | }
193 |
194 | func (logger *Logger) Panicf(format string, args ...interface{}) {
195 | logger.Logf(PanicLevel, format, args...)
196 | }
197 |
198 | // Log will log a message at the level given as parameter.
199 | // Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
200 | // For this behaviour Logger.Panic or Logger.Fatal should be used instead.
201 | func (logger *Logger) Log(level Level, args ...interface{}) {
202 | if logger.IsLevelEnabled(level) {
203 | entry := logger.newEntry()
204 | entry.Log(level, args...)
205 | logger.releaseEntry(entry)
206 | }
207 | }
208 |
209 | func (logger *Logger) LogFn(level Level, fn LogFunction) {
210 | if logger.IsLevelEnabled(level) {
211 | entry := logger.newEntry()
212 | entry.Log(level, fn()...)
213 | logger.releaseEntry(entry)
214 | }
215 | }
216 |
217 | func (logger *Logger) Trace(args ...interface{}) {
218 | logger.Log(TraceLevel, args...)
219 | }
220 |
221 | func (logger *Logger) Debug(args ...interface{}) {
222 | logger.Log(DebugLevel, args...)
223 | }
224 |
225 | func (logger *Logger) Info(args ...interface{}) {
226 | logger.Log(InfoLevel, args...)
227 | }
228 |
229 | func (logger *Logger) Print(args ...interface{}) {
230 | entry := logger.newEntry()
231 | entry.Print(args...)
232 | logger.releaseEntry(entry)
233 | }
234 |
235 | func (logger *Logger) Warn(args ...interface{}) {
236 | logger.Log(WarnLevel, args...)
237 | }
238 |
239 | func (logger *Logger) Warning(args ...interface{}) {
240 | logger.Warn(args...)
241 | }
242 |
243 | func (logger *Logger) Error(args ...interface{}) {
244 | logger.Log(ErrorLevel, args...)
245 | }
246 |
247 | func (logger *Logger) Fatal(args ...interface{}) {
248 | logger.Log(FatalLevel, args...)
249 | logger.Exit(1)
250 | }
251 |
252 | func (logger *Logger) Panic(args ...interface{}) {
253 | logger.Log(PanicLevel, args...)
254 | }
255 |
256 | func (logger *Logger) TraceFn(fn LogFunction) {
257 | logger.LogFn(TraceLevel, fn)
258 | }
259 |
260 | func (logger *Logger) DebugFn(fn LogFunction) {
261 | logger.LogFn(DebugLevel, fn)
262 | }
263 |
264 | func (logger *Logger) InfoFn(fn LogFunction) {
265 | logger.LogFn(InfoLevel, fn)
266 | }
267 |
268 | func (logger *Logger) PrintFn(fn LogFunction) {
269 | entry := logger.newEntry()
270 | entry.Print(fn()...)
271 | logger.releaseEntry(entry)
272 | }
273 |
274 | func (logger *Logger) WarnFn(fn LogFunction) {
275 | logger.LogFn(WarnLevel, fn)
276 | }
277 |
278 | func (logger *Logger) WarningFn(fn LogFunction) {
279 | logger.WarnFn(fn)
280 | }
281 |
282 | func (logger *Logger) ErrorFn(fn LogFunction) {
283 | logger.LogFn(ErrorLevel, fn)
284 | }
285 |
286 | func (logger *Logger) FatalFn(fn LogFunction) {
287 | logger.LogFn(FatalLevel, fn)
288 | logger.Exit(1)
289 | }
290 |
291 | func (logger *Logger) PanicFn(fn LogFunction) {
292 | logger.LogFn(PanicLevel, fn)
293 | }
294 |
295 | func (logger *Logger) Logln(level Level, args ...interface{}) {
296 | if logger.IsLevelEnabled(level) {
297 | entry := logger.newEntry()
298 | entry.Logln(level, args...)
299 | logger.releaseEntry(entry)
300 | }
301 | }
302 |
303 | func (logger *Logger) Traceln(args ...interface{}) {
304 | logger.Logln(TraceLevel, args...)
305 | }
306 |
307 | func (logger *Logger) Debugln(args ...interface{}) {
308 | logger.Logln(DebugLevel, args...)
309 | }
310 |
311 | func (logger *Logger) Infoln(args ...interface{}) {
312 | logger.Logln(InfoLevel, args...)
313 | }
314 |
315 | func (logger *Logger) Println(args ...interface{}) {
316 | entry := logger.newEntry()
317 | entry.Println(args...)
318 | logger.releaseEntry(entry)
319 | }
320 |
321 | func (logger *Logger) Warnln(args ...interface{}) {
322 | logger.Logln(WarnLevel, args...)
323 | }
324 |
325 | func (logger *Logger) Warningln(args ...interface{}) {
326 | logger.Warnln(args...)
327 | }
328 |
329 | func (logger *Logger) Errorln(args ...interface{}) {
330 | logger.Logln(ErrorLevel, args...)
331 | }
332 |
333 | func (logger *Logger) Fatalln(args ...interface{}) {
334 | logger.Logln(FatalLevel, args...)
335 | logger.Exit(1)
336 | }
337 |
338 | func (logger *Logger) Panicln(args ...interface{}) {
339 | logger.Logln(PanicLevel, args...)
340 | }
341 |
342 | func (logger *Logger) Exit(code int) {
343 | runHandlers()
344 | if logger.ExitFunc == nil {
345 | logger.ExitFunc = os.Exit
346 | }
347 | logger.ExitFunc(code)
348 | }
349 |
350 | // SetNoLock disables the lock for situations where a file is opened with
351 | // appending mode, and safe for concurrent writes to the file (within 4k
352 | // message on Linux). In these cases user can choose to disable the lock.
353 | func (logger *Logger) SetNoLock() {
354 | logger.mu.Disable()
355 | }
356 |
357 | func (logger *Logger) level() Level {
358 | return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
359 | }
360 |
361 | // SetLevel sets the logger level.
362 | func (logger *Logger) SetLevel(level Level) {
363 | atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
364 | }
365 |
366 | // GetLevel returns the logger level.
367 | func (logger *Logger) GetLevel() Level {
368 | return logger.level()
369 | }
370 |
371 | // AddHook adds a hook to the logger hooks.
372 | func (logger *Logger) AddHook(hook Hook) {
373 | logger.mu.Lock()
374 | defer logger.mu.Unlock()
375 | logger.Hooks.Add(hook)
376 | }
377 |
378 | // IsLevelEnabled checks if the log level of the logger is greater than the level param
379 | func (logger *Logger) IsLevelEnabled(level Level) bool {
380 | return logger.level() >= level
381 | }
382 |
383 | // SetFormatter sets the logger formatter.
384 | func (logger *Logger) SetFormatter(formatter Formatter) {
385 | logger.mu.Lock()
386 | defer logger.mu.Unlock()
387 | logger.Formatter = formatter
388 | }
389 |
390 | // SetOutput sets the logger output.
391 | func (logger *Logger) SetOutput(output io.Writer) {
392 | logger.mu.Lock()
393 | defer logger.mu.Unlock()
394 | logger.Out = output
395 | }
396 |
397 | func (logger *Logger) SetReportCaller(reportCaller bool) {
398 | logger.mu.Lock()
399 | defer logger.mu.Unlock()
400 | logger.ReportCaller = reportCaller
401 | }
402 |
403 | // ReplaceHooks replaces the logger hooks and returns the old ones
404 | func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
405 | logger.mu.Lock()
406 | oldHooks := logger.Hooks
407 | logger.Hooks = hooks
408 | logger.mu.Unlock()
409 | return oldHooks
410 | }
411 |
412 | // SetBufferPool sets the logger buffer pool.
413 | func (logger *Logger) SetBufferPool(pool BufferPool) {
414 | logger.mu.Lock()
415 | defer logger.mu.Unlock()
416 | logger.BufferPool = pool
417 | }
418 |
--------------------------------------------------------------------------------
/entry.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "os"
8 | "reflect"
9 | "runtime"
10 | "strings"
11 | "sync"
12 | "time"
13 | )
14 |
15 | var (
16 |
17 | // qualified package name, cached at first use
18 | logrusPackage string
19 |
20 | // Positions in the call stack when tracing to report the calling method
21 | minimumCallerDepth int
22 |
23 | // Used for caller information initialisation
24 | callerInitOnce sync.Once
25 | )
26 |
27 | const (
28 | maximumCallerDepth int = 25
29 | knownLogrusFrames int = 4
30 | )
31 |
32 | func init() {
33 | // start at the bottom of the stack before the package-name cache is primed
34 | minimumCallerDepth = 1
35 | }
36 |
37 | // ErrorKey defines the key when adding errors using [WithError], [Logger.WithError].
38 | var ErrorKey = "error"
39 |
40 | // Entry is the final or intermediate Logrus logging entry. It contains all
41 | // the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
42 | // Info, Warn, Error, Fatal or Panic is called on it. These objects can be
43 | // reused and passed around as much as you wish to avoid field duplication.
44 | //
45 | //nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver.
46 | type Entry struct {
47 | Logger *Logger
48 |
49 | // Contains all the fields set by the user.
50 | Data Fields
51 |
52 | // Time at which the log entry was created
53 | Time time.Time
54 |
55 | // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
56 | // This field will be set on entry firing and the value will be equal to the one in Logger struct field.
57 | Level Level
58 |
59 | // Calling method, with package name
60 | Caller *runtime.Frame
61 |
62 | // Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
63 | Message string
64 |
65 | // When formatter is called in entry.log(), a Buffer may be set to entry
66 | Buffer *bytes.Buffer
67 |
68 | // Contains the context set by the user. Useful for hook processing etc.
69 | Context context.Context
70 |
71 | // err may contain a field formatting error
72 | err string
73 | }
74 |
75 | func NewEntry(logger *Logger) *Entry {
76 | return &Entry{
77 | Logger: logger,
78 | // Default is three fields, plus one optional. Give a little extra room.
79 | Data: make(Fields, 6),
80 | }
81 | }
82 |
83 | func (entry *Entry) Dup() *Entry {
84 | data := make(Fields, len(entry.Data))
85 | for k, v := range entry.Data {
86 | data[k] = v
87 | }
88 | return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
89 | }
90 |
91 | // Bytes returns the bytes representation of this entry from the formatter.
92 | func (entry *Entry) Bytes() ([]byte, error) {
93 | return entry.Logger.Formatter.Format(entry)
94 | }
95 |
96 | // String returns the string representation from the reader and ultimately the
97 | // formatter.
98 | func (entry *Entry) String() (string, error) {
99 | serialized, err := entry.Bytes()
100 | if err != nil {
101 | return "", err
102 | }
103 | str := string(serialized)
104 | return str, nil
105 | }
106 |
107 | // WithError adds an error as single field (using the key defined in [ErrorKey])
108 | // to the Entry.
109 | func (entry *Entry) WithError(err error) *Entry {
110 | return entry.WithField(ErrorKey, err)
111 | }
112 |
113 | // WithContext adds a context to the Entry.
114 | func (entry *Entry) WithContext(ctx context.Context) *Entry {
115 | dataCopy := make(Fields, len(entry.Data))
116 | for k, v := range entry.Data {
117 | dataCopy[k] = v
118 | }
119 | return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
120 | }
121 |
122 | // WithField adds a single field to the Entry.
123 | func (entry *Entry) WithField(key string, value interface{}) *Entry {
124 | return entry.WithFields(Fields{key: value})
125 | }
126 |
127 | // WithFields adds a map of fields to the Entry.
128 | func (entry *Entry) WithFields(fields Fields) *Entry {
129 | data := make(Fields, len(entry.Data)+len(fields))
130 | for k, v := range entry.Data {
131 | data[k] = v
132 | }
133 | fieldErr := entry.err
134 | for k, v := range fields {
135 | isErrField := false
136 | if t := reflect.TypeOf(v); t != nil {
137 | switch {
138 | case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
139 | isErrField = true
140 | }
141 | }
142 | if isErrField {
143 | tmp := fmt.Sprintf("can not add field %q", k)
144 | if fieldErr != "" {
145 | fieldErr = entry.err + ", " + tmp
146 | } else {
147 | fieldErr = tmp
148 | }
149 | } else {
150 | data[k] = v
151 | }
152 | }
153 | return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
154 | }
155 |
156 | // WithTime overrides the time of the Entry.
157 | func (entry *Entry) WithTime(t time.Time) *Entry {
158 | dataCopy := make(Fields, len(entry.Data))
159 | for k, v := range entry.Data {
160 | dataCopy[k] = v
161 | }
162 | return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
163 | }
164 |
165 | // getPackageName reduces a fully qualified function name to the package name
166 | // There really ought to be to be a better way...
167 | func getPackageName(f string) string {
168 | for {
169 | lastPeriod := strings.LastIndex(f, ".")
170 | lastSlash := strings.LastIndex(f, "/")
171 | if lastPeriod > lastSlash {
172 | f = f[:lastPeriod]
173 | } else {
174 | break
175 | }
176 | }
177 |
178 | return f
179 | }
180 |
181 | // getCaller retrieves the name of the first non-logrus calling function
182 | func getCaller() *runtime.Frame {
183 | // cache this package's fully-qualified name
184 | callerInitOnce.Do(func() {
185 | pcs := make([]uintptr, maximumCallerDepth)
186 | _ = runtime.Callers(0, pcs)
187 |
188 | // dynamic get the package name and the minimum caller depth
189 | for i := 0; i < maximumCallerDepth; i++ {
190 | funcName := runtime.FuncForPC(pcs[i]).Name()
191 | if strings.Contains(funcName, "getCaller") {
192 | logrusPackage = getPackageName(funcName)
193 | break
194 | }
195 | }
196 |
197 | minimumCallerDepth = knownLogrusFrames
198 | })
199 |
200 | // Restrict the lookback frames to avoid runaway lookups
201 | pcs := make([]uintptr, maximumCallerDepth)
202 | depth := runtime.Callers(minimumCallerDepth, pcs)
203 | frames := runtime.CallersFrames(pcs[:depth])
204 |
205 | for f, again := frames.Next(); again; f, again = frames.Next() {
206 | pkg := getPackageName(f.Function)
207 |
208 | // If the caller isn't part of this package, we're done
209 | if pkg != logrusPackage {
210 | return &f
211 | }
212 | }
213 |
214 | // if we got here, we failed to find the caller's context
215 | return nil
216 | }
217 |
218 | func (entry Entry) HasCaller() (has bool) {
219 | return entry.Logger != nil &&
220 | entry.Logger.ReportCaller &&
221 | entry.Caller != nil
222 | }
223 |
224 | func (entry *Entry) log(level Level, msg string) {
225 | var buffer *bytes.Buffer
226 |
227 | newEntry := entry.Dup()
228 |
229 | if newEntry.Time.IsZero() {
230 | newEntry.Time = time.Now()
231 | }
232 |
233 | newEntry.Level = level
234 | newEntry.Message = msg
235 |
236 | newEntry.Logger.mu.Lock()
237 | reportCaller := newEntry.Logger.ReportCaller
238 | bufPool := newEntry.getBufferPool()
239 | newEntry.Logger.mu.Unlock()
240 |
241 | if reportCaller {
242 | newEntry.Caller = getCaller()
243 | }
244 |
245 | newEntry.fireHooks()
246 | buffer = bufPool.Get()
247 | defer func() {
248 | newEntry.Buffer = nil
249 | buffer.Reset()
250 | bufPool.Put(buffer)
251 | }()
252 | buffer.Reset()
253 | newEntry.Buffer = buffer
254 |
255 | newEntry.write()
256 |
257 | newEntry.Buffer = nil
258 |
259 | // To avoid Entry#log() returning a value that only would make sense for
260 | // panic() to use in Entry#Panic(), we avoid the allocation by checking
261 | // directly here.
262 | if level <= PanicLevel {
263 | panic(newEntry)
264 | }
265 | }
266 |
267 | func (entry *Entry) getBufferPool() (pool BufferPool) {
268 | if entry.Logger.BufferPool != nil {
269 | return entry.Logger.BufferPool
270 | }
271 | return bufferPool
272 | }
273 |
274 | func (entry *Entry) fireHooks() {
275 | var tmpHooks LevelHooks
276 | entry.Logger.mu.Lock()
277 | tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
278 | for k, v := range entry.Logger.Hooks {
279 | tmpHooks[k] = v
280 | }
281 | entry.Logger.mu.Unlock()
282 |
283 | err := tmpHooks.Fire(entry.Level, entry)
284 | if err != nil {
285 | fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
286 | }
287 | }
288 |
289 | func (entry *Entry) write() {
290 | entry.Logger.mu.Lock()
291 | defer entry.Logger.mu.Unlock()
292 | serialized, err := entry.Logger.Formatter.Format(entry)
293 | if err != nil {
294 | fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
295 | return
296 | }
297 | if _, err := entry.Logger.Out.Write(serialized); err != nil {
298 | fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
299 | }
300 | }
301 |
302 | // Log will log a message at the level given as parameter.
303 | // Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
304 | // For this behaviour Entry.Panic or Entry.Fatal should be used instead.
305 | func (entry *Entry) Log(level Level, args ...interface{}) {
306 | if entry.Logger.IsLevelEnabled(level) {
307 | entry.log(level, fmt.Sprint(args...))
308 | }
309 | }
310 |
311 | func (entry *Entry) Trace(args ...interface{}) {
312 | entry.Log(TraceLevel, args...)
313 | }
314 |
315 | func (entry *Entry) Debug(args ...interface{}) {
316 | entry.Log(DebugLevel, args...)
317 | }
318 |
319 | func (entry *Entry) Print(args ...interface{}) {
320 | entry.Info(args...)
321 | }
322 |
323 | func (entry *Entry) Info(args ...interface{}) {
324 | entry.Log(InfoLevel, args...)
325 | }
326 |
327 | func (entry *Entry) Warn(args ...interface{}) {
328 | entry.Log(WarnLevel, args...)
329 | }
330 |
331 | func (entry *Entry) Warning(args ...interface{}) {
332 | entry.Warn(args...)
333 | }
334 |
335 | func (entry *Entry) Error(args ...interface{}) {
336 | entry.Log(ErrorLevel, args...)
337 | }
338 |
339 | func (entry *Entry) Fatal(args ...interface{}) {
340 | entry.Log(FatalLevel, args...)
341 | entry.Logger.Exit(1)
342 | }
343 |
344 | func (entry *Entry) Panic(args ...interface{}) {
345 | entry.Log(PanicLevel, args...)
346 | }
347 |
348 | // Entry Printf family functions
349 |
350 | func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
351 | if entry.Logger.IsLevelEnabled(level) {
352 | entry.Log(level, fmt.Sprintf(format, args...))
353 | }
354 | }
355 |
356 | func (entry *Entry) Tracef(format string, args ...interface{}) {
357 | entry.Logf(TraceLevel, format, args...)
358 | }
359 |
360 | func (entry *Entry) Debugf(format string, args ...interface{}) {
361 | entry.Logf(DebugLevel, format, args...)
362 | }
363 |
364 | func (entry *Entry) Infof(format string, args ...interface{}) {
365 | entry.Logf(InfoLevel, format, args...)
366 | }
367 |
368 | func (entry *Entry) Printf(format string, args ...interface{}) {
369 | entry.Infof(format, args...)
370 | }
371 |
372 | func (entry *Entry) Warnf(format string, args ...interface{}) {
373 | entry.Logf(WarnLevel, format, args...)
374 | }
375 |
376 | func (entry *Entry) Warningf(format string, args ...interface{}) {
377 | entry.Warnf(format, args...)
378 | }
379 |
380 | func (entry *Entry) Errorf(format string, args ...interface{}) {
381 | entry.Logf(ErrorLevel, format, args...)
382 | }
383 |
384 | func (entry *Entry) Fatalf(format string, args ...interface{}) {
385 | entry.Logf(FatalLevel, format, args...)
386 | entry.Logger.Exit(1)
387 | }
388 |
389 | func (entry *Entry) Panicf(format string, args ...interface{}) {
390 | entry.Logf(PanicLevel, format, args...)
391 | }
392 |
393 | // Entry Println family functions
394 |
395 | func (entry *Entry) Logln(level Level, args ...interface{}) {
396 | if entry.Logger.IsLevelEnabled(level) {
397 | entry.Log(level, entry.sprintlnn(args...))
398 | }
399 | }
400 |
401 | func (entry *Entry) Traceln(args ...interface{}) {
402 | entry.Logln(TraceLevel, args...)
403 | }
404 |
405 | func (entry *Entry) Debugln(args ...interface{}) {
406 | entry.Logln(DebugLevel, args...)
407 | }
408 |
409 | func (entry *Entry) Infoln(args ...interface{}) {
410 | entry.Logln(InfoLevel, args...)
411 | }
412 |
413 | func (entry *Entry) Println(args ...interface{}) {
414 | entry.Infoln(args...)
415 | }
416 |
417 | func (entry *Entry) Warnln(args ...interface{}) {
418 | entry.Logln(WarnLevel, args...)
419 | }
420 |
421 | func (entry *Entry) Warningln(args ...interface{}) {
422 | entry.Warnln(args...)
423 | }
424 |
425 | func (entry *Entry) Errorln(args ...interface{}) {
426 | entry.Logln(ErrorLevel, args...)
427 | }
428 |
429 | func (entry *Entry) Fatalln(args ...interface{}) {
430 | entry.Logln(FatalLevel, args...)
431 | entry.Logger.Exit(1)
432 | }
433 |
434 | func (entry *Entry) Panicln(args ...interface{}) {
435 | entry.Logln(PanicLevel, args...)
436 | }
437 |
438 | // sprintlnn => Sprint no newline. This is to get the behavior of how
439 | // fmt.Sprintln where spaces are always added between operands, regardless of
440 | // their type. Instead of vendoring the Sprintln implementation to spare a
441 | // string allocation, we do the simplest thing.
442 | func (entry *Entry) sprintlnn(args ...interface{}) string {
443 | msg := fmt.Sprintln(args...)
444 | return msg[:len(msg)-1]
445 | }
446 |
--------------------------------------------------------------------------------
/text_formatter_test.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "runtime"
9 | "sort"
10 | "strings"
11 | "testing"
12 | "time"
13 |
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestFormatting(t *testing.T) {
19 | tf := &TextFormatter{DisableColors: true}
20 |
21 | testCases := []struct {
22 | value string
23 | expected string
24 | }{
25 | {`foo`, "time=\"0001-01-01T00:00:00Z\" level=panic test=foo\n"},
26 | }
27 |
28 | for _, tc := range testCases {
29 | b, _ := tf.Format(WithField("test", tc.value))
30 |
31 | if string(b) != tc.expected {
32 | t.Errorf("formatting expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
33 | }
34 | }
35 | }
36 |
37 | func TestQuoting(t *testing.T) {
38 | tf := &TextFormatter{DisableColors: true}
39 |
40 | checkQuoting := func(q bool, value interface{}) {
41 | b, _ := tf.Format(WithField("test", value))
42 | idx := bytes.Index(b, ([]byte)("test="))
43 | cont := bytes.Contains(b[idx+5:], []byte("\""))
44 | if cont != q {
45 | if q {
46 | t.Errorf("quoting expected for: %#v", value)
47 | } else {
48 | t.Errorf("quoting not expected for: %#v", value)
49 | }
50 | }
51 | }
52 |
53 | checkQuoting(false, "")
54 | checkQuoting(false, "abcd")
55 | checkQuoting(false, "v1.0")
56 | checkQuoting(false, "1234567890")
57 | checkQuoting(false, "/foobar")
58 | checkQuoting(false, "foo_bar")
59 | checkQuoting(false, "foo@bar")
60 | checkQuoting(false, "foobar^")
61 | checkQuoting(false, "+/-_^@f.oobar")
62 | checkQuoting(true, "foo\n\rbar")
63 | checkQuoting(true, "foobar$")
64 | checkQuoting(true, "&foobar")
65 | checkQuoting(true, "x y")
66 | checkQuoting(true, "x,y")
67 | checkQuoting(false, errors.New("invalid"))
68 | checkQuoting(true, errors.New("invalid argument"))
69 |
70 | // Test for quoting empty fields.
71 | tf.QuoteEmptyFields = true
72 | checkQuoting(true, "")
73 | checkQuoting(false, "abcd")
74 | checkQuoting(true, "foo\n\rbar")
75 | checkQuoting(true, errors.New("invalid argument"))
76 |
77 | // Test forcing quotes.
78 | tf.ForceQuote = true
79 | checkQuoting(true, "")
80 | checkQuoting(true, "abcd")
81 | checkQuoting(true, "foo\n\rbar")
82 | checkQuoting(true, errors.New("invalid argument"))
83 |
84 | // Test forcing quotes when also disabling them.
85 | tf.DisableQuote = true
86 | checkQuoting(true, "")
87 | checkQuoting(true, "abcd")
88 | checkQuoting(true, "foo\n\rbar")
89 | checkQuoting(true, errors.New("invalid argument"))
90 |
91 | // Test disabling quotes
92 | tf.ForceQuote = false
93 | tf.QuoteEmptyFields = false
94 | checkQuoting(false, "")
95 | checkQuoting(false, "abcd")
96 | checkQuoting(false, "foo\n\rbar")
97 | checkQuoting(false, errors.New("invalid argument"))
98 | }
99 |
100 | func TestEscaping(t *testing.T) {
101 | tf := &TextFormatter{DisableColors: true}
102 |
103 | testCases := []struct {
104 | value string
105 | expected string
106 | }{
107 | {`ba"r`, `ba\"r`},
108 | {`ba'r`, `ba'r`},
109 | }
110 |
111 | for _, tc := range testCases {
112 | b, _ := tf.Format(WithField("test", tc.value))
113 | if !bytes.Contains(b, []byte(tc.expected)) {
114 | t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
115 | }
116 | }
117 | }
118 |
119 | func TestEscaping_Interface(t *testing.T) {
120 | tf := &TextFormatter{DisableColors: true}
121 |
122 | ts := time.Now()
123 |
124 | testCases := []struct {
125 | value interface{}
126 | expected string
127 | }{
128 | {ts, fmt.Sprintf("\"%s\"", ts.String())},
129 | {errors.New("error: something went wrong"), "\"error: something went wrong\""},
130 | }
131 |
132 | for _, tc := range testCases {
133 | b, _ := tf.Format(WithField("test", tc.value))
134 | if !bytes.Contains(b, []byte(tc.expected)) {
135 | t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
136 | }
137 | }
138 | }
139 |
140 | func TestTimestampFormat(t *testing.T) {
141 | checkTimeStr := func(format string) {
142 | customFormatter := &TextFormatter{DisableColors: true, TimestampFormat: format}
143 | customStr, _ := customFormatter.Format(WithField("test", "test"))
144 | timeStart := bytes.Index(customStr, ([]byte)("time="))
145 | timeEnd := bytes.Index(customStr, ([]byte)("level="))
146 | timeStr := customStr[timeStart+5+len("\"") : timeEnd-1-len("\"")]
147 | if format == "" {
148 | format = time.RFC3339
149 | }
150 | _, e := time.Parse(format, (string)(timeStr))
151 | if e != nil {
152 | t.Errorf("time string \"%s\" did not match provided time format \"%s\": %s", timeStr, format, e)
153 | }
154 | }
155 |
156 | checkTimeStr("2006-01-02T15:04:05.000000000Z07:00")
157 | checkTimeStr("Mon Jan _2 15:04:05 2006")
158 | checkTimeStr("")
159 | }
160 |
161 | func TestDisableLevelTruncation(t *testing.T) {
162 | entry := &Entry{
163 | Time: time.Now(),
164 | Message: "testing",
165 | }
166 | keys := []string{}
167 | timestampFormat := "Mon Jan 2 15:04:05 -0700 MST 2006"
168 | checkDisableTruncation := func(disabled bool, level Level) {
169 | tf := &TextFormatter{DisableLevelTruncation: disabled}
170 | var b bytes.Buffer
171 | entry.Level = level
172 | tf.printColored(&b, entry, keys, nil, timestampFormat)
173 | logLine := (&b).String()
174 | if disabled {
175 | expected := strings.ToUpper(level.String())
176 | if !strings.Contains(logLine, expected) {
177 | t.Errorf("level string expected to be %s when truncation disabled", expected)
178 | }
179 | } else {
180 | expected := strings.ToUpper(level.String())
181 | if len(level.String()) > 4 {
182 | if strings.Contains(logLine, expected) {
183 | t.Errorf("level string %s expected to be truncated to %s when truncation is enabled", expected, expected[0:4])
184 | }
185 | } else {
186 | if !strings.Contains(logLine, expected) {
187 | t.Errorf("level string expected to be %s when truncation is enabled and level string is below truncation threshold", expected)
188 | }
189 | }
190 | }
191 | }
192 |
193 | checkDisableTruncation(true, DebugLevel)
194 | checkDisableTruncation(true, InfoLevel)
195 | checkDisableTruncation(false, ErrorLevel)
196 | checkDisableTruncation(false, InfoLevel)
197 | }
198 |
199 | func TestPadLevelText(t *testing.T) {
200 | // A note for future maintainers / committers:
201 | //
202 | // This test denormalizes the level text as a part of its assertions.
203 | // Because of that, its not really a "unit test" of the PadLevelText functionality.
204 | // So! Many apologies to the potential future person who has to rewrite this test
205 | // when they are changing some completely unrelated functionality.
206 | params := []struct {
207 | name string
208 | level Level
209 | paddedLevelText string
210 | }{
211 | {
212 | name: "PanicLevel",
213 | level: PanicLevel,
214 | paddedLevelText: "PANIC ", // 2 extra spaces
215 | },
216 | {
217 | name: "FatalLevel",
218 | level: FatalLevel,
219 | paddedLevelText: "FATAL ", // 2 extra spaces
220 | },
221 | {
222 | name: "ErrorLevel",
223 | level: ErrorLevel,
224 | paddedLevelText: "ERROR ", // 2 extra spaces
225 | },
226 | {
227 | name: "WarnLevel",
228 | level: WarnLevel,
229 | // WARNING is already the max length, so we don't need to assert a paddedLevelText
230 | },
231 | {
232 | name: "DebugLevel",
233 | level: DebugLevel,
234 | paddedLevelText: "DEBUG ", // 2 extra spaces
235 | },
236 | {
237 | name: "TraceLevel",
238 | level: TraceLevel,
239 | paddedLevelText: "TRACE ", // 2 extra spaces
240 | },
241 | {
242 | name: "InfoLevel",
243 | level: InfoLevel,
244 | paddedLevelText: "INFO ", // 3 extra spaces
245 | },
246 | }
247 |
248 | // We create a "default" TextFormatter to do a control test.
249 | // We also create a TextFormatter with PadLevelText, which is the parameter we want to do our most relevant assertions against.
250 | tfDefault := TextFormatter{}
251 | tfWithPadding := TextFormatter{PadLevelText: true}
252 |
253 | for _, val := range params {
254 | t.Run(val.name, func(t *testing.T) {
255 | // TextFormatter writes into these bytes.Buffers, and we make assertions about their contents later
256 | var bytesDefault bytes.Buffer
257 | var bytesWithPadding bytes.Buffer
258 |
259 | // The TextFormatter instance and the bytes.Buffer instance are different here
260 | // all the other arguments are the same. We also initialize them so that they
261 | // fill in the value of levelTextMaxLength.
262 | tfDefault.init(&Entry{})
263 | tfDefault.printColored(&bytesDefault, &Entry{Level: val.level}, []string{}, nil, "")
264 | tfWithPadding.init(&Entry{})
265 | tfWithPadding.printColored(&bytesWithPadding, &Entry{Level: val.level}, []string{}, nil, "")
266 |
267 | // turn the bytes back into a string so that we can actually work with the data
268 | logLineDefault := (&bytesDefault).String()
269 | logLineWithPadding := (&bytesWithPadding).String()
270 |
271 | // Control: the level text should not be padded by default
272 | if val.paddedLevelText != "" && strings.Contains(logLineDefault, val.paddedLevelText) {
273 | t.Errorf("log line %q should not contain the padded level text %q by default", logLineDefault, val.paddedLevelText)
274 | }
275 |
276 | // Assertion: the level text should still contain the string representation of the level
277 | if !strings.Contains(strings.ToLower(logLineWithPadding), val.level.String()) {
278 | t.Errorf("log line %q should contain the level text %q when padding is enabled", logLineWithPadding, val.level.String())
279 | }
280 |
281 | // Assertion: the level text should be in its padded form now
282 | if val.paddedLevelText != "" && !strings.Contains(logLineWithPadding, val.paddedLevelText) {
283 | t.Errorf("log line %q should contain the padded level text %q when padding is enabled", logLineWithPadding, val.paddedLevelText)
284 | }
285 |
286 | })
287 | }
288 | }
289 |
290 | func TestDisableTimestampWithColoredOutput(t *testing.T) {
291 | tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}
292 |
293 | b, _ := tf.Format(WithField("test", "test"))
294 | if strings.Contains(string(b), "[0000]") {
295 | t.Error("timestamp not expected when DisableTimestamp is true")
296 | }
297 | }
298 |
299 | func TestNewlineBehavior(t *testing.T) {
300 | tf := &TextFormatter{ForceColors: true}
301 |
302 | // Ensure a single new line is removed as per stdlib log
303 | e := NewEntry(StandardLogger())
304 | e.Message = "test message\n"
305 | b, _ := tf.Format(e)
306 | if bytes.Contains(b, []byte("test message\n")) {
307 | t.Error("first newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected newline to be removed.")
308 | }
309 |
310 | // Ensure a double new line is reduced to a single new line
311 | e = NewEntry(StandardLogger())
312 | e.Message = "test message\n\n"
313 | b, _ = tf.Format(e)
314 | if bytes.Contains(b, []byte("test message\n\n")) {
315 | t.Error("Double newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected single newline")
316 | }
317 | if !bytes.Contains(b, []byte("test message\n")) {
318 | t.Error("Double newline at end of Entry.Message did not result in a single newline after formatting")
319 | }
320 | }
321 |
322 | func TestTextFormatterFieldMap(t *testing.T) {
323 | formatter := &TextFormatter{
324 | DisableColors: true,
325 | FieldMap: FieldMap{
326 | FieldKeyMsg: "message",
327 | FieldKeyLevel: "somelevel",
328 | FieldKeyTime: "timeywimey",
329 | },
330 | }
331 |
332 | entry := &Entry{
333 | Message: "oh hi",
334 | Level: WarnLevel,
335 | Time: time.Date(1981, time.February, 24, 4, 28, 3, 100, time.UTC),
336 | Data: Fields{
337 | "field1": "f1",
338 | "message": "messagefield",
339 | "somelevel": "levelfield",
340 | "timeywimey": "timeywimeyfield",
341 | },
342 | }
343 |
344 | b, err := formatter.Format(entry)
345 | if err != nil {
346 | t.Fatal("Unable to format entry: ", err)
347 | }
348 |
349 | assert.Equal(t,
350 | `timeywimey="1981-02-24T04:28:03Z" `+
351 | `somelevel=warning `+
352 | `message="oh hi" `+
353 | `field1=f1 `+
354 | `fields.message=messagefield `+
355 | `fields.somelevel=levelfield `+
356 | `fields.timeywimey=timeywimeyfield`+"\n",
357 | string(b),
358 | "Formatted output doesn't respect FieldMap")
359 | }
360 |
361 | func TestTextFormatterIsColored(t *testing.T) {
362 | params := []struct {
363 | name string
364 | expectedResult bool
365 | isTerminal bool
366 | disableColor bool
367 | forceColor bool
368 | envColor bool
369 | clicolorIsSet bool
370 | clicolorForceIsSet bool
371 | clicolorVal string
372 | clicolorForceVal string
373 | }{
374 | // Default values
375 | {
376 | name: "testcase1",
377 | expectedResult: false,
378 | isTerminal: false,
379 | disableColor: false,
380 | forceColor: false,
381 | envColor: false,
382 | clicolorIsSet: false,
383 | clicolorForceIsSet: false,
384 | },
385 | // Output on terminal
386 | {
387 | name: "testcase2",
388 | expectedResult: true,
389 | isTerminal: true,
390 | disableColor: false,
391 | forceColor: false,
392 | envColor: false,
393 | clicolorIsSet: false,
394 | clicolorForceIsSet: false,
395 | },
396 | // Output on terminal with color disabled
397 | {
398 | name: "testcase3",
399 | expectedResult: false,
400 | isTerminal: true,
401 | disableColor: true,
402 | forceColor: false,
403 | envColor: false,
404 | clicolorIsSet: false,
405 | clicolorForceIsSet: false,
406 | },
407 | // Output not on terminal with color disabled
408 | {
409 | name: "testcase4",
410 | expectedResult: false,
411 | isTerminal: false,
412 | disableColor: true,
413 | forceColor: false,
414 | envColor: false,
415 | clicolorIsSet: false,
416 | clicolorForceIsSet: false,
417 | },
418 | // Output not on terminal with color forced
419 | {
420 | name: "testcase5",
421 | expectedResult: true,
422 | isTerminal: false,
423 | disableColor: false,
424 | forceColor: true,
425 | envColor: false,
426 | clicolorIsSet: false,
427 | clicolorForceIsSet: false,
428 | },
429 | // Output on terminal with clicolor set to "0"
430 | {
431 | name: "testcase6",
432 | expectedResult: false,
433 | isTerminal: true,
434 | disableColor: false,
435 | forceColor: false,
436 | envColor: true,
437 | clicolorIsSet: true,
438 | clicolorForceIsSet: false,
439 | clicolorVal: "0",
440 | },
441 | // Output on terminal with clicolor set to "1"
442 | {
443 | name: "testcase7",
444 | expectedResult: true,
445 | isTerminal: true,
446 | disableColor: false,
447 | forceColor: false,
448 | envColor: true,
449 | clicolorIsSet: true,
450 | clicolorForceIsSet: false,
451 | clicolorVal: "1",
452 | },
453 | // Output not on terminal with clicolor set to "0"
454 | {
455 | name: "testcase8",
456 | expectedResult: false,
457 | isTerminal: false,
458 | disableColor: false,
459 | forceColor: false,
460 | envColor: true,
461 | clicolorIsSet: true,
462 | clicolorForceIsSet: false,
463 | clicolorVal: "0",
464 | },
465 | // Output not on terminal with clicolor set to "1"
466 | {
467 | name: "testcase9",
468 | expectedResult: false,
469 | isTerminal: false,
470 | disableColor: false,
471 | forceColor: false,
472 | envColor: true,
473 | clicolorIsSet: true,
474 | clicolorForceIsSet: false,
475 | clicolorVal: "1",
476 | },
477 | // Output not on terminal with clicolor set to "1" and force color
478 | {
479 | name: "testcase10",
480 | expectedResult: true,
481 | isTerminal: false,
482 | disableColor: false,
483 | forceColor: true,
484 | envColor: true,
485 | clicolorIsSet: true,
486 | clicolorForceIsSet: false,
487 | clicolorVal: "1",
488 | },
489 | // Output not on terminal with clicolor set to "0" and force color
490 | {
491 | name: "testcase11",
492 | expectedResult: false,
493 | isTerminal: false,
494 | disableColor: false,
495 | forceColor: true,
496 | envColor: true,
497 | clicolorIsSet: true,
498 | clicolorForceIsSet: false,
499 | clicolorVal: "0",
500 | },
501 | // Output not on terminal with clicolor_force set to "1"
502 | {
503 | name: "testcase12",
504 | expectedResult: true,
505 | isTerminal: false,
506 | disableColor: false,
507 | forceColor: false,
508 | envColor: true,
509 | clicolorIsSet: false,
510 | clicolorForceIsSet: true,
511 | clicolorForceVal: "1",
512 | },
513 | // Output not on terminal with clicolor_force set to "0"
514 | {
515 | name: "testcase13",
516 | expectedResult: false,
517 | isTerminal: false,
518 | disableColor: false,
519 | forceColor: false,
520 | envColor: true,
521 | clicolorIsSet: false,
522 | clicolorForceIsSet: true,
523 | clicolorForceVal: "0",
524 | },
525 | // Output on terminal with clicolor_force set to "0"
526 | {
527 | name: "testcase14",
528 | expectedResult: false,
529 | isTerminal: true,
530 | disableColor: false,
531 | forceColor: false,
532 | envColor: true,
533 | clicolorIsSet: false,
534 | clicolorForceIsSet: true,
535 | clicolorForceVal: "0",
536 | },
537 | }
538 |
539 | cleanenv := func() {
540 | os.Unsetenv("CLICOLOR")
541 | os.Unsetenv("CLICOLOR_FORCE")
542 | }
543 |
544 | defer cleanenv()
545 |
546 | for _, val := range params {
547 | t.Run("textformatter_"+val.name, func(subT *testing.T) {
548 | tf := TextFormatter{
549 | isTerminal: val.isTerminal,
550 | DisableColors: val.disableColor,
551 | ForceColors: val.forceColor,
552 | EnvironmentOverrideColors: val.envColor,
553 | }
554 | cleanenv()
555 | if val.clicolorIsSet {
556 | os.Setenv("CLICOLOR", val.clicolorVal)
557 | }
558 | if val.clicolorForceIsSet {
559 | os.Setenv("CLICOLOR_FORCE", val.clicolorForceVal)
560 | }
561 | res := tf.isColored()
562 | if runtime.GOOS == "windows" && !tf.ForceColors && !val.clicolorForceIsSet {
563 | assert.Equal(subT, false, res)
564 | } else {
565 | assert.Equal(subT, val.expectedResult, res)
566 | }
567 | })
568 | }
569 | }
570 |
571 | func TestCustomSorting(t *testing.T) {
572 | formatter := &TextFormatter{
573 | DisableColors: true,
574 | SortingFunc: func(keys []string) {
575 | sort.Slice(keys, func(i, j int) bool {
576 | if keys[j] == "prefix" {
577 | return false
578 | }
579 | if keys[i] == "prefix" {
580 | return true
581 | }
582 | return strings.Compare(keys[i], keys[j]) == -1
583 | })
584 | },
585 | }
586 |
587 | entry := &Entry{
588 | Message: "Testing custom sort function",
589 | Time: time.Now(),
590 | Level: InfoLevel,
591 | Data: Fields{
592 | "test": "testvalue",
593 | "prefix": "the application prefix",
594 | "blablabla": "blablabla",
595 | },
596 | }
597 | b, err := formatter.Format(entry)
598 | require.NoError(t, err)
599 | require.True(t, strings.HasPrefix(string(b), "prefix="), "format output is %q", string(b))
600 | }
601 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Logrus
[](https://github.com/sirupsen/logrus/actions?query=workflow%3ACI) [](https://pkg.go.dev/github.com/sirupsen/logrus)
2 |
3 | Logrus is a structured logger for Go (golang), completely API compatible with
4 | the standard library logger.
5 |
6 | **Logrus is in maintenance-mode.** We will not be introducing new features. It's
7 | simply too hard to do in a way that won't break many people's projects, which is
8 | the last thing you want from your Logging library (again...).
9 |
10 | This does not mean Logrus is dead. Logrus will continue to be maintained for
11 | security, (backwards compatible) bug fixes, and performance (where we are
12 | limited by the interface).
13 |
14 | I believe Logrus' biggest contribution is to have played a part in today's
15 | widespread use of structured logging in Golang. There doesn't seem to be a
16 | reason to do a major, breaking iteration into Logrus V2, since the fantastic Go
17 | community has built those independently. Many fantastic alternatives have sprung
18 | up. Logrus would look like those, had it been re-designed with what we know
19 | about structured logging in Go today. Check out, for example,
20 | [Zerolog][zerolog], [Zap][zap], and [Apex][apex].
21 |
22 | [zerolog]: https://github.com/rs/zerolog
23 | [zap]: https://github.com/uber-go/zap
24 | [apex]: https://github.com/apex/log
25 |
26 | **Seeing weird case-sensitive problems?** It's in the past been possible to
27 | import Logrus as both upper- and lower-case. Due to the Go package environment,
28 | this caused issues in the community and we needed a standard. Some environments
29 | experienced problems with the upper-case variant, so the lower-case was decided.
30 | Everything using `logrus` will need to use the lower-case:
31 | `github.com/sirupsen/logrus`. Any package that isn't, should be changed.
32 |
33 | To fix Glide, see [these
34 | comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437).
35 | For an in-depth explanation of the casing issue, see [this
36 | comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276).
37 |
38 | Nicely color-coded in development (when a TTY is attached, otherwise just
39 | plain text):
40 |
41 | 
42 |
43 | With `logrus.SetFormatter(&logrus.JSONFormatter{})`, for easy parsing by logstash
44 | or Splunk:
45 |
46 | ```text
47 | {"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
48 | ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
49 |
50 | {"level":"warning","msg":"The group's number increased tremendously!",
51 | "number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
52 |
53 | {"animal":"walrus","level":"info","msg":"A giant walrus appears!",
54 | "size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
55 |
56 | {"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
57 | "size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
58 |
59 | {"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
60 | "time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
61 | ```
62 |
63 | With the default `logrus.SetFormatter(&logrus.TextFormatter{})` when a TTY is not
64 | attached, the output is compatible with the
65 | [logfmt](https://pkg.go.dev/github.com/kr/logfmt) format:
66 |
67 | ```text
68 | time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
69 | time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
70 | time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
71 | time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
72 | time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
73 | time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
74 | ```
75 | To ensure this behaviour even if a TTY is attached, set your formatter as follows:
76 |
77 | ```go
78 | logrus.SetFormatter(&logrus.TextFormatter{
79 | DisableColors: true,
80 | FullTimestamp: true,
81 | })
82 | ```
83 |
84 | #### Logging Method Name
85 |
86 | If you wish to add the calling method as a field, instruct the logger via:
87 |
88 | ```go
89 | logrus.SetReportCaller(true)
90 | ```
91 | This adds the caller as 'method' like so:
92 |
93 | ```json
94 | {"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
95 | "time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
96 | ```
97 |
98 | ```text
99 | time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
100 | ```
101 | Note that this does add measurable overhead - the cost will depend on the version of Go, but is
102 | between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
103 | environment via benchmarks:
104 |
105 | ```bash
106 | go test -bench=.*CallerTracing
107 | ```
108 |
109 | #### Case-sensitivity
110 |
111 | The organization's name was changed to lower-case--and this will not be changed
112 | back. If you are getting import conflicts due to case sensitivity, please use
113 | the lower-case import: `github.com/sirupsen/logrus`.
114 |
115 | #### Example
116 |
117 | The simplest way to use Logrus is simply the package-level exported logger:
118 |
119 | ```go
120 | package main
121 |
122 | import "github.com/sirupsen/logrus"
123 |
124 | func main() {
125 | logrus.WithFields(logrus.Fields{
126 | "animal": "walrus",
127 | }).Info("A walrus appears")
128 | }
129 | ```
130 |
131 | Note that it's completely api-compatible with the stdlib logger, so you can
132 | replace your `log` imports everywhere with `log "github.com/sirupsen/logrus"`
133 | and you'll now have the flexibility of Logrus. You can customize it all you
134 | want:
135 |
136 | ```go
137 | package main
138 |
139 | import (
140 | "os"
141 |
142 | log "github.com/sirupsen/logrus"
143 | )
144 |
145 | func init() {
146 | // Log as JSON instead of the default ASCII formatter.
147 | log.SetFormatter(&log.JSONFormatter{})
148 |
149 | // Output to stdout instead of the default stderr
150 | // Can be any io.Writer, see below for File example
151 | log.SetOutput(os.Stdout)
152 |
153 | // Only log the warning severity or above.
154 | log.SetLevel(log.WarnLevel)
155 | }
156 |
157 | func main() {
158 | log.WithFields(log.Fields{
159 | "animal": "walrus",
160 | "size": 10,
161 | }).Info("A group of walrus emerges from the ocean")
162 |
163 | log.WithFields(log.Fields{
164 | "omg": true,
165 | "number": 122,
166 | }).Warn("The group's number increased tremendously!")
167 |
168 | log.WithFields(log.Fields{
169 | "omg": true,
170 | "number": 100,
171 | }).Fatal("The ice breaks!")
172 |
173 | // A common pattern is to re-use fields between logging statements by re-using
174 | // the logrus.Entry returned from WithFields()
175 | contextLogger := log.WithFields(log.Fields{
176 | "common": "this is a common field",
177 | "other": "I also should be logged always",
178 | })
179 |
180 | contextLogger.Info("I'll be logged with common and other field")
181 | contextLogger.Info("Me too")
182 | }
183 | ```
184 |
185 | For more advanced usage such as logging to multiple locations from the same
186 | application, you can also create an instance of the `logrus` Logger:
187 |
188 | ```go
189 | package main
190 |
191 | import (
192 | "os"
193 |
194 | "github.com/sirupsen/logrus"
195 | )
196 |
197 | // Create a new instance of the logger. You can have any number of instances.
198 | var logger = logrus.New()
199 |
200 | func main() {
201 | // The API for setting attributes is a little different than the package level
202 | // exported logger. See Godoc.
203 | logger.Out = os.Stdout
204 |
205 | // You could set this to any `io.Writer` such as a file
206 | // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
207 | // if err == nil {
208 | // logger.Out = file
209 | // } else {
210 | // logger.Info("Failed to log to file, using default stderr")
211 | // }
212 |
213 | logger.WithFields(logrus.Fields{
214 | "animal": "walrus",
215 | "size": 10,
216 | }).Info("A group of walrus emerges from the ocean")
217 | }
218 | ```
219 |
220 | #### Fields
221 |
222 | Logrus encourages careful, structured logging through logging fields instead of
223 | long, unparseable error messages. For example, instead of: `logrus.Fatalf("Failed
224 | to send event %s to topic %s with key %d")`, you should log the much more
225 | discoverable:
226 |
227 | ```go
228 | logrus.WithFields(logrus.Fields{
229 | "event": event,
230 | "topic": topic,
231 | "key": key,
232 | }).Fatal("Failed to send event")
233 | ```
234 |
235 | We've found this API forces you to think about logging in a way that produces
236 | much more useful logging messages. We've been in countless situations where just
237 | a single added field to a log statement that was already there would've saved us
238 | hours. The `WithFields` call is optional.
239 |
240 | In general, with Logrus using any of the `printf`-family functions should be
241 | seen as a hint you should add a field, however, you can still use the
242 | `printf`-family functions with Logrus.
243 |
244 | #### Default Fields
245 |
246 | Often it's helpful to have fields _always_ attached to log statements in an
247 | application or parts of one. For example, you may want to always log the
248 | `request_id` and `user_ip` in the context of a request. Instead of writing
249 | `logger.WithFields(logrus.Fields{"request_id": request_id, "user_ip": user_ip})` on
250 | every line, you can create a `logrus.Entry` to pass around instead:
251 |
252 | ```go
253 | requestLogger := logger.WithFields(logrus.Fields{"request_id": request_id, "user_ip": user_ip})
254 | requestLogger.Info("something happened on that request") // will log request_id and user_ip
255 | requestLogger.Warn("something not great happened")
256 | ```
257 |
258 | #### Hooks
259 |
260 | You can add hooks for logging levels. For example to send errors to an exception
261 | tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
262 | multiple places simultaneously, e.g. syslog.
263 |
264 | Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
265 | `init`:
266 |
267 | ```go
268 | package main
269 |
270 | import (
271 | "log/syslog"
272 |
273 | "github.com/sirupsen/logrus"
274 | airbrake "gopkg.in/gemnasium/logrus-airbrake-hook.v2"
275 | logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
276 | )
277 |
278 | func init() {
279 |
280 | // Use the Airbrake hook to report errors that have Error severity or above to
281 | // an exception tracker. You can create custom hooks, see the Hooks section.
282 | logrus.AddHook(airbrake.NewHook(123, "xyz", "production"))
283 |
284 | hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
285 | if err != nil {
286 | logrus.Error("Unable to connect to local syslog daemon")
287 | } else {
288 | logrus.AddHook(hook)
289 | }
290 | }
291 | ```
292 | Note: Syslog hooks also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
293 |
294 | A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
295 |
296 |
297 | #### Level logging
298 |
299 | Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
300 |
301 | ```go
302 | logrus.Trace("Something very low level.")
303 | logrus.Debug("Useful debugging information.")
304 | logrus.Info("Something noteworthy happened!")
305 | logrus.Warn("You should probably take a look at this.")
306 | logrus.Error("Something failed but I'm not quitting.")
307 | // Calls os.Exit(1) after logging
308 | logrus.Fatal("Bye.")
309 | // Calls panic() after logging
310 | logrus.Panic("I'm bailing.")
311 | ```
312 |
313 | You can set the logging level on a `Logger`, then it will only log entries with
314 | that severity or anything above it:
315 |
316 | ```go
317 | // Will log anything that is info or above (warn, error, fatal, panic). Default.
318 | logrus.SetLevel(logrus.InfoLevel)
319 | ```
320 |
321 | It may be useful to set `logrus.Level = logrus.DebugLevel` in a debug or verbose
322 | environment if your application has that.
323 |
324 | Note: If you want different log levels for global (`logrus.SetLevel(...)`) and syslog logging, please check the [syslog hook README](hooks/syslog/README.md#different-log-levels-for-local-and-remote-logging).
325 |
326 | #### Entries
327 |
328 | Besides the fields added with `WithField` or `WithFields` some fields are
329 | automatically added to all logging events:
330 |
331 | 1. `time`. The timestamp when the entry was created.
332 | 2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after
333 | the `AddFields` call. E.g. `Failed to send event.`
334 | 3. `level`. The logging level. E.g. `info`.
335 |
336 | #### Environments
337 |
338 | Logrus has no notion of environment.
339 |
340 | If you wish for hooks and formatters to only be used in specific environments,
341 | you should handle that yourself. For example, if your application has a global
342 | variable `Environment`, which is a string representation of the environment you
343 | could do:
344 |
345 | ```go
346 | import (
347 | "github.com/sirupsen/logrus"
348 | )
349 |
350 | func init() {
351 | // do something here to set environment depending on an environment variable
352 | // or command-line flag
353 | if Environment == "production" {
354 | logrus.SetFormatter(&logrus.JSONFormatter{})
355 | } else {
356 | // The TextFormatter is default, you don't actually have to do this.
357 | logrus.SetFormatter(&logrus.TextFormatter{})
358 | }
359 | }
360 | ```
361 |
362 | This configuration is how `logrus` was intended to be used, but JSON in
363 | production is mostly only useful if you do log aggregation with tools like
364 | Splunk or Logstash.
365 |
366 | #### Formatters
367 |
368 | The built-in logging formatters are:
369 |
370 | * `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
371 | without colors.
372 | * *Note:* to force colored output when there is no TTY, set the `ForceColors`
373 | field to `true`. To force no colored output even if there is a TTY set the
374 | `DisableColors` field to `true`. For Windows, see
375 | [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
376 | * When colors are enabled, levels are truncated to 4 characters by default. To disable
377 | truncation set the `DisableLevelTruncation` field to `true`.
378 | * When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
379 | * All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter).
380 | * `logrus.JSONFormatter`. Logs fields as JSON.
381 | * All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter).
382 |
383 | Third-party logging formatters:
384 |
385 | * [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
386 | * [`GELF`](https://github.com/fabienm/go-logrus-formatters). Formats entries so they comply to Graylog's [GELF 1.1 specification](http://docs.graylog.org/en/2.4/pages/gelf.html).
387 | * [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
388 | * [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
389 | * [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo.
390 | * [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
391 | * [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Save log to files.
392 | * [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
393 |
394 | You can define your formatter by implementing the `Formatter` interface,
395 | requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
396 | `Fields` type (`map[string]interface{}`) with all your fields as well as the
397 | default ones (see Entries section above):
398 |
399 | ```go
400 | type MyJSONFormatter struct{}
401 |
402 | logrus.SetFormatter(new(MyJSONFormatter))
403 |
404 | func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
405 | // Note this doesn't include Time, Level and Message which are available on
406 | // the Entry. Consult `godoc` on information about those fields or read the
407 | // source of the official loggers.
408 | serialized, err := json.Marshal(entry.Data)
409 | if err != nil {
410 | return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
411 | }
412 | return append(serialized, '\n'), nil
413 | }
414 | ```
415 |
416 | #### Logger as an `io.Writer`
417 |
418 | Logrus can be transformed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
419 |
420 | ```go
421 | w := logger.Writer()
422 | defer w.Close()
423 |
424 | srv := http.Server{
425 | // create a stdlib log.Logger that writes to
426 | // logrus.Logger.
427 | ErrorLog: log.New(w, "", 0),
428 | }
429 | ```
430 |
431 | Each line written to that writer will be printed the usual way, using formatters
432 | and hooks. The level for those entries is `info`.
433 |
434 | This means that we can override the standard library logger easily:
435 |
436 | ```go
437 | logger := logrus.New()
438 | logger.Formatter = &logrus.JSONFormatter{}
439 |
440 | // Use logrus for standard log output
441 | // Note that `log` here references stdlib's log
442 | // Not logrus imported under the name `log`.
443 | log.SetOutput(logger.Writer())
444 | ```
445 |
446 | #### Rotation
447 |
448 | Log rotation is not provided with Logrus. Log rotation should be done by an
449 | external program (like `logrotate(8)`) that can compress and delete old log
450 | entries. It should not be a feature of the application-level logger.
451 |
452 | #### Tools
453 |
454 | | Tool | Description |
455 | | ---- | ----------- |
456 | |[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will be generated with different configs in different environments.|
457 | |[Logrus Viper Helper](https://github.com/heirko/go-contrib/tree/master/logrusHelper)|An Helper around Logrus to wrap with spf13/Viper to load configuration with fangs! And to simplify Logrus configuration use some behavior of [Logrus Mate](https://github.com/gogap/logrus_mate). [sample](https://github.com/heirko/iris-contrib/blob/master/middleware/logrus-logger/example) |
458 |
459 | #### Testing
460 |
461 | Logrus has a built-in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
462 |
463 | * decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just adds the `test` hook
464 | * a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
465 |
466 | ```go
467 | import(
468 | "testing"
469 |
470 | "github.com/sirupsen/logrus"
471 | "github.com/sirupsen/logrus/hooks/test"
472 | "github.com/stretchr/testify/assert"
473 | )
474 |
475 | func TestSomething(t*testing.T){
476 | logger, hook := test.NewNullLogger()
477 | logger.Error("Helloerror")
478 |
479 | assert.Equal(t, 1, len(hook.Entries))
480 | assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
481 | assert.Equal(t, "Helloerror", hook.LastEntry().Message)
482 |
483 | hook.Reset()
484 | assert.Nil(t, hook.LastEntry())
485 | }
486 | ```
487 |
488 | #### Fatal handlers
489 |
490 | Logrus can register one or more functions that will be called when any `fatal`
491 | level message is logged. The registered handlers will be executed before
492 | logrus performs an `os.Exit(1)`. This behavior may be helpful if callers need
493 | to gracefully shut down. Unlike a `panic("Something went wrong...")` call which can be intercepted with a deferred `recover` a call to `os.Exit(1)` can not be intercepted.
494 |
495 | ```go
496 | // ...
497 | handler := func() {
498 | // gracefully shut down something...
499 | }
500 | logrus.RegisterExitHandler(handler)
501 | // ...
502 | ```
503 |
504 | #### Thread safety
505 |
506 | By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
507 | If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
508 |
509 | Situations when locking is not needed include:
510 |
511 | * You have no hooks registered, or hooks calling is already thread-safe.
512 |
513 | * Writing to logger.Out is already thread-safe, for example:
514 |
515 | 1) logger.Out is protected by locks.
516 |
517 | 2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing)
518 |
519 | (Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)
520 |
--------------------------------------------------------------------------------
/logrus_test.go:
--------------------------------------------------------------------------------
1 | package logrus_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "runtime"
11 | "sync"
12 | "testing"
13 | "time"
14 |
15 | "github.com/stretchr/testify/assert"
16 | "github.com/stretchr/testify/require"
17 |
18 | . "github.com/sirupsen/logrus"
19 | . "github.com/sirupsen/logrus/internal/testutils"
20 | )
21 |
22 | // TestReportCaller verifies that when ReportCaller is set, the 'func' field
23 | // is added, and when it is unset it is not set or modified
24 | // Verify that functions within the Logrus package aren't considered when
25 | // discovering the caller.
26 | func TestReportCallerWhenConfigured(t *testing.T) {
27 | LogAndAssertJSON(t, func(log *Logger) {
28 | log.ReportCaller = false
29 | log.Print("testNoCaller")
30 | }, func(fields Fields) {
31 | assert.Equal(t, "testNoCaller", fields["msg"])
32 | assert.Equal(t, "info", fields["level"])
33 | assert.Nil(t, fields["func"])
34 | })
35 |
36 | LogAndAssertJSON(t, func(log *Logger) {
37 | log.ReportCaller = true
38 | log.Print("testWithCaller")
39 | }, func(fields Fields) {
40 | assert.Equal(t, "testWithCaller", fields["msg"])
41 | assert.Equal(t, "info", fields["level"])
42 | assert.Equal(t,
43 | "github.com/sirupsen/logrus_test.TestReportCallerWhenConfigured.func3", fields[FieldKeyFunc])
44 | })
45 |
46 | LogAndAssertJSON(t, func(log *Logger) {
47 | log.ReportCaller = true
48 | log.Formatter.(*JSONFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
49 | return "somekindoffunc", "thisisafilename"
50 | }
51 | log.Print("testWithCallerPrettyfier")
52 | }, func(fields Fields) {
53 | assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
54 | assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
55 | })
56 |
57 | LogAndAssertText(t, func(log *Logger) {
58 | log.ReportCaller = true
59 | log.Formatter.(*TextFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
60 | return "somekindoffunc", "thisisafilename"
61 | }
62 | log.Print("testWithCallerPrettyfier")
63 | }, func(fields map[string]string) {
64 | assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
65 | assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
66 | })
67 | }
68 |
69 | func logSomething(t *testing.T, message string) Fields {
70 | var buffer bytes.Buffer
71 | var fields Fields
72 |
73 | logger := New()
74 | logger.Out = &buffer
75 | logger.Formatter = new(JSONFormatter)
76 | logger.ReportCaller = true
77 |
78 | entry := logger.WithFields(Fields{
79 | "foo": "bar",
80 | })
81 |
82 | entry.Info(message)
83 |
84 | err := json.Unmarshal(buffer.Bytes(), &fields)
85 | require.NoError(t, err)
86 |
87 | return fields
88 | }
89 |
90 | // TestReportCallerHelperDirect - verify reference when logging from a regular function
91 | func TestReportCallerHelperDirect(t *testing.T) {
92 | fields := logSomething(t, "direct")
93 |
94 | assert.Equal(t, "direct", fields["msg"])
95 | assert.Equal(t, "info", fields["level"])
96 | assert.Regexp(t, "github.com/.*/logrus_test.logSomething", fields["func"])
97 | }
98 |
99 | // TestReportCallerHelperDirect - verify reference when logging from a function called via pointer
100 | func TestReportCallerHelperViaPointer(t *testing.T) {
101 | fptr := logSomething
102 | fields := fptr(t, "via pointer")
103 |
104 | assert.Equal(t, "via pointer", fields["msg"])
105 | assert.Equal(t, "info", fields["level"])
106 | assert.Regexp(t, "github.com/.*/logrus_test.logSomething", fields["func"])
107 | }
108 |
109 | func TestPrint(t *testing.T) {
110 | LogAndAssertJSON(t, func(log *Logger) {
111 | log.Print("test")
112 | }, func(fields Fields) {
113 | assert.Equal(t, "test", fields["msg"])
114 | assert.Equal(t, "info", fields["level"])
115 | })
116 | }
117 |
118 | func TestInfo(t *testing.T) {
119 | LogAndAssertJSON(t, func(log *Logger) {
120 | log.Info("test")
121 | }, func(fields Fields) {
122 | assert.Equal(t, "test", fields["msg"])
123 | assert.Equal(t, "info", fields["level"])
124 | })
125 | }
126 |
127 | func TestWarn(t *testing.T) {
128 | LogAndAssertJSON(t, func(log *Logger) {
129 | log.Warn("test")
130 | }, func(fields Fields) {
131 | assert.Equal(t, "test", fields["msg"])
132 | assert.Equal(t, "warning", fields["level"])
133 | })
134 | }
135 |
136 | func TestLog(t *testing.T) {
137 | LogAndAssertJSON(t, func(log *Logger) {
138 | log.Log(WarnLevel, "test")
139 | }, func(fields Fields) {
140 | assert.Equal(t, "test", fields["msg"])
141 | assert.Equal(t, "warning", fields["level"])
142 | })
143 | }
144 |
145 | func TestInfolnShouldAddSpacesBetweenStrings(t *testing.T) {
146 | LogAndAssertJSON(t, func(log *Logger) {
147 | log.Infoln("test", "test")
148 | }, func(fields Fields) {
149 | assert.Equal(t, "test test", fields["msg"])
150 | })
151 | }
152 |
153 | func TestInfolnShouldAddSpacesBetweenStringAndNonstring(t *testing.T) {
154 | LogAndAssertJSON(t, func(log *Logger) {
155 | log.Infoln("test", 10)
156 | }, func(fields Fields) {
157 | assert.Equal(t, "test 10", fields["msg"])
158 | })
159 | }
160 |
161 | func TestInfolnShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
162 | LogAndAssertJSON(t, func(log *Logger) {
163 | log.Infoln(10, 10)
164 | }, func(fields Fields) {
165 | assert.Equal(t, "10 10", fields["msg"])
166 | })
167 | }
168 |
169 | func TestInfoShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
170 | LogAndAssertJSON(t, func(log *Logger) {
171 | log.Infoln(10, 10)
172 | }, func(fields Fields) {
173 | assert.Equal(t, "10 10", fields["msg"])
174 | })
175 | }
176 |
177 | func TestInfoShouldNotAddSpacesBetweenStringAndNonstring(t *testing.T) {
178 | LogAndAssertJSON(t, func(log *Logger) {
179 | log.Info("test", 10)
180 | }, func(fields Fields) {
181 | assert.Equal(t, "test10", fields["msg"])
182 | })
183 | }
184 |
185 | func TestInfoShouldNotAddSpacesBetweenStrings(t *testing.T) {
186 | LogAndAssertJSON(t, func(log *Logger) {
187 | log.Info("test", "test")
188 | }, func(fields Fields) {
189 | assert.Equal(t, "testtest", fields["msg"])
190 | })
191 | }
192 |
193 | func TestWithFieldsShouldAllowAssignments(t *testing.T) {
194 | var buffer bytes.Buffer
195 | var fields Fields
196 |
197 | logger := New()
198 | logger.Out = &buffer
199 | logger.Formatter = new(JSONFormatter)
200 |
201 | localLog := logger.WithFields(Fields{
202 | "key1": "value1",
203 | })
204 |
205 | localLog.WithField("key2", "value2").Info("test")
206 | err := json.Unmarshal(buffer.Bytes(), &fields)
207 | require.NoError(t, err)
208 |
209 | assert.Equal(t, "value2", fields["key2"])
210 | assert.Equal(t, "value1", fields["key1"])
211 |
212 | buffer = bytes.Buffer{}
213 | fields = Fields{}
214 | localLog.Info("test")
215 | err = json.Unmarshal(buffer.Bytes(), &fields)
216 | require.NoError(t, err)
217 |
218 | _, ok := fields["key2"]
219 | assert.False(t, ok)
220 | assert.Equal(t, "value1", fields["key1"])
221 | }
222 |
223 | func TestUserSuppliedFieldDoesNotOverwriteDefaults(t *testing.T) {
224 | LogAndAssertJSON(t, func(log *Logger) {
225 | log.WithField("msg", "hello").Info("test")
226 | }, func(fields Fields) {
227 | assert.Equal(t, "test", fields["msg"])
228 | })
229 | }
230 |
231 | func TestUserSuppliedMsgFieldHasPrefix(t *testing.T) {
232 | LogAndAssertJSON(t, func(log *Logger) {
233 | log.WithField("msg", "hello").Info("test")
234 | }, func(fields Fields) {
235 | assert.Equal(t, "test", fields["msg"])
236 | assert.Equal(t, "hello", fields["fields.msg"])
237 | })
238 | }
239 |
240 | func TestUserSuppliedTimeFieldHasPrefix(t *testing.T) {
241 | LogAndAssertJSON(t, func(log *Logger) {
242 | log.WithField("time", "hello").Info("test")
243 | }, func(fields Fields) {
244 | assert.Equal(t, "hello", fields["fields.time"])
245 | })
246 | }
247 |
248 | func TestUserSuppliedLevelFieldHasPrefix(t *testing.T) {
249 | LogAndAssertJSON(t, func(log *Logger) {
250 | log.WithField("level", 1).Info("test")
251 | }, func(fields Fields) {
252 | assert.Equal(t, "info", fields["level"])
253 | assert.Equal(t, 1.0, fields["fields.level"]) // JSON has floats only
254 | })
255 | }
256 |
257 | func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
258 | LogAndAssertText(t, func(log *Logger) {
259 | ll := log.WithField("herp", "derp")
260 | ll.Info("hello")
261 | ll.Info("bye")
262 | }, func(fields map[string]string) {
263 | for _, fieldName := range []string{"fields.level", "fields.time", "fields.msg"} {
264 | if _, ok := fields[fieldName]; ok {
265 | t.Fatalf("should not have prefixed %q: %v", fieldName, fields)
266 | }
267 | }
268 | })
269 | }
270 |
271 | func TestWithTimeShouldOverrideTime(t *testing.T) {
272 | now := time.Now().Add(24 * time.Hour)
273 |
274 | LogAndAssertJSON(t, func(log *Logger) {
275 | log.WithTime(now).Info("foobar")
276 | }, func(fields Fields) {
277 | assert.Equal(t, fields["time"], now.Format(time.RFC3339))
278 | })
279 | }
280 |
281 | func TestWithTimeShouldNotOverrideFields(t *testing.T) {
282 | now := time.Now().Add(24 * time.Hour)
283 |
284 | LogAndAssertJSON(t, func(log *Logger) {
285 | log.WithField("herp", "derp").WithTime(now).Info("blah")
286 | }, func(fields Fields) {
287 | assert.Equal(t, now.Format(time.RFC3339), fields["time"])
288 | assert.Equal(t, "derp", fields["herp"])
289 | })
290 | }
291 |
292 | func TestWithFieldShouldNotOverrideTime(t *testing.T) {
293 | now := time.Now().Add(24 * time.Hour)
294 |
295 | LogAndAssertJSON(t, func(log *Logger) {
296 | log.WithTime(now).WithField("herp", "derp").Info("blah")
297 | }, func(fields Fields) {
298 | assert.Equal(t, now.Format(time.RFC3339), fields["time"])
299 | assert.Equal(t, "derp", fields["herp"])
300 | })
301 | }
302 |
303 | func TestTimeOverrideMultipleLogs(t *testing.T) {
304 | var buffer bytes.Buffer
305 | var firstFields, secondFields Fields
306 |
307 | logger := New()
308 | logger.Out = &buffer
309 | formatter := new(JSONFormatter)
310 | formatter.TimestampFormat = time.StampMilli
311 | logger.Formatter = formatter
312 |
313 | llog := logger.WithField("herp", "derp")
314 | llog.Info("foo")
315 |
316 | err := json.Unmarshal(buffer.Bytes(), &firstFields)
317 | require.NoError(t, err, "should have decoded first message")
318 |
319 | buffer.Reset()
320 |
321 | time.Sleep(10 * time.Millisecond)
322 | llog.Info("bar")
323 |
324 | err = json.Unmarshal(buffer.Bytes(), &secondFields)
325 | require.NoError(t, err, "should have decoded second message")
326 |
327 | assert.NotEqual(t, firstFields["time"], secondFields["time"], "timestamps should not be equal")
328 | }
329 |
330 | func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
331 |
332 | var buffer bytes.Buffer
333 | var fields Fields
334 |
335 | logger := New()
336 | logger.Out = &buffer
337 | logger.Formatter = new(JSONFormatter)
338 |
339 | llog := logger.WithField("context", "eating raw fish")
340 |
341 | llog.Info("looks delicious")
342 |
343 | err := json.Unmarshal(buffer.Bytes(), &fields)
344 | require.NoError(t, err, "should have decoded first message")
345 | assert.Equal(t, 4, len(fields), "should only have msg/time/level/context fields")
346 | assert.Equal(t, "looks delicious", fields["msg"])
347 | assert.Equal(t, "eating raw fish", fields["context"])
348 |
349 | buffer.Reset()
350 |
351 | llog.Warn("omg it is!")
352 |
353 | err = json.Unmarshal(buffer.Bytes(), &fields)
354 | require.NoError(t, err, "should have decoded second message")
355 | assert.Equal(t, 4, len(fields), "should only have msg/time/level/context fields")
356 | assert.Equal(t, "omg it is!", fields["msg"])
357 | assert.Equal(t, "eating raw fish", fields["context"])
358 | assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
359 |
360 | }
361 |
362 | func TestNestedLoggingReportsCorrectCaller(t *testing.T) {
363 | var buffer bytes.Buffer
364 | var fields Fields
365 |
366 | logger := New()
367 | logger.Out = &buffer
368 | logger.Formatter = new(JSONFormatter)
369 | logger.ReportCaller = true
370 |
371 | llog := logger.WithField("context", "eating raw fish")
372 |
373 | llog.Info("looks delicious")
374 | _, _, line, _ := runtime.Caller(0)
375 |
376 | err := json.Unmarshal(buffer.Bytes(), &fields)
377 | require.NoError(t, err, "should have decoded first message")
378 | assert.Equal(t, 6, len(fields), "should have msg/time/level/func/context fields")
379 | assert.Equal(t, "looks delicious", fields["msg"])
380 | assert.Equal(t, "eating raw fish", fields["context"])
381 | assert.Equal(t,
382 | "github.com/sirupsen/logrus_test.TestNestedLoggingReportsCorrectCaller", fields["func"])
383 | cwd, err := os.Getwd()
384 | require.NoError(t, err)
385 | assert.Equal(t, filepath.ToSlash(fmt.Sprintf("%s/logrus_test.go:%d", cwd, line-1)), filepath.ToSlash(fields["file"].(string)))
386 |
387 | buffer.Reset()
388 |
389 | logger.WithFields(Fields{
390 | "Clyde": "Stubblefield",
391 | }).WithFields(Fields{
392 | "Jab'o": "Starks",
393 | }).WithFields(Fields{
394 | "uri": "https://www.youtube.com/watch?v=V5DTznu-9v0",
395 | }).WithFields(Fields{
396 | "func": "y drummer",
397 | }).WithFields(Fields{
398 | "James": "Brown",
399 | }).Print("The hardest workin' man in show business")
400 | _, _, line, _ = runtime.Caller(0)
401 |
402 | err = json.Unmarshal(buffer.Bytes(), &fields)
403 | require.NoError(t, err, "should have decoded second message")
404 | assert.Equal(t, 11, len(fields), "should have all builtin fields plus foo,bar,baz,...")
405 | assert.Equal(t, "Stubblefield", fields["Clyde"])
406 | assert.Equal(t, "Starks", fields["Jab'o"])
407 | assert.Equal(t, "https://www.youtube.com/watch?v=V5DTznu-9v0", fields["uri"])
408 | assert.Equal(t, "y drummer", fields["fields.func"])
409 | assert.Equal(t, "Brown", fields["James"])
410 | assert.Equal(t, "The hardest workin' man in show business", fields["msg"])
411 | assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
412 | assert.Equal(t,
413 | "github.com/sirupsen/logrus_test.TestNestedLoggingReportsCorrectCaller", fields["func"])
414 | require.NoError(t, err)
415 | assert.Equal(t, filepath.ToSlash(fmt.Sprintf("%s/logrus_test.go:%d", cwd, line-1)), filepath.ToSlash(fields["file"].(string)))
416 |
417 | logger.ReportCaller = false // return to default value
418 | }
419 |
420 | func logLoop(iterations int, reportCaller bool) {
421 | var buffer bytes.Buffer
422 |
423 | logger := New()
424 | logger.Out = &buffer
425 | logger.Formatter = new(JSONFormatter)
426 | logger.ReportCaller = reportCaller
427 |
428 | for i := 0; i < iterations; i++ {
429 | logger.Infof("round %d of %d", i, iterations)
430 | }
431 | }
432 |
433 | // Assertions for upper bounds to reporting overhead
434 | func TestCallerReportingOverhead(t *testing.T) {
435 | iterations := 5000
436 | before := time.Now()
437 | logLoop(iterations, false)
438 | during := time.Now()
439 | logLoop(iterations, true)
440 | after := time.Now()
441 |
442 | elapsedNotReporting := during.Sub(before).Nanoseconds()
443 | elapsedReporting := after.Sub(during).Nanoseconds()
444 |
445 | maxDelta := 1 * time.Second
446 | assert.WithinDuration(t, during, before, maxDelta,
447 | "%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
448 | iterations, maxDelta.Seconds(), elapsedNotReporting)
449 | assert.WithinDuration(t, after, during, maxDelta,
450 | "%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
451 | iterations, maxDelta.Seconds(), elapsedReporting)
452 | }
453 |
454 | // benchmarks for both with and without caller-function reporting
455 | func BenchmarkWithoutCallerTracing(b *testing.B) {
456 | for i := 0; i < b.N; i++ {
457 | logLoop(1000, false)
458 | }
459 | }
460 |
461 | func BenchmarkWithCallerTracing(b *testing.B) {
462 | for i := 0; i < b.N; i++ {
463 | logLoop(1000, true)
464 | }
465 | }
466 |
467 | func TestConvertLevelToString(t *testing.T) {
468 | assert.Equal(t, "trace", TraceLevel.String())
469 | assert.Equal(t, "debug", DebugLevel.String())
470 | assert.Equal(t, "info", InfoLevel.String())
471 | assert.Equal(t, "warning", WarnLevel.String())
472 | assert.Equal(t, "error", ErrorLevel.String())
473 | assert.Equal(t, "fatal", FatalLevel.String())
474 | assert.Equal(t, "panic", PanicLevel.String())
475 | }
476 |
477 | func TestParseLevel(t *testing.T) {
478 | l, err := ParseLevel("panic")
479 | require.NoError(t, err)
480 | assert.Equal(t, PanicLevel, l)
481 |
482 | l, err = ParseLevel("PANIC")
483 | require.NoError(t, err)
484 | assert.Equal(t, PanicLevel, l)
485 |
486 | l, err = ParseLevel("fatal")
487 | require.NoError(t, err)
488 | assert.Equal(t, FatalLevel, l)
489 |
490 | l, err = ParseLevel("FATAL")
491 | require.NoError(t, err)
492 | assert.Equal(t, FatalLevel, l)
493 |
494 | l, err = ParseLevel("error")
495 | require.NoError(t, err)
496 | assert.Equal(t, ErrorLevel, l)
497 |
498 | l, err = ParseLevel("ERROR")
499 | require.NoError(t, err)
500 | assert.Equal(t, ErrorLevel, l)
501 |
502 | l, err = ParseLevel("warn")
503 | require.NoError(t, err)
504 | assert.Equal(t, WarnLevel, l)
505 |
506 | l, err = ParseLevel("WARN")
507 | require.NoError(t, err)
508 | assert.Equal(t, WarnLevel, l)
509 |
510 | l, err = ParseLevel("warning")
511 | require.NoError(t, err)
512 | assert.Equal(t, WarnLevel, l)
513 |
514 | l, err = ParseLevel("WARNING")
515 | require.NoError(t, err)
516 | assert.Equal(t, WarnLevel, l)
517 |
518 | l, err = ParseLevel("info")
519 | require.NoError(t, err)
520 | assert.Equal(t, InfoLevel, l)
521 |
522 | l, err = ParseLevel("INFO")
523 | require.NoError(t, err)
524 | assert.Equal(t, InfoLevel, l)
525 |
526 | l, err = ParseLevel("debug")
527 | require.NoError(t, err)
528 | assert.Equal(t, DebugLevel, l)
529 |
530 | l, err = ParseLevel("DEBUG")
531 | require.NoError(t, err)
532 | assert.Equal(t, DebugLevel, l)
533 |
534 | l, err = ParseLevel("trace")
535 | require.NoError(t, err)
536 | assert.Equal(t, TraceLevel, l)
537 |
538 | l, err = ParseLevel("TRACE")
539 | require.NoError(t, err)
540 | assert.Equal(t, TraceLevel, l)
541 |
542 | _, err = ParseLevel("invalid")
543 | assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
544 | }
545 |
546 | func TestLevelString(t *testing.T) {
547 | var loggerlevel Level = 32000
548 |
549 | _ = loggerlevel.String()
550 | }
551 |
552 | func TestGetSetLevelRace(t *testing.T) {
553 | wg := sync.WaitGroup{}
554 | for i := 0; i < 100; i++ {
555 | wg.Add(1)
556 | go func(i int) {
557 | defer wg.Done()
558 | if i%2 == 0 {
559 | SetLevel(InfoLevel)
560 | } else {
561 | GetLevel()
562 | }
563 | }(i)
564 |
565 | }
566 | wg.Wait()
567 | }
568 |
569 | func TestLoggingRace(t *testing.T) {
570 | logger := New()
571 |
572 | var wg sync.WaitGroup
573 | wg.Add(100)
574 |
575 | for i := 0; i < 100; i++ {
576 | go func() {
577 | logger.Info("info")
578 | wg.Done()
579 | }()
580 | }
581 | wg.Wait()
582 | }
583 |
584 | func TestLoggingRaceWithHooksOnEntry(t *testing.T) {
585 | logger := New()
586 | hook := new(ModifyHook)
587 | logger.AddHook(hook)
588 | entry := logger.WithField("context", "clue")
589 |
590 | var (
591 | wg sync.WaitGroup
592 | mtx sync.Mutex
593 | start bool
594 | )
595 |
596 | cond := sync.NewCond(&mtx)
597 |
598 | wg.Add(100)
599 |
600 | for i := 0; i < 50; i++ {
601 | go func() {
602 | cond.L.Lock()
603 | for !start {
604 | cond.Wait()
605 | }
606 | cond.L.Unlock()
607 | for j := 0; j < 100; j++ {
608 | entry.Info("info")
609 | }
610 | wg.Done()
611 | }()
612 | }
613 |
614 | for i := 0; i < 50; i++ {
615 | go func() {
616 | cond.L.Lock()
617 | for !start {
618 | cond.Wait()
619 | }
620 | cond.L.Unlock()
621 | for j := 0; j < 100; j++ {
622 | entry.WithField("another field", "with some data").Info("info")
623 | }
624 | wg.Done()
625 | }()
626 | }
627 |
628 | cond.L.Lock()
629 | start = true
630 | cond.L.Unlock()
631 | cond.Broadcast()
632 | wg.Wait()
633 | }
634 |
635 | func TestReplaceHooks(t *testing.T) {
636 | old, cur := &TestHook{}, &TestHook{}
637 |
638 | logger := New()
639 | logger.SetOutput(io.Discard)
640 | logger.AddHook(old)
641 |
642 | hooks := make(LevelHooks)
643 | hooks.Add(cur)
644 | replaced := logger.ReplaceHooks(hooks)
645 |
646 | logger.Info("test")
647 |
648 | assert.False(t, old.Fired)
649 | assert.True(t, cur.Fired)
650 |
651 | logger.ReplaceHooks(replaced)
652 | logger.Info("test")
653 | assert.True(t, old.Fired)
654 | }
655 |
656 | // Compile test
657 | func TestLogrusInterfaces(t *testing.T) {
658 | var buffer bytes.Buffer
659 | // This verifies FieldLogger and Ext1FieldLogger work as designed.
660 | // Please don't use them. Use Logger and Entry directly.
661 | fn := func(xl Ext1FieldLogger) {
662 | var l FieldLogger = xl
663 | b := l.WithField("key", "value")
664 | b.Debug("Test")
665 | }
666 | // test logger
667 | logger := New()
668 | logger.Out = &buffer
669 | fn(logger)
670 |
671 | // test Entry
672 | e := logger.WithField("another", "value")
673 | fn(e)
674 | }
675 |
676 | // Implements io.Writer using channels for synchronization, so we can wait on
677 | // the Entry.Writer goroutine to write in a non-racey way. This does assume that
678 | // there is a single call to Logger.Out for each message.
679 | type channelWriter chan []byte
680 |
681 | func (cw channelWriter) Write(p []byte) (int, error) {
682 | cw <- p
683 | return len(p), nil
684 | }
685 |
686 | func TestEntryWriter(t *testing.T) {
687 | cw := channelWriter(make(chan []byte, 1))
688 | log := New()
689 | log.Out = cw
690 | log.Formatter = new(JSONFormatter)
691 | _, err := log.WithField("foo", "bar").WriterLevel(WarnLevel).Write([]byte("hello\n"))
692 | if err != nil {
693 | t.Error("unexecpted error", err)
694 | }
695 |
696 | bs := <-cw
697 | var fields Fields
698 | err = json.Unmarshal(bs, &fields)
699 | require.NoError(t, err)
700 | assert.Equal(t, "bar", fields["foo"])
701 | assert.Equal(t, "warning", fields["level"])
702 | }
703 |
704 | func TestLogLevelEnabled(t *testing.T) {
705 | log := New()
706 | log.SetLevel(PanicLevel)
707 | assert.True(t, log.IsLevelEnabled(PanicLevel))
708 | assert.False(t, log.IsLevelEnabled(FatalLevel))
709 | assert.False(t, log.IsLevelEnabled(ErrorLevel))
710 | assert.False(t, log.IsLevelEnabled(WarnLevel))
711 | assert.False(t, log.IsLevelEnabled(InfoLevel))
712 | assert.False(t, log.IsLevelEnabled(DebugLevel))
713 | assert.False(t, log.IsLevelEnabled(TraceLevel))
714 |
715 | log.SetLevel(FatalLevel)
716 | assert.True(t, log.IsLevelEnabled(PanicLevel))
717 | assert.True(t, log.IsLevelEnabled(FatalLevel))
718 | assert.False(t, log.IsLevelEnabled(ErrorLevel))
719 | assert.False(t, log.IsLevelEnabled(WarnLevel))
720 | assert.False(t, log.IsLevelEnabled(InfoLevel))
721 | assert.False(t, log.IsLevelEnabled(DebugLevel))
722 | assert.False(t, log.IsLevelEnabled(TraceLevel))
723 |
724 | log.SetLevel(ErrorLevel)
725 | assert.True(t, log.IsLevelEnabled(PanicLevel))
726 | assert.True(t, log.IsLevelEnabled(FatalLevel))
727 | assert.True(t, log.IsLevelEnabled(ErrorLevel))
728 | assert.False(t, log.IsLevelEnabled(WarnLevel))
729 | assert.False(t, log.IsLevelEnabled(InfoLevel))
730 | assert.False(t, log.IsLevelEnabled(DebugLevel))
731 | assert.False(t, log.IsLevelEnabled(TraceLevel))
732 |
733 | log.SetLevel(WarnLevel)
734 | assert.True(t, log.IsLevelEnabled(PanicLevel))
735 | assert.True(t, log.IsLevelEnabled(FatalLevel))
736 | assert.True(t, log.IsLevelEnabled(ErrorLevel))
737 | assert.True(t, log.IsLevelEnabled(WarnLevel))
738 | assert.False(t, log.IsLevelEnabled(InfoLevel))
739 | assert.False(t, log.IsLevelEnabled(DebugLevel))
740 | assert.False(t, log.IsLevelEnabled(TraceLevel))
741 |
742 | log.SetLevel(InfoLevel)
743 | assert.True(t, log.IsLevelEnabled(PanicLevel))
744 | assert.True(t, log.IsLevelEnabled(FatalLevel))
745 | assert.True(t, log.IsLevelEnabled(ErrorLevel))
746 | assert.True(t, log.IsLevelEnabled(WarnLevel))
747 | assert.True(t, log.IsLevelEnabled(InfoLevel))
748 | assert.False(t, log.IsLevelEnabled(DebugLevel))
749 | assert.False(t, log.IsLevelEnabled(TraceLevel))
750 |
751 | log.SetLevel(DebugLevel)
752 | assert.True(t, log.IsLevelEnabled(PanicLevel))
753 | assert.True(t, log.IsLevelEnabled(FatalLevel))
754 | assert.True(t, log.IsLevelEnabled(ErrorLevel))
755 | assert.True(t, log.IsLevelEnabled(WarnLevel))
756 | assert.True(t, log.IsLevelEnabled(InfoLevel))
757 | assert.True(t, log.IsLevelEnabled(DebugLevel))
758 | assert.False(t, log.IsLevelEnabled(TraceLevel))
759 |
760 | log.SetLevel(TraceLevel)
761 | assert.True(t, log.IsLevelEnabled(PanicLevel))
762 | assert.True(t, log.IsLevelEnabled(FatalLevel))
763 | assert.True(t, log.IsLevelEnabled(ErrorLevel))
764 | assert.True(t, log.IsLevelEnabled(WarnLevel))
765 | assert.True(t, log.IsLevelEnabled(InfoLevel))
766 | assert.True(t, log.IsLevelEnabled(DebugLevel))
767 | assert.True(t, log.IsLevelEnabled(TraceLevel))
768 | }
769 |
770 | func TestReportCallerOnTextFormatter(t *testing.T) {
771 | l := New()
772 |
773 | l.Formatter.(*TextFormatter).ForceColors = true
774 | l.Formatter.(*TextFormatter).DisableColors = false
775 | l.WithFields(Fields{"func": "func", "file": "file"}).Info("test")
776 |
777 | l.Formatter.(*TextFormatter).ForceColors = false
778 | l.Formatter.(*TextFormatter).DisableColors = true
779 | l.WithFields(Fields{"func": "func", "file": "file"}).Info("test")
780 | }
781 |
782 | func TestSetReportCallerRace(t *testing.T) {
783 | l := New()
784 | l.Out = io.Discard
785 | l.SetReportCaller(true)
786 |
787 | var wg sync.WaitGroup
788 | wg.Add(100)
789 |
790 | for i := 0; i < 100; i++ {
791 | go func() {
792 | l.Error("Some Error")
793 | wg.Done()
794 | }()
795 | }
796 | wg.Wait()
797 | }
798 |
--------------------------------------------------------------------------------