From 6637ba60cde50cec11022cd1ab40d7c88a9ca86c Mon Sep 17 00:00:00 2001
From: Drew Parsons <dparsons@debian.org>
Date: Wed, 9 Nov 2022 18:07:28 +0100
Subject: [PATCH] New upstream version 1.9.0

---
 .github/stale.yml            |  20 ------
 .github/workflows/ci.yaml    |  61 +++++++++++++++++
 .github/workflows/stale.yaml |  22 +++++++
 .travis.yml                  |  14 ++--
 CHANGELOG.md                 |  36 ++++++++++
 README.md                    |   6 +-
 buffer_pool.go               |   9 ---
 ci/go.mod                    |   5 ++
 ci/go.sum                    |   2 +
 ci/mage.go                   |  10 +++
 ci/magefile.go               | 123 +++++++++++++++++++++++++++++++++++
 entry.go                     |  86 ++++++++++++++----------
 entry_test.go                |  47 ++++++++++++-
 go.mod                       |   5 +-
 go.sum                       |  16 +++--
 hook_test.go                 |  16 +++++
 json_formatter.go            |   5 +-
 logger.go                    |  15 ++++-
 logger_test.go               |  30 ++++++++-
 logrus_test.go               |  39 ++++++++++-
 terminal_check_unix.go       |   2 +-
 text_formatter.go            |   7 +-
 travis/install.sh            |  20 +-----
 travis/lint.sh               |   5 --
 24 files changed, 488 insertions(+), 113 deletions(-)
 delete mode 100644 .github/stale.yml
 create mode 100644 .github/workflows/ci.yaml
 create mode 100644 .github/workflows/stale.yaml
 create mode 100644 ci/go.mod
 create mode 100644 ci/go.sum
 create mode 100644 ci/mage.go
 create mode 100644 ci/magefile.go
 delete mode 100755 travis/lint.sh

diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 27c3cf9d..00000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 60
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 14
-# Issues with these labels will never be considered stale
-exemptLabels:
-  - question
-  - bug
-  - documentation
-# Label to use when marking an issue as stale
-staleLabel: stale
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
-  This issue has been automatically marked as stale because it has not had
-  recent activity. It will be closed if no further activity occurs. Thank you
-  for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
-# Only mark issues as stale for now until we go through backlog of PRs
-only: issues
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 00000000..66f76aed
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,61 @@
+name: CI
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+jobs:
+
+  lint:
+    name: Golang-CI Lint
+    timeout-minutes: 10
+    strategy:
+      matrix:
+        platform: [ubuntu-latest]
+    runs-on: ${{ matrix.platform }}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: golangci/golangci-lint-action@v2
+        with:
+          # must be specified without patch version
+          version: v1.46
+  cross:
+    name: Cross
+    timeout-minutes: 10
+    strategy:
+      matrix:
+        go-version: [1.17.x]
+        platform: [ubuntu-latest]
+    runs-on: ${{ matrix.platform }}
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go-version }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Cross
+        working-directory: ci
+        run: go run mage.go -v -w ../ crossBuild
+
+  test:
+    name: Unit test
+    timeout-minutes: 10
+    strategy:
+      matrix:
+        go-version: [1.17.x]
+        platform: [ubuntu-latest, windows-latest]
+    runs-on: ${{ matrix.platform }}
+    steps:
+    - name: Install Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ matrix.go-version }}
+    - name: Checkout code
+      uses: actions/checkout@v2
+    - name: Test
+      run: go test -race -v ./...
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 00000000..246c34d3
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,22 @@
+name: Close inactive issues
+on:
+  schedule:
+    - cron: "30 1 * * *"
+
+jobs:
+  close-issues:
+    runs-on: ubuntu-latest
+    permissions:
+      issues: write
+      pull-requests: write
+    steps:
+      - uses: actions/stale@v3
+        with:
+          days-before-issue-stale: 30
+          days-before-issue-close: 14
+          stale-issue-label: "stale"
+          stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
+          close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
+          days-before-pr-stale: -1
+          days-before-pr-close: -1
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.travis.yml b/.travis.yml
index 5e20aa41..c1dbd5a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,14 +4,12 @@ git:
   depth: 1
 env:
   - GO111MODULE=on
