diff --git a/.travis.yml b/.travis.yml index a8f1545..848938a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,52 +1,25 @@ language: go go_import_path: github.com/sirupsen/logrus +git: + depth: 1 env: - - GOMAXPROCS=4 GORACE=halt_on_error=1 + - GO111MODULE=on + - GO111MODULE=off +go: [ 1.11.x, 1.12.x ] +os: [ linux, osx ] matrix: - include: - - go: 1.10.x - install: - - go get github.com/stretchr/testify/assert - - go get golang.org/x/crypto/ssh/terminal - - go get golang.org/x/sys/unix - - go get golang.org/x/sys/windows - script: - - go test -race -v ./... - - go: 1.11.x - env: GO111MODULE=on - install: - - go mod download - script: - - go test -race -v ./... - - go: 1.11.x + exclude: + - go: 1.12.x env: GO111MODULE=off - install: - - go get github.com/stretchr/testify/assert - - go get golang.org/x/crypto/ssh/terminal - - go get golang.org/x/sys/unix - - go get golang.org/x/sys/windows - script: - - go test -race -v ./... - - go: 1.10.x - install: - - go get github.com/stretchr/testify/assert - - go get golang.org/x/crypto/ssh/terminal - - go get golang.org/x/sys/unix - - go get golang.org/x/sys/windows - script: - - go test -race -v -tags appengine ./... - go: 1.11.x - env: GO111MODULE=on - install: - - go mod download - script: - - go test -race -v -tags appengine ./... - - go: 1.11.x - env: GO111MODULE=off - install: - - go get github.com/stretchr/testify/assert - - go get golang.org/x/crypto/ssh/terminal - - go get golang.org/x/sys/unix - - go get golang.org/x/sys/windows - script: - - go test -race -v -tags appengine ./... + os: osx +install: + - ./travis/install.sh + - if [[ "$GO111MODULE" == "on" ]]; then go mod download; fi + - if [[ "$GO111MODULE" == "off" ]]; then go get github.com/stretchr/testify/assert golang.org/x/sys/unix github.com/konsorten/go-windows-terminal-sequences; fi +script: + - ./travis/cross_build.sh + - export GOMAXPROCS=4 + - export GORACE=halt_on_error=1 + - go test -race -v ./... + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then go test -race -v -tags appengine ./... ; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 9978b41..51a7ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.4.2 + * Fixes build break for plan9, nacl, solaris +# 1.4.1 +This new release introduces: + * Enhance TextFormatter to not print caller information when they are empty (#944) + * Remove dependency on golang.org/x/crypto (#932, #943) + +Fixes: + * Fix Entry.WithContext method to return a copy of the initial entry (#941) + # 1.4.0 This new release introduces: * Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848). diff --git a/README.md b/README.md index a4796eb..7aa7bab 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,6 @@ comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437). For an in-depth explanation of the casing issue, see [this comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276). -**Are you interested in assisting in maintaining Logrus?** Currently I have a -lot of obligations, and I am unable to provide Logrus with the maintainership it -needs. If you'd like to help, please reach out to me at `simon at author's -username dot com`. - Nicely color-coded in development (when a TTY is attached, otherwise just plain text): @@ -187,7 +182,7 @@ func main() { log.Out = os.Stdout // You could set this to any `io.Writer` such as a file - // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666) + // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) // if err == nil { // log.Out = file // } else { @@ -354,6 +349,7 @@ The built-in logging formatters are: [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable). * When colors are enabled, levels are truncated to 4 characters by default. To disable truncation set the `DisableLevelTruncation` field to `true`. + * 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. * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter). * `logrus.JSONFormatter`. Logs fields as JSON. * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter). diff --git a/entry.go b/entry.go index 7e8ad25..ba5087b 100644 --- a/entry.go +++ b/entry.go @@ -108,8 +108,7 @@ func (entry *Entry) WithError(err error) *Entry { // Add a context to the Entry. func (entry *Entry) WithContext(ctx context.Context) *Entry { - entry.Context = ctx - return entry + return &Entry{Logger: entry.Logger, Data: entry.Data, Time: entry.Time, err: entry.err, Context: ctx} } // Add a single field to the Entry. diff --git a/go.mod b/go.mod index 94574cc..12fdf98 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,5 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.2.2 - golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 - golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 + golang.org/x/sys v0.0.0-20190422165155-953cdadca894 ) diff --git a/go.sum b/go.sum index 133d34a..596c318 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -9,7 +10,7 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/terminal_check_aix.go b/terminal_check_aix.go deleted file mode 100644 index 04fdb7b..0000000 --- a/terminal_check_aix.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !appengine,!js,!windows,aix - -package logrus - -import "io" - -func checkIfTerminal(w io.Writer) bool { - return false -} diff --git a/terminal_check_bsd.go b/terminal_check_bsd.go new file mode 100644 index 0000000..3c4f43f --- /dev/null +++ b/terminal_check_bsd.go @@ -0,0 +1,13 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +package logrus + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA + +func isTerminal(fd int) bool { + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil +} + diff --git a/terminal_check_js.go b/terminal_check_no_terminal.go similarity index 79% rename from terminal_check_js.go rename to terminal_check_no_terminal.go index 0c20975..97af92c 100644 --- a/terminal_check_js.go +++ b/terminal_check_no_terminal.go @@ -1,4 +1,4 @@ -// +build js +// +build js nacl plan9 package logrus diff --git a/terminal_check_notappengine.go b/terminal_check_notappengine.go index d465565..3293fb3 100644 --- a/terminal_check_notappengine.go +++ b/terminal_check_notappengine.go @@ -1,18 +1,16 @@ -// +build !appengine,!js,!windows,!aix +// +build !appengine,!js,!windows,!nacl,!plan9 package logrus import ( "io" "os" - - "golang.org/x/crypto/ssh/terminal" ) func checkIfTerminal(w io.Writer) bool { switch v := w.(type) { case *os.File: - return terminal.IsTerminal(int(v.Fd())) + return isTerminal(int(v.Fd())) default: return false } diff --git a/terminal_check_solaris.go b/terminal_check_solaris.go new file mode 100644 index 0000000..f6710b3 --- /dev/null +++ b/terminal_check_solaris.go @@ -0,0 +1,11 @@ +package logrus + +import ( + "golang.org/x/sys/unix" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +func isTerminal(fd int) bool { + _, err := unix.IoctlGetTermio(fd, unix.TCGETA) + return err == nil +} diff --git a/terminal_check_unix.go b/terminal_check_unix.go new file mode 100644 index 0000000..355dc96 --- /dev/null +++ b/terminal_check_unix.go @@ -0,0 +1,13 @@ +// +build linux aix + +package logrus + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS + +func isTerminal(fd int) bool { + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil +} + diff --git a/terminal_check_windows.go b/terminal_check_windows.go index 3b9d286..572889d 100644 --- a/terminal_check_windows.go +++ b/terminal_check_windows.go @@ -6,15 +6,29 @@ import ( "io" "os" "syscall" + + sequences "github.com/konsorten/go-windows-terminal-sequences" ) +func initTerminal(w io.Writer) { + switch v := w.(type) { + case *os.File: + sequences.EnableVirtualTerminalProcessing(syscall.Handle(v.Fd()), true) + } +} + func checkIfTerminal(w io.Writer) bool { + var ret bool switch v := w.(type) { case *os.File: var mode uint32 err := syscall.GetConsoleMode(syscall.Handle(v.Fd()), &mode) - return err == nil + ret = (err == nil) default: - return false + ret = false } + if ret { + initTerminal(w) + } + return ret } diff --git a/terminal_notwindows.go b/terminal_notwindows.go deleted file mode 100644 index 3dbd237..0000000 --- a/terminal_notwindows.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !windows - -package logrus - -import "io" - -func initTerminal(w io.Writer) { -} diff --git a/terminal_windows.go b/terminal_windows.go deleted file mode 100644 index b4ef528..0000000 --- a/terminal_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !appengine,!js,windows - -package logrus - -import ( - "io" - "os" - "syscall" - - sequences "github.com/konsorten/go-windows-terminal-sequences" -) - -func initTerminal(w io.Writer) { - switch v := w.(type) { - case *os.File: - sequences.EnableVirtualTerminalProcessing(syscall.Handle(v.Fd()), true) - } -} diff --git a/text_formatter.go b/text_formatter.go index b3b1a30..f08563e 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -6,9 +6,11 @@ import ( "os" "runtime" "sort" + "strconv" "strings" "sync" "time" + "unicode/utf8" ) const ( @@ -57,6 +59,10 @@ type TextFormatter struct { // Disables the truncation of the level text to 4 characters. DisableLevelTruncation bool + // PadLevelText Adds padding the level text so that all the levels output at the same length + // PadLevelText is a superset of the DisableLevelTruncation option + PadLevelText bool + // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool @@ -73,20 +79,26 @@ type TextFormatter struct { FieldMap FieldMap // CallerPrettyfier can be set by the user to modify the content - // of the function and file keys in the json data when ReportCaller is + // of the function and file keys in the data when ReportCaller is // activated. If any of the returned value is the empty string the - // corresponding key will be removed from json fields. + // corresponding key will be removed from fields. CallerPrettyfier func(*runtime.Frame) (function string, file string) terminalInitOnce sync.Once + + // The max length of the level text, generated dynamically on init + levelTextMaxLength int } func (f *TextFormatter) init(entry *Entry) { if entry.Logger != nil { f.isTerminal = checkIfTerminal(entry.Logger.Out) - - if f.isTerminal { - initTerminal(entry.Logger.Out) + } + // Get the max length of the level text + for _, level := range AllLevels { + levelTextLength := utf8.RuneCount([]byte(level.String())) + if levelTextLength > f.levelTextMaxLength { + f.levelTextMaxLength = levelTextLength } } } @@ -133,14 +145,19 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) } if entry.HasCaller() { - fixedKeys = append(fixedKeys, - f.FieldMap.resolve(FieldKeyFunc), f.FieldMap.resolve(FieldKeyFile)) if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } else { funcVal = entry.Caller.Function fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) } + + if funcVal != "" { + fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc)) + } + if fileVal != "" { + fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile)) + } } if !f.DisableSorting { @@ -216,16 +233,24 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin } levelText := strings.ToUpper(entry.Level.String()) - if !f.DisableLevelTruncation { + if !f.DisableLevelTruncation && !f.PadLevelText { levelText = levelText[0:4] } + if f.PadLevelText { + // Generates the format string used in the next line, for example "%-6s" or "%-7s". + // Based on the max level text length. + formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s" + // Formats the level text by appending spaces up to the max length, for example: + // - "INFO " + // - "WARNING" + levelText = fmt.Sprintf(formatString, levelText) + } // Remove a single newline if it already exists in the message to keep // the behavior of logrus text_formatter the same as the stdlib log package entry.Message = strings.TrimSuffix(entry.Message, "\n") caller := "" - if entry.HasCaller() { funcVal := fmt.Sprintf("%s()", entry.Caller.Function) fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) @@ -233,7 +258,14 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } - caller = fileVal + " " + funcVal + + if fileVal == "" { + caller = funcVal + } else if funcVal == "" { + caller = fileVal + } else { + caller = fileVal + " " + funcVal + } } if f.DisableTimestamp { diff --git a/text_formatter_test.go b/text_formatter_test.go index 9c5e6f0..99b5d8c 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -172,6 +172,97 @@ func TestDisableLevelTruncation(t *testing.T) { checkDisableTruncation(false, InfoLevel) } +func TestPadLevelText(t *testing.T) { + // A note for future maintainers / committers: + // + // This test denormalizes the level text as a part of its assertions. + // Because of that, its not really a "unit test" of the PadLevelText functionality. + // So! Many apologies to the potential future person who has to rewrite this test + // when they are changing some completely unrelated functionality. + params := []struct { + name string + level Level + paddedLevelText string + }{ + { + name: "PanicLevel", + level: PanicLevel, + paddedLevelText: "PANIC ", // 2 extra spaces + }, + { + name: "FatalLevel", + level: FatalLevel, + paddedLevelText: "FATAL ", // 2 extra spaces + }, + { + name: "ErrorLevel", + level: ErrorLevel, + paddedLevelText: "ERROR ", // 2 extra spaces + }, + { + name: "WarnLevel", + level: WarnLevel, + // WARNING is already the max length, so we don't need to assert a paddedLevelText + }, + { + name: "DebugLevel", + level: DebugLevel, + paddedLevelText: "DEBUG ", // 2 extra spaces + }, + { + name: "TraceLevel", + level: TraceLevel, + paddedLevelText: "TRACE ", // 2 extra spaces + }, + { + name: "InfoLevel", + level: InfoLevel, + paddedLevelText: "INFO ", // 3 extra spaces + }, + } + + // We create a "default" TextFormatter to do a control test. + // We also create a TextFormatter with PadLevelText, which is the parameter we want to do our most relevant assertions against. + tfDefault := TextFormatter{} + tfWithPadding := TextFormatter{PadLevelText: true} + + for _, val := range params { + t.Run(val.name, func(t *testing.T) { + // TextFormatter writes into these bytes.Buffers, and we make assertions about their contents later + var bytesDefault bytes.Buffer + var bytesWithPadding bytes.Buffer + + // The TextFormatter instance and the bytes.Buffer instance are different here + // all the other arguments are the same. We also initialize them so that they + // fill in the value of levelTextMaxLength. + tfDefault.init(&Entry{}) + tfDefault.printColored(&bytesDefault, &Entry{Level: val.level}, []string{}, nil, "") + tfWithPadding.init(&Entry{}) + tfWithPadding.printColored(&bytesWithPadding, &Entry{Level: val.level}, []string{}, nil, "") + + // turn the bytes back into a string so that we can actually work with the data + logLineDefault := (&bytesDefault).String() + logLineWithPadding := (&bytesWithPadding).String() + + // Control: the level text should not be padded by default + if val.paddedLevelText != "" && strings.Contains(logLineDefault, val.paddedLevelText) { + t.Errorf("log line %q should not contain the padded level text %q by default", logLineDefault, val.paddedLevelText) + } + + // Assertion: the level text should still contain the string representation of the level + if !strings.Contains(strings.ToLower(logLineWithPadding), val.level.String()) { + t.Errorf("log line %q should contain the level text %q when padding is enabled", logLineWithPadding, val.level.String()) + } + + // Assertion: the level text should be in its padded form now + if val.paddedLevelText != "" && !strings.Contains(logLineWithPadding, val.paddedLevelText) { + t.Errorf("log line %q should contain the padded level text %q when padding is enabled", logLineWithPadding, val.paddedLevelText) + } + + }) + } +} + func TestDisableTimestampWithColoredOutput(t *testing.T) { tf := &TextFormatter{DisableTimestamp: true, ForceColors: true} diff --git a/travis/cross_build.sh b/travis/cross_build.sh new file mode 100755 index 0000000..545d8c3 --- /dev/null +++ b/travis/cross_build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +if [[ "$TRAVIS_GO_VERSION" =~ ^1.\12\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + /tmp/gox/gox -build-lib -all +fi diff --git a/travis/install.sh b/travis/install.sh new file mode 100755 index 0000000..07f4532 --- /dev/null +++ b/travis/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +if [[ "$TRAVIS_GO_VERSION" =~ ^1.\12\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + git clone https://github.com/dgsb/gox.git /tmp/gox + pushd /tmp/gox + git checkout new_master + go build ./ + popd +fi