-go: [1.13.x, 1.14.x]
-os: [linux, osx]
+go: 1.15.x
+os: linux
 install:
   - ./travis/install.sh
 script:
-  - ./travis/cross_build.sh
-  - ./travis/lint.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
+  - cd ci
+  - go run mage.go -v -w ../ crossBuild
+  - go run mage.go -v -w ../ lint
+  - go run mage.go -v -w ../ test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 584026d6..7567f612 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
+# 1.8.1
+Code quality:
+  * move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
+  * improve timestamp format documentation
+
+Fixes:
+  * fix race condition on logger hooks
+
+
+# 1.8.0
+
+Correct versioning number replacing v1.7.1.
+
+# 1.7.1
+
+Beware this release has introduced a new public API and its semver is therefore incorrect.
+
+Code quality:
+  * use go 1.15 in travis
+  * use magefile as task runner
+
+Fixes:
+  * small fixes about new go 1.13 error formatting system
+  * Fix for long time race condiction with mutating data hooks
+
+Features:
+  * build support for zos
+
+# 1.7.0
+Fixes:
+  * the dependency toward a windows terminal library has been removed
+
+Features:
+  * a new buffer pool management API has been added
+  * a set of `<LogLevel>Fn()` functions have been added
+
 # 1.6.0
 Fixes:
   * end of line cleanup
diff --git a/README.md b/README.md
index 5796706d..b042c896 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus)
+# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://github.com/sirupsen/logrus/workflows/CI/badge.svg)](https://github.com/sirupsen/logrus/actions?query=workflow%3ACI) [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![Go Reference](https://pkg.go.dev/badge/github.com/sirupsen/logrus.svg)](https://pkg.go.dev/github.com/sirupsen/logrus)
 
 Logrus is a structured logger for Go (golang), completely API compatible with
 the standard library logger.
@@ -341,7 +341,7 @@ import (
   log "github.com/sirupsen/logrus"
 )
 
-init() {
+func init() {
   // do something here to set environment depending on an environment variable
   // or command-line flag
   if Environment == "production" {
@@ -402,7 +402,7 @@ func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
   // source of the official loggers.
   serialized, err := json.Marshal(entry.Data)
     if err != nil {
-      return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
+      return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
     }
   return append(serialized, '\n'), nil
 }
diff --git a/buffer_pool.go b/buffer_pool.go
index 4545dec0..c7787f77 100644
--- a/buffer_pool.go
+++ b/buffer_pool.go
@@ -26,15 +26,6 @@ func (p *defaultPool) Get() *bytes.Buffer {
 	return p.pool.Get().(*bytes.Buffer)
 }
 
-func getBuffer() *bytes.Buffer {
-	return bufferPool.Get()
-}
-
-func putBuffer(buf *bytes.Buffer) {
-	buf.Reset()
-	bufferPool.Put(buf)
-}
-
 // SetBufferPool allows to replace the default logrus buffer pool
 // to better meets the specific needs of an application.
 func SetBufferPool(bp BufferPool) {
diff --git a/ci/go.mod b/ci/go.mod
new file mode 100644
index 00000000..e895e95a
--- /dev/null
+++ b/ci/go.mod
@@ -0,0 +1,5 @@
+module github.com/sirupsen/logrus/ci
+
+go 1.15
+
+require github.com/magefile/mage v1.11.0
diff --git a/ci/go.sum b/ci/go.sum
new file mode 100644
index 00000000..edf273b8
--- /dev/null
+++ b/ci/go.sum
@@ -0,0 +1,2 @@
+github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
+github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
diff --git a/ci/mage.go b/ci/mage.go
new file mode 100644
index 00000000..4273031d
--- /dev/null
+++ b/ci/mage.go
@@ -0,0 +1,10 @@
+// +build ignore
+
+package main
+
+import (
+	"github.com/magefile/mage/mage"
+	"os"
+)
+
+func main() { os.Exit(mage.Main()) }
diff --git a/ci/magefile.go b/ci/magefile.go
new file mode 100644
index 00000000..ceda305c
--- /dev/null
+++ b/ci/magefile.go
@@ -0,0 +1,123 @@
+//go:build mage
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path"
+	"sort"
+
+	"github.com/magefile/mage/mg"
+	"github.com/magefile/mage/sh"
+)
+
+func intersect(a, b []string) []string {
+	sort.Strings(a)
+	sort.Strings(b)
+
+	res := make([]string, 0, func() int {
+		if len(a) < len(b) {
+			return len(a)
+		}
+		return len(b)
+	}())
+
+	for _, v := range a {
+		idx := sort.SearchStrings(b, v)
+		if idx < len(b) && b[idx] == v {
+			res = append(res, v)
+		}
+	}
+	return res
+}
+
+// getBuildMatrix returns the build matrix from the current version of the go compiler
+func getFullBuildMatrix() (map[string][]string, error) {
+	jsonData, err := sh.Output("go", "tool", "dist", "list", "-json")
+	if err != nil {
+		return nil, err
+	}
+	var data []struct {
+		Goos   string
+		Goarch string
+	}
+	if err := json.Unmarshal([]byte(jsonData), &data); err != nil {
+		return nil, err
+	}
+
+	matrix := map[string][]string{}
+	for _, v := range data {
+		if val, ok := matrix[v.Goos]; ok {
+			matrix[v.Goos] = append(val, v.Goarch)
+		} else {
+			matrix[v.Goos] = []string{v.Goarch}
+		}
+	}
+
+	return matrix, nil
+}
+
+func getBuildMatrix() (map[string][]string, error) {
+	minimalMatrix := map[string][]string{
+		"linux":   []string{"amd64"},
+		"darwin":  []string{"amd64", "arm64"},
+		"freebsd": []string{"amd64"},
+		"js":      []string{"wasm"},
+		"solaris": []string{"amd64"},
+		"windows": []string{"amd64", "arm64"},
+	}
+
+	fullMatrix, err := getFullBuildMatrix()
+	if err != nil {
+		return nil, err
+	}
+
+	for os, arches := range minimalMatrix {
+		if fullV, ok := fullMatrix[os]; !ok {
+			delete(minimalMatrix, os)
+		} else {
+			minimalMatrix[os] = intersect(arches, fullV)
+		}
+	}
+	return minimalMatrix, nil
+}
+
+func CrossBuild() error {
+	matrix, err := getBuildMatrix()
+	if err != nil {
+		return err
+	}
+
+	for os, arches := range matrix {
+		for _, arch := range arches {
+			env := map[string]string{
+				"GOOS":   os,
+				"GOARCH": arch,
+			}
+			if mg.Verbose() {
+				fmt.Printf("Building for GOOS=%s GOARCH=%s\n", os, arch)
+			}
+			if err := sh.RunWith(env, "go", "build", "./..."); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func Lint() error {
+	gopath := os.Getenv("GOPATH")
+	if gopath == "" {
+		return fmt.Errorf("cannot retrieve GOPATH")
+	}
+
+	return sh.Run(path.Join(gopath, "bin", "golangci-lint"), "run", "./...")
+}
+
+// Run the test suite
+func Test() error {
+	return sh.RunWith(map[string]string{"GORACE": "halt_on_error=1"},
+		"go", "test", "-race", "-v", "./...")
+}
diff --git a/entry.go b/entry.go
index 5a5cbfe7..71cdbbc3 100644
--- a/entry.go
+++ b/entry.go
@@ -78,6 +78,14 @@ func NewEntry(logger *Logger) *Entry {
 	}
 }
 
+func (entry *Entry) Dup() *Entry {
+	data := make(Fields, len(entry.Data))
+	for k, v := range entry.Data {
+		data[k] = v
+	}
+	return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
+}
+
 // Returns the bytes representation of this entry from the formatter.
 func (entry *Entry) Bytes() ([]byte, error) {
 	return entry.Logger.Formatter.Format(entry)
@@ -123,11 +131,9 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
 	for k, v := range fields {
 		isErrField := false
 		if t := reflect.TypeOf(v); t != nil {
-			switch t.Kind() {
-			case reflect.Func:
+			switch {
+			case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
 				isErrField = true
-			case reflect.Ptr:
-				isErrField = t.Elem().Kind() == reflect.Func
 			}
 		}
 		if isErrField {
@@ -212,54 +218,66 @@ func (entry Entry) HasCaller() (has bool) {
 		entry.Caller != nil
 }
 
-// This function is not declared with a pointer value because otherwise
-// race conditions will occur when using multiple goroutines
-func (entry Entry) log(level Level, msg string) {
+func (entry *Entry) log(level Level, msg string) {
 	var buffer *bytes.Buffer
 
-	// Default to now, but allow users to override if they want.
-	//
-	// We don't have to worry about polluting future calls to Entry#log()
-	// with this assignment because this function is declared with a
-	// non-pointer receiver.
-	if entry.Time.IsZero() {
-		entry.Time = time.Now()
-	}
+	newEntry := entry.Dup()
 
-	entry.Level = level
-	entry.Message = msg
-	entry.Logger.mu.Lock()
-	if entry.Logger.ReportCaller {
-		entry.Caller = getCaller()
+	if newEntry.Time.IsZero() {
+		newEntry.Time = time.Now()
 	}
-	entry.Logger.mu.Unlock()
 
-	entry.fireHooks()
+	newEntry.Level = level
+	newEntry.Message = msg
+
+	newEntry.Logger.mu.Lock()
+	reportCaller := newEntry.Logger.ReportCaller
+	bufPool := newEntry.getBufferPool()
+	newEntry.Logger.mu.Unlock()
 
-	buffer = getBuffer()
+	if reportCaller {
+		newEntry.Caller = getCaller()
+	}
+
+	newEntry.fireHooks()
+	buffer = bufPool.Get()
 	defer func() {
-		entry.Buffer = nil
-		putBuffer(buffer)
+		newEntry.Buffer = nil
+		buffer.Reset()
+		bufPool.Put(buffer)
 	}()
 	buffer.Reset()
-	entry.Buffer = buffer
+	newEntry.Buffer = buffer
 
-	entry.write()
+	newEntry.write()
 
-	entry.Buffer = nil
+	newEntry.Buffer = nil
 
 	// To avoid Entry#log() returning a value that only would make sense for
 	// panic() to use in Entry#Panic(), we avoid the allocation by checking
 	// directly here.
 	if level <= PanicLevel {
-		panic(&entry)
+		panic(newEntry)
+	}
+}
+
+func (entry *Entry) getBufferPool() (pool BufferPool) {
+	if entry.Logger.BufferPool != nil {
+		return entry.Logger.BufferPool
 	}
+	return bufferPool
 }
 
 func (entry *Entry) fireHooks() {
+	var tmpHooks LevelHooks
 	entry.Logger.mu.Lock()
-	defer entry.Logger.mu.Unlock()
-	err := entry.Logger.Hooks.Fire(entry.Level, entry)
+	tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
+	for k, v := range entry.Logger.Hooks {
+		tmpHooks[k] = v
+	}
+	entry.Logger.mu.Unlock()
+
+	err := tmpHooks.Fire(entry.Level, entry)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
 	}
@@ -273,11 +291,14 @@ func (entry *Entry) write() {
 		fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
 		return
 	}
-	if _, err = entry.Logger.Out.Write(serialized); err != nil {
+	if _, err := entry.Logger.Out.Write(serialized); err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
 	}
 }
 
+// Log will log a message at the level given as parameter.
+// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
+// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
 func (entry *Entry) Log(level Level, args ...interface{}) {
 	if entry.Logger.IsLevelEnabled(level) {
 		entry.log(level, fmt.Sprint(args...))
@@ -319,7 +340,6 @@ func (entry *Entry) Fatal(args ...interface{}) {
 
 func (entry *Entry) Panic(args ...interface{}) {
 	entry.Log(PanicLevel, args...)
-	panic(fmt.Sprint(args...))
 }
 
 // Entry Printf family functions
diff --git a/entry_test.go b/entry_test.go
index b98e14ad..41c47a2f 100644
--- a/entry_test.go
+++ b/entry_test.go
@@ -167,6 +167,28 @@ func TestEntryPanicf(t *testing.T) {
 	entry.WithField("err", errBoom).Panicf("kaboom %v", true)
 }
 
+func TestEntryPanic(t *testing.T) {
+	errBoom := fmt.Errorf("boom again")
+
+	defer func() {
+		p := recover()
+		assert.NotNil(t, p)
+
+		switch pVal := p.(type) {
+		case *Entry:
+			assert.Equal(t, "kaboom", pVal.Message)
+			assert.Equal(t, errBoom, pVal.Data["err"])
+		default:
+			t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
+		}
+	}()
+
+	logger := New()
+	logger.Out = &bytes.Buffer{}
+	entry := NewEntry(logger)
+	entry.WithField("err", errBoom).Panic("kaboom")
+}
+
 const (
 	badMessage   = "this is going to panic"
 	panicMessage = "this is broken"
@@ -210,7 +232,7 @@ func TestEntryWithIncorrectField(t *testing.T) {
 
 	fn := func() {}
 
-	e := Entry{}
+	e := Entry{Logger: New()}
 	eWithFunc := e.WithFields(Fields{"func": fn})
 	eWithFuncPtr := e.WithFields(Fields{"funcPtr": &fn})
 
@@ -247,6 +269,12 @@ func TestEntryLogfLevel(t *testing.T) {
 func TestEntryReportCallerRace(t *testing.T) {
 	logger := New()
 	entry := NewEntry(logger)
+
+	// logging before SetReportCaller has the highest chance of causing a race condition
+	// to be detected, but doing it twice just to increase the likelyhood of detecting the race
+	go func() {
+		entry.Info("should not race")
+	}()
 	go func() {
 		logger.SetReportCaller(true)
 	}()
@@ -254,3 +282,20 @@ func TestEntryReportCallerRace(t *testing.T) {
 		entry.Info("should not race")
 	}()
 }
+
+func TestEntryFormatterRace(t *testing.T) {
+	logger := New()
+	entry := NewEntry(logger)
+
+	// logging before SetReportCaller has the highest chance of causing a race condition
+	// to be detected, but doing it twice just to increase the likelyhood of detecting the race
+	go func() {
+		entry.Info("should not race")
+	}()
+	go func() {
+		logger.SetFormatter(&TextFormatter{})
+	}()
+	go func() {
+		entry.Info("should not race")
+	}()
+}
diff --git a/go.mod b/go.mod
index b3919d5e..8b3f6d37 100644
--- a/go.mod
+++ b/go.mod
@@ -2,9 +2,8 @@ module github.com/sirupsen/logrus
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/stretchr/testify v1.2.2
-	golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
+	github.com/stretchr/testify v1.7.0
+	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index 1edc143b..e5fdc85b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,14 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-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/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hook_test.go b/hook_test.go
index b5cf077b..a2becc82 100644
--- a/hook_test.go
+++ b/hook_test.go
@@ -3,6 +3,7 @@ package logrus_test
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"sync"
 	"testing"
 
@@ -10,6 +11,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	. "github.com/sirupsen/logrus"
+	"github.com/sirupsen/logrus/hooks/test"
 	. "github.com/sirupsen/logrus/internal/testutils"
 )
 
@@ -191,6 +193,20 @@ func TestAddHookRace(t *testing.T) {
 	})
 }
 
+func TestAddHookRace2(t *testing.T) {
+	t.Parallel()
+
+	for i := 0; i < 3; i++ {
+		testname := fmt.Sprintf("Test %d", i)
+		t.Run(testname, func(t *testing.T) {
+			t.Parallel()
+
+			_ = test.NewGlobal()
+			Info(testname)
+		})
+	}
+}
+
 type HookCallFunc struct {
 	F func()
 }
diff --git a/json_formatter.go b/json_formatter.go
index ba7f2371..c96dc563 100644
--- a/json_formatter.go
+++ b/json_formatter.go
@@ -23,6 +23,9 @@ func (f FieldMap) resolve(key fieldKey) string {
 // JSONFormatter formats logs into parsable json
 type JSONFormatter struct {
 	// TimestampFormat sets the format used for marshaling timestamps.
+	// The format to use is the same than for time.Format or time.Parse from the standard
+	// library.
+	// The standard Library already provides a set of predefined format.
 	TimestampFormat string
 
 	// DisableTimestamp allows disabling automatic timestamps in output
@@ -118,7 +121,7 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
 		encoder.SetIndent("", "  ")
 	}
 	if err := encoder.Encode(data); err != nil {
-		return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err)
+		return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
 	}
 
 	return b.Bytes(), nil
diff --git a/logger.go b/logger.go
index dbf627c9..5ff0aef6 100644
--- a/logger.go
+++ b/logger.go
@@ -12,7 +12,7 @@ import (
 // LogFunction For big messages, it can be more efficient to pass a function
 // and only call it if the log level is actually enables rather than
 // generating the log message and then checking if the level is enabled
-type LogFunction func()[]interface{}
+type LogFunction func() []interface{}
 
 type Logger struct {
 	// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
@@ -44,6 +44,9 @@ type Logger struct {
 	entryPool sync.Pool
 	// Function to exit the application, defaults to `os.Exit()`
 	ExitFunc exitFunc
+	// The buffer pool used to format the log. If it is nil, the default global
+	// buffer pool will be used.
+	BufferPool BufferPool
 }
 
 type exitFunc func(int)
@@ -192,6 +195,9 @@ func (logger *Logger) Panicf(format string, args ...interface{}) {
 	logger.Logf(PanicLevel, format, args...)
 }
 
+// Log will log a message at the level given as parameter.
+// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
+// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
 func (logger *Logger) Log(level Level, args ...interface{}) {
 	if logger.IsLevelEnabled(level) {
 		entry := logger.newEntry()
@@ -402,3 +408,10 @@ func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
 	logger.mu.Unlock()
 	return oldHooks
 }
+
+// SetBufferPool sets the logger buffer pool.
+func (logger *Logger) SetBufferPool(pool BufferPool) {
+	logger.mu.Lock()
+	defer logger.mu.Unlock()
+	logger.BufferPool = pool
+}
diff --git a/logger_test.go b/logger_test.go
index f12a04e7..595c6821 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -25,7 +25,7 @@ func TestFieldValueError(t *testing.T) {
 		t.Error("unexpected error", err)
 	}
 	_, ok := data[FieldKeyLogrusError]
-	require.True(t, ok)
+	require.True(t, ok, `cannot found expected "logrus_error" field: %v`, data)
 }
 
 func TestNoFieldValueError(t *testing.T) {
@@ -67,3 +67,31 @@ func TestWarninglnNotEqualToWarning(t *testing.T) {
 
 	assert.NotEqual(t, buf.String(), bufln.String(), "Warning() and Wantingln() should not be equal")
 }
+
+type testBufferPool struct {
+	buffers []*bytes.Buffer
+	get int
+}
+
+func (p *testBufferPool) Get() *bytes.Buffer {
+	p.get++
+	return new(bytes.Buffer)
+}
+
+func (p *testBufferPool) Put(buf *bytes.Buffer) {
+	p.buffers = append(p.buffers, buf)
+}
+
+func TestLogger_SetBufferPool(t *testing.T) {
+	out := &bytes.Buffer{}
+	l := New()
+	l.SetOutput(out)
+
+	pool := new(testBufferPool)
+	l.SetBufferPool(pool)
+
+	l.Info("test")
+
+	assert.Equal(t, pool.get, 1, "Logger.SetBufferPool(): The BufferPool.Get() must be called")
+	assert.Len(t, pool.buffers, 1, "Logger.SetBufferPool(): The BufferPool.Put() must be called")
+}
diff --git a/logrus_test.go b/logrus_test.go
index 9c3c920d..4edee283 100644
--- a/logrus_test.go
+++ b/logrus_test.go
@@ -588,15 +588,48 @@ func TestLoggingRaceWithHooksOnEntry(t *testing.T) {
 	logger.AddHook(hook)
 	entry := logger.WithField("context", "clue")
 
-	var wg sync.WaitGroup
+	var (
+		wg    sync.WaitGroup
+		mtx   sync.Mutex
+		start bool
+	)
+
+	cond := sync.NewCond(&mtx)
+
 	wg.Add(100)
 
-	for i := 0; i < 100; i++ {
+	for i := 0; i < 50; i++ {
+		go func() {
+			cond.L.Lock()
+			for !start {
+				cond.Wait()
+			}
+			cond.L.Unlock()
+			for j := 0; j < 100; j++ {
+				entry.Info("info")
+			}
+			wg.Done()
+		}()
+	}
+
+	for i := 0; i < 50; i++ {
 		go func() {
-			entry.Info("info")
+			cond.L.Lock()
+			for !start {
+				cond.Wait()
+			}
+			cond.L.Unlock()
+			for j := 0; j < 100; j++ {
+				entry.WithField("another field", "with some data").Info("info")
+			}
 			wg.Done()
 		}()
 	}
+
+	cond.L.Lock()
+	start = true
+	cond.L.Unlock()
+	cond.Broadcast()
 	wg.Wait()
 }
 
diff --git a/terminal_check_unix.go b/terminal_check_unix.go
index cc4fe6e3..04748b85 100644
--- a/terminal_check_unix.go
+++ b/terminal_check_unix.go
@@ -1,4 +1,4 @@
-// +build linux aix
+// +build linux aix zos
 // +build !js
 
 package logrus
diff --git a/text_formatter.go b/text_formatter.go
index 3c28b54c..be2c6efe 100644
--- a/text_formatter.go
+++ b/text_formatter.go
@@ -53,7 +53,10 @@ type TextFormatter struct {
 	// the time passed since beginning of execution.
 	FullTimestamp bool
 
-	// TimestampFormat to use for display when a full timestamp is printed
+	// TimestampFormat to use for display when a full timestamp is printed.
+	// The format to use is the same than for time.Format or time.Parse from the standard
+	// library.
+	// The standard Library already provides a set of predefined format.
 	TimestampFormat string
 
 	// The fields are sorted by default for a consistent output. For applications
@@ -235,6 +238,8 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
 		levelColor = yellow
 	case ErrorLevel, FatalLevel, PanicLevel:
 		levelColor = red
+	case InfoLevel:
+		levelColor = blue
 	default:
 		levelColor = blue
 	}
diff --git a/travis/install.sh b/travis/install.sh
index 5fc40dd8..837c82d0 100755
--- a/travis/install.sh
+++ b/travis/install.sh
@@ -2,21 +2,7 @@
 
 set -e
 
-# Install golanci 1.21.0
-if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]]; then
-    curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.21.0
-fi
-
-# Only do this for go1.12 when modules are on so that it doesn't need to be done when modules are off as well.
-if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$GO111MODULE" == "on" ]]; then
-    GO111MODULE=off go get github.com/dgsb/gox
-fi
-
-if [[ "$GO111MODULE" ==  "on" ]]; then
-    go mod download
-fi
-
-if [[ "$GO111MODULE" == "off" ]]; then
-    # Should contain all regular (not indirect) modules from go.mod
-    go get github.com/stretchr/testify golang.org/x/sys/unix golang.org/x/sys/windows
+# Install golanci 1.32.2
+if [[ "$TRAVIS_GO_VERSION" =~ ^1\.15\. ]]; then
+    curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.32.2
 fi
diff --git a/travis/lint.sh b/travis/lint.sh
deleted file mode 100755
index 0ed1d7c7..00000000
--- a/travis/lint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$GO111MODULE" == "on" ]]; then
-    $(go env GOPATH)/bin/golangci-lint run ./...
-fi
-- 
GitLab