Skip to content

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Dec 11, 2024

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
github.com/sirupsen/logrus v1.3.0 -> v1.9.3 age adoption passing confidence

Release Notes

sirupsen/logrus (github.com/sirupsen/logrus)

v1.9.3

Compare Source

Full Changelog: sirupsen/logrus@v1.9.2...v1.9.3

v1.9.2

Compare Source

Full Changelog: sirupsen/logrus@v1.9.1...v1.9.2

v1.9.1

Compare Source

What's Changed

New Contributors

Full Changelog: sirupsen/logrus@v1.9.0...v1.9.1

v1.9.0

Compare Source

v1.8.3

Compare Source

What's Changed

New Contributors

Full Changelog: sirupsen/logrus@v1.8.2...v1.8.3

v1.8.2

Compare Source

What's Changed

New Contributors

Full Changelog: sirupsen/logrus@v1.8.1...v1.8.2

v1.8.1

Compare Source

v1.8.0

Compare Source

Correct versioning number replacing v1.7.1

v1.7.1

Compare Source

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

v1.7.0: Add new BufferPool and LogFunction APIs

Compare Source

  • a new buffer pool management API has been added
  • a set of <LogLevel>Fn() functions have been added
  • the dependency toward a windows terminal library has been removed

v1.6.0

Compare Source

Release v1.6.0

v1.5.0

Compare Source

This new release introduces:

v1.4.2

Compare Source

v1.4.1

Compare Source

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)

v1.4.0

Compare Source

This new release introduces:

  • Add DeferExitHandler, similar to RegisterExitHandler but prepending the handler to the list of handlers (semantically like defer) (#​848).
  • Add CallerPrettyfier to JSONFormatter and `TextFormatter (#​909, #​911)
  • Add Entry.WithContext() and Entry.Context, to set a context on entries to be used e.g. in hooks (#​919).

Fixes:

  • Fix wrong method calls Logger.Print and Logger.Warningln (#​893).
  • Update Entry.Logf to not do string formatting unless the log level is enabled (#​903)
  • Fix infinite recursion on unknown Level.String() (#​907)
  • Fix race condition in getCaller (#​916).

Configuration

📅 Schedule: Branch creation - "* * * * 2-4" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

Copy link
Contributor Author

renovate bot commented Dec 11, 2024

ℹ Artifact update notice

File name: tools/dleq-test-gen/go.mod

In order to perform the update(s) described in the table above, Renovate ran the go get command, which resulted in the following additional change(s):

  • 1 additional dependency was updated

Details:

Package Change
github.com/tmthrgd/go-rand v0.0.0-20180829004326-9716d99b29d1 -> v0.0.0-20180829004326-9716d99b29d1
File name: tools/oprf-test-gen/go.mod

In order to perform the update(s) described in the table above, Renovate ran the go get command, which resulted in the following additional change(s):

  • 1 additional dependency was updated

Details:

Package Change
github.com/tmthrgd/go-rand v0.0.0-20180829004326-9716d99b29d1 -> v0.0.0-20180829004326-9716d99b29d1

@renovate renovate bot force-pushed the renovate/github.colasdn.workers.dev-sirupsen-logrus-1.x branch from e746f88 to 91e5e16 Compare March 5, 2025 03:40
@renovate renovate bot force-pushed the renovate/github.colasdn.workers.dev-sirupsen-logrus-1.x branch 2 times, most recently from 916c86a to 016f689 Compare March 17, 2025 18:46
@renovate renovate bot changed the title Update module github.com/sirupsen/logrus to v1.9.3 fix(deps): update module github.com/sirupsen/logrus to v1.9.3 Mar 28, 2025
@renovate renovate bot force-pushed the renovate/github.colasdn.workers.dev-sirupsen-logrus-1.x branch from 016f689 to 281af12 Compare April 8, 2025 14:28
Copy link

🚨 Potential security issues detected. Learn more about Socket for GitHub ↗︎

To accept the risk, merge this PR and you will not be notified again.

Alert Package NoteCI
High CVE golang/gopkg.in/[email protected] ⚠︎

View full report↗︎

Next steps

What is a CVE?

Contains a high severity Common Vulnerability and Exposure (CVE).

Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Take a deeper look at the dependency

Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support [AT] socket [DOT] dev.

Remove the package

If you happen to install a dependency that Socket reports as Known Malware you should immediately remove it and select a different dependency. For other alert types, you may may wish to investigate alternative packages or consider if there are other ways to mitigate the specific risk posed by the dependency.

Mark a package as acceptable risk

To ignore an alert, reply with a comment starting with @SocketSecurity ignore followed by a space separated list of ecosystem/package-name@version specifiers. e.g. @SocketSecurity ignore npm/[email protected] or ignore all packages with @SocketSecurity ignore-all

Copy link

github-actions bot commented Apr 8, 2025

[puLL-Merge] - sirupsen/[email protected]

Diff
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 000000000..66f76aeda
--- /dev/null
+++ .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 000000000..246c34d31
--- /dev/null
+++ .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 .gitignore .gitignore
index 6b7d7d1e8..1fb13abeb 100644
--- .gitignore
+++ .gitignore
@@ -1,2 +1,4 @@
 logrus
 vendor
+
+.idea/
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 000000000..65dc28503
--- /dev/null
+++ .golangci.yml
@@ -0,0 +1,40 @@
+run:
+  # do not run on test files yet
+  tests: false
+
+# all available settings of specific linters
+linters-settings:
+  errcheck:
+    # report about not checking of errors in type assetions: `a := b.(MyStruct)`;
+    # default is false: such cases aren't reported by default.
+    check-type-assertions: false
+
+    # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
+    # default is false: such cases aren't reported by default.
+    check-blank: false
+
+  lll:
+    line-length: 100
+    tab-width: 4
+
+  prealloc:
+    simple: false
+    range-loops: false
+    for-loops: false
+
+  whitespace:
+    multi-if: false   # Enforces newlines (or comments) after every multi-line if statement
+    multi-func: false # Enforces newlines (or comments) after every multi-line function signature
+
+linters:
+  enable:
+    - megacheck
+    - govet
+  disable:
+    - maligned
+    - prealloc
+  disable-all: false
+  presets:
+    - bugs
+    - unused
+  fast: false
diff --git .travis.yml .travis.yml
index a8f154515..c1dbd5a3a 100644
--- .travis.yml
+++ .travis.yml
@@ -1,52 +1,15 @@
 language: go
 go_import_path: github.com/sirupsen/logrus
+git:
+  depth: 1
 env:
-  - GOMAXPROCS=4 GORACE=halt_on_error=1
-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
-      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 ./...
+  - GO111MODULE=on
+go: 1.15.x
+os: linux
+install:
+  - ./travis/install.sh
+script:
+  - 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 CHANGELOG.md CHANGELOG.md
index cb85d9f9f..7567f6128 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,3 +1,97 @@
+# 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
+  * revert the entry concurrency bug fix whic leads to deadlock under some circumstances
+  * update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
+
+Features:
+  * add an option to the `TextFormatter` to completely disable fields quoting
+
+# 1.5.0
+Code quality:
+  * add golangci linter run on travis
+
+Fixes:
+  * add mutex for hooks concurrent access on `Entry` data
+  * caller function field for go1.14
+  * fix build issue for gopherjs target
+
+Feature:
+  * add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
+  * add a `DisableHTMLEscape` option in the `JSONFormatter`
+  * add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
+
+# 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).
+  * Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
+  * Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
+
+Fixes:
+  * Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
+  * Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
+  * Fix infinite recursion on unknown `Level.String()` (#907)
+  * Fix race condition in `getCaller` (#916).
+
+
+# 1.3.0
+This new release introduces:
+  * Log, Logf, Logln functions for Logger and Entry that take a Level
+
+Fixes:
+  * Building prometheus node_exporter on AIX (#840)
+  * Race condition in TextFormatter (#468)
+  * Travis CI import path (#868)
+  * Remove coloured output on Windows (#862)
+  * Pointer to func as field in JSONFormatter (#870)
+  * Properly marshal Levels (#873)
+
 # 1.2.0
 This new release introduces:
   * A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
diff --git README.md README.md
index 398731055..d1d4a85fd 100644
--- README.md
+++ README.md
@@ -1,8 +1,28 @@
-# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus)&nbsp;[![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.
 
+**Logrus is in maintenance-mode.** We will not be introducing new features. It's
+simply too hard to do in a way that won't break many people's projects, which is
+the last thing you want from your Logging library (again...).
+
+This does not mean Logrus is dead. Logrus will continue to be maintained for
+security, (backwards compatible) bug fixes, and performance (where we are
+limited by the interface).
+
+I believe Logrus' biggest contribution is to have played a part in today's
+widespread use of structured logging in Golang. There doesn't seem to be a
+reason to do a major, breaking iteration into Logrus V2, since the fantastic Go
+community has built those independently. Many fantastic alternatives have sprung
+up. Logrus would look like those, had it been re-designed with what we know
+about structured logging in Go today. Check out, for example,
+[Zerolog][zerolog], [Zap][zap], and [Apex][apex].
+
+[zerolog]: https://github.com/rs/zerolog
+[zap]: https://github.com/uber-go/zap
+[apex]: https://github.com/apex/log
+
 **Seeing weird case-sensitive problems?** It's in the past been possible to
 import Logrus as both upper- and lower-case. Due to the Go package environment,
 this caused issues in the community and we needed a standard. Some environments
@@ -15,11 +35,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):
 
@@ -28,7 +43,7 @@ plain text):
 With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash
 or Splunk:
 
-```json
+```text
 {"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
 ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
 
@@ -84,7 +99,7 @@ time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcr
 ```
 Note that this does add measurable overhead - the cost will depend on the version of Go, but is
 between 20 and 40% in recent tests with 1.6 and 1.7.  You can validate this in your
-environment via benchmarks: 
+environment via benchmarks:
 ```
 go test -bench=.*CallerTracing
 ```
@@ -187,7 +202,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 {
@@ -272,7 +287,7 @@ func init() {
 ```
 Note: Syslog hook 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).
 
-A list of currently known of service hook can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
+A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
 
 
 #### Level logging
@@ -302,6 +317,8 @@ log.SetLevel(log.InfoLevel)
 It may be useful to set `log.Level = logrus.DebugLevel` in a debug or verbose
 environment if your application has that.
 
+Note: If you want different log levels for global (`log.SetLevel(...)`) and syslog logging, please check the [syslog hook README](hooks/syslog/README.md#different-log-levels-for-local-and-remote-logging).
+
 #### Entries
 
 Besides the fields added with `WithField` or `WithFields` some fields are
@@ -326,7 +343,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" {
@@ -354,6 +371,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).
@@ -364,7 +382,10 @@ Third party logging formatters:
 * [`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).
 * [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
 * [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
-* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
+* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo.
+* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
+* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Sava log to files.
+* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
 
 You can define your formatter by implementing the `Formatter` interface,
 requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
@@ -383,7 +404,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
 }
@@ -429,14 +450,14 @@ entries. It should not be a feature of the application-level logger.
 
 | Tool | Description |
 | ---- | ----------- |
-|[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 generated with different config at different environment.|
+|[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.|
 |[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) |
 
 #### Testing
 
 Logrus has a built in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
 
-* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just add the `test` hook
+* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just adds the `test` hook
 * a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
 
 ```go
@@ -464,7 +485,7 @@ func TestSomething(t*testing.T){
 
 Logrus can register one or more functions that will be called when any `fatal`
 level message is logged. The registered handlers will be executed before
-logrus performs a `os.Exit(1)`. This behavior may be helpful if callers need
+logrus performs an `os.Exit(1)`. This behavior may be helpful if callers need
 to gracefully shutdown. 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.
 
 ```
@@ -489,6 +510,6 @@ Situation when locking is not needed includes:
 
   1) logger.Out is protected by locks.
 
-  2) logger.Out is a os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allow multi-thread/multi-process writing)
+  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)
 
      (Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)
diff --git alt_exit.go alt_exit.go
index 8af90637a..8fd189e1c 100644
--- alt_exit.go
+++ alt_exit.go
@@ -51,9 +51,9 @@ func Exit(code int) {
 	os.Exit(code)
 }
 
-// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke
-// all handlers. The handlers will also be invoked when any Fatal log entry is
-// made.
+// RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
+// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
+// any Fatal log entry is made.
 //
 // This method is useful when a caller wishes to use logrus to log a fatal
 // message but also needs to gracefully shutdown. An example usecase could be
@@ -62,3 +62,15 @@ func Exit(code int) {
 func RegisterExitHandler(handler func()) {
 	handlers = append(handlers, handler)
 }
+
+// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
+// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
+// any Fatal log entry is made.
+//
+// This method is useful when a caller wishes to use logrus to log a fatal
+// message but also needs to gracefully shutdown. An example usecase could be
+// closing database connections, or sending a alert that the application is
+// closing.
+func DeferExitHandler(handler func()) {
+	handlers = append([]func(){handler}, handlers...)
+}
diff --git alt_exit_test.go alt_exit_test.go
index 0a2ff5650..54d503cb4 100644
--- alt_exit_test.go
+++ alt_exit_test.go
@@ -14,9 +14,61 @@ import (
 
 func TestRegister(t *testing.T) {
 	current := len(handlers)
-	RegisterExitHandler(func() {})
-	if len(handlers) != current+1 {
-		t.Fatalf("expected %d handlers, got %d", current+1, len(handlers))
+
+	var results []string
+
+	h1 := func() { results = append(results, "first") }
+	h2 := func() { results = append(results, "second") }
+
+	RegisterExitHandler(h1)
+	RegisterExitHandler(h2)
+
+	if len(handlers) != current+2 {
+		t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
+	}
+
+	runHandlers()
+
+	if len(results) != 2 {
+		t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
+	}
+
+	if results[0] != "first" {
+		t.Fatal("expected handler h1 to be run first, but it wasn't")
+	}
+
+	if results[1] != "second" {
+		t.Fatal("expected handler h2 to be run second, but it wasn't")
+	}
+}
+
+func TestDefer(t *testing.T) {
+	current := len(handlers)
+
+	var results []string
+
+	h1 := func() { results = append(results, "first") }
+	h2 := func() { results = append(results, "second") }
+
+	DeferExitHandler(h1)
+	DeferExitHandler(h2)
+
+	if len(handlers) != current+2 {
+		t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
+	}
+
+	runHandlers()
+
+	if len(results) != 2 {
+		t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
+	}
+
+	if results[0] != "second" {
+		t.Fatal("expected handler h2 to be run first, but it wasn't")
+	}
+
+	if results[1] != "first" {
+		t.Fatal("expected handler h1 to be run second, but it wasn't")
 	}
 }
 
diff --git appveyor.yml appveyor.yml
index 96c2ce15f..df9d65c3a 100644
--- appveyor.yml
+++ appveyor.yml
@@ -1,14 +1,14 @@
-version: "{build}"
-platform: x64
-clone_folder: c:\gopath\src\github.com\sirupsen\logrus
-environment:  
-  GOPATH: c:\gopath
-branches:  
-  only:
-    - master
-install:  
-  - set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
-  - go version
-build_script:  
-  - go get -t
-  - go test
+version: "{build}"
+platform: x64
+clone_folder: c:\gopath\src\github.com\sirupsen\logrus
+environment:
+  GOPATH: c:\gopath
+branches:
+  only:
+    - master
+install:
+  - set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
+  - go version
+build_script:
+  - go get -t
+  - go test
diff --git a/buffer_pool.go b/buffer_pool.go
new file mode 100644
index 000000000..c7787f77c
--- /dev/null
+++ buffer_pool.go
@@ -0,0 +1,43 @@
+package logrus
+
+import (
+	"bytes"
+	"sync"
+)
+
+var (
+	bufferPool BufferPool
+)
+
+type BufferPool interface {
+	Put(*bytes.Buffer)
+	Get() *bytes.Buffer
+}
+
+type defaultPool struct {
+	pool *sync.Pool
+}
+
+func (p *defaultPool) Put(buf *bytes.Buffer) {
+	p.pool.Put(buf)
+}
+
+func (p *defaultPool) Get() *bytes.Buffer {
+	return p.pool.Get().(*bytes.Buffer)
+}
+
+// SetBufferPool allows to replace the default logrus buffer pool
+// to better meets the specific needs of an application.
+func SetBufferPool(bp BufferPool) {
+	bufferPool = bp
+}
+
+func init() {
+	SetBufferPool(&defaultPool{
+		pool: &sync.Pool{
+			New: func() interface{} {
+				return new(bytes.Buffer)
+			},
+		},
+	})
+}
diff --git a/ci/go.mod b/ci/go.mod
new file mode 100644
index 000000000..e895e95a2
--- /dev/null
+++ 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 000000000..edf273b8e
--- /dev/null
+++ 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 000000000..4273031d1
--- /dev/null
+++ 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 000000000..ceda305cd
--- /dev/null
+++ 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 entry.go entry.go
index df6d188de..71cdbbc35 100644
--- entry.go
+++ entry.go
@@ -2,6 +2,7 @@ package logrus
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"os"
 	"reflect"
@@ -12,7 +13,6 @@ import (
 )
 
 var (
-	bufferPool *sync.Pool
 
 	// qualified package name, cached at first use
 	logrusPackage string
@@ -30,12 +30,6 @@ const (
 )
 
 func init() {
-	bufferPool = &sync.Pool{
-		New: func() interface{} {
-			return new(bytes.Buffer)
-		},
-	}
-
 	// start at the bottom of the stack before the package-name cache is primed
 	minimumCallerDepth = 1
 }
@@ -69,6 +63,9 @@ type Entry struct {
 	// When formatter is called in entry.log(), a Buffer may be set to entry
 	Buffer *bytes.Buffer
 
+	// Contains the context set by the user. Useful for hook processing etc.
+	Context context.Context
+
 	// err may contain a field formatting error
 	err string
 }
@@ -81,10 +78,23 @@ 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)
+}
+
 // Returns the string representation from the reader and ultimately the
 // formatter.
 func (entry *Entry) String() (string, error) {
-	serialized, err := entry.Logger.Formatter.Format(entry)
+	serialized, err := entry.Bytes()
 	if err != nil {
 		return "", err
 	}
@@ -97,6 +107,15 @@ func (entry *Entry) WithError(err error) *Entry {
 	return entry.WithField(ErrorKey, err)
 }
 
+// Add a context to the Entry.
+func (entry *Entry) WithContext(ctx context.Context) *Entry {
+	dataCopy := make(Fields, len(entry.Data))
+	for k, v := range entry.Data {
+		dataCopy[k] = v
+	}
+	return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
+}
+
 // Add a single field to the Entry.
 func (entry *Entry) WithField(key string, value interface{}) *Entry {
 	return entry.WithFields(Fields{key: value})
@@ -112,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 {
@@ -130,12 +147,16 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
 			data[k] = v
 		}
 	}
-	return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr}
+	return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
 }
 
 // Overrides the time of the Entry.
 func (entry *Entry) WithTime(t time.Time) *Entry {
-	return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t, err: entry.err}
+	dataCopy := make(Fields, len(entry.Data))
+	for k, v := range entry.Data {
+		dataCopy[k] = v
+	}
+	return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
 }
 
 // getPackageName reduces a fully qualified function name to the package name
@@ -156,26 +177,34 @@ func getPackageName(f string) string {
 
 // getCaller retrieves the name of the first non-logrus calling function
 func getCaller() *runtime.Frame {
-	// Restrict the lookback frames to avoid runaway lookups
-	pcs := make([]uintptr, maximumCallerDepth)
-	depth := runtime.Callers(minimumCallerDepth, pcs)
-	frames := runtime.CallersFrames(pcs[:depth])
-
 	// cache this package's fully-qualified name
 	callerInitOnce.Do(func() {
-		logrusPackage = getPackageName(runtime.FuncForPC(pcs[0]).Name())
+		pcs := make([]uintptr, maximumCallerDepth)
+		_ = runtime.Callers(0, pcs)
+
+		// dynamic get the package name and the minimum caller depth
+		for i := 0; i < maximumCallerDepth; i++ {
+			funcName := runtime.FuncForPC(pcs[i]).Name()
+			if strings.Contains(funcName, "getCaller") {
+				logrusPackage = getPackageName(funcName)
+				break
+			}
+		}
 
-		// now that we have the cache, we can skip a minimum count of known-logrus functions
-		// XXX this is dubious, the number of frames may vary store an entry in a logger interface
 		minimumCallerDepth = knownLogrusFrames
 	})
 
+	// Restrict the lookback frames to avoid runaway lookups
+	pcs := make([]uintptr, maximumCallerDepth)
+	depth := runtime.Callers(minimumCallerDepth, pcs)
+	frames := runtime.CallersFrames(pcs[:depth])
+
 	for f, again := frames.Next(); again; f, again = frames.Next() {
 		pkg := getPackageName(f.Function)
 
 		// If the caller isn't part of this package, we're done
 		if pkg != logrusPackage {
-			return &f
+			return &f //nolint:scopelint
 		}
 	}
 
@@ -189,49 +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
-	if entry.Logger.ReportCaller {
-		entry.Caller = getCaller()
+	if newEntry.Time.IsZero() {
+		newEntry.Time = time.Now()
 	}
 
-	entry.fireHooks()
+	newEntry.Level = level
+	newEntry.Message = msg
+
+	newEntry.Logger.mu.Lock()
+	reportCaller := newEntry.Logger.ReportCaller
+	bufPool := newEntry.getBufferPool()
+	newEntry.Logger.mu.Unlock()
+
+	if reportCaller {
+		newEntry.Caller = getCaller()
+	}
 
-	buffer = bufferPool.Get().(*bytes.Buffer)
+	newEntry.fireHooks()
+	buffer = bufPool.Get()
+	defer func() {
+		newEntry.Buffer = nil
+		buffer.Reset()
+		bufPool.Put(buffer)
+	}()
 	buffer.Reset()
-	defer bufferPool.Put(buffer)
-	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)
 	}
@@ -243,14 +289,16 @@ func (entry *Entry) write() {
 	serialized, err := entry.Logger.Formatter.Format(entry)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
-	} else {
-		_, err = entry.Logger.Out.Write(serialized)
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
-		}
+		return
+	}
+	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...))
@@ -292,13 +340,14 @@ func (entry *Entry) Fatal(args ...interface{}) {
 
 func (entry *Entry) Panic(args ...interface{}) {
 	entry.Log(PanicLevel, args...)
-	panic(fmt.Sprint(args...))
 }
 
 // Entry Printf family functions
 
 func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
-	entry.Log(level, fmt.Sprintf(format, args...))
+	if entry.Logger.IsLevelEnabled(level) {
+		entry.Log(level, fmt.Sprintf(format, args...))
+	}
 }
 
 func (entry *Entry) Tracef(format string, args ...interface{}) {
diff --git entry_test.go entry_test.go
index 5e6634112..41c47a2fb 100644
--- entry_test.go
+++ entry_test.go
@@ -2,6 +2,7 @@ package logrus
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -33,6 +34,95 @@ func TestEntryWithError(t *testing.T) {
 
 }
 
+func TestEntryWithContext(t *testing.T) {
+	assert := assert.New(t)
+	ctx := context.WithValue(context.Background(), "foo", "bar")
+
+	assert.Equal(ctx, WithContext(ctx).Context)
+
+	logger := New()
+	logger.Out = &bytes.Buffer{}
+	entry := NewEntry(logger)
+
+	assert.Equal(ctx, entry.WithContext(ctx).Context)
+}
+
+func TestEntryWithContextCopiesData(t *testing.T) {
+	assert := assert.New(t)
+
+	// Initialize a parent Entry object with a key/value set in its Data map
+	logger := New()
+	logger.Out = &bytes.Buffer{}
+	parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
+
+	// Create two children Entry objects from the parent in different contexts
+	ctx1 := context.WithValue(context.Background(), "foo", "bar")
+	childEntry1 := parentEntry.WithContext(ctx1)
+	assert.Equal(ctx1, childEntry1.Context)
+
+	ctx2 := context.WithValue(context.Background(), "bar", "baz")
+	childEntry2 := parentEntry.WithContext(ctx2)
+	assert.Equal(ctx2, childEntry2.Context)
+	assert.NotEqual(ctx1, ctx2)
+
+	// Ensure that data set in the parent Entry are preserved to both children
+	assert.Equal("parentValue", childEntry1.Data["parentKey"])
+	assert.Equal("parentValue", childEntry2.Data["parentKey"])
+
+	// Modify data stored in the child entry
+	childEntry1.Data["childKey"] = "childValue"
+
+	// Verify that data is successfully stored in the child it was set on
+	val, exists := childEntry1.Data["childKey"]
+	assert.True(exists)
+	assert.Equal("childValue", val)
+
+	// Verify that the data change to child 1 has not affected its sibling
+	val, exists = childEntry2.Data["childKey"]
+	assert.False(exists)
+	assert.Empty(val)
+
+	// Verify that the data change to child 1 has not affected its parent
+	val, exists = parentEntry.Data["childKey"]
+	assert.False(exists)
+	assert.Empty(val)
+}
+
+func TestEntryWithTimeCopiesData(t *testing.T) {
+	assert := assert.New(t)
+
+	// Initialize a parent Entry object with a key/value set in its Data map
+	logger := New()
+	logger.Out = &bytes.Buffer{}
+	parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
+
+	// Create two children Entry objects from the parent with two different times
+	childEntry1 := parentEntry.WithTime(time.Now().AddDate(0, 0, 1))
+	childEntry2 := parentEntry.WithTime(time.Now().AddDate(0, 0, 2))
+
+	// Ensure that data set in the parent Entry are preserved to both children
+	assert.Equal("parentValue", childEntry1.Data["parentKey"])
+	assert.Equal("parentValue", childEntry2.Data["parentKey"])
+
+	// Modify data stored in the child entry
+	childEntry1.Data["childKey"] = "childValue"
+
+	// Verify that data is successfully stored in the child it was set on
+	val, exists := childEntry1.Data["childKey"]
+	assert.True(exists)
+	assert.Equal("childValue", val)
+
+	// Verify that the data change to child 1 has not affected its sibling
+	val, exists = childEntry2.Data["childKey"]
+	assert.False(exists)
+	assert.Empty(val)
+
+	// Verify that the data change to child 1 has not affected its parent
+	val, exists = parentEntry.Data["childKey"]
+	assert.False(exists)
+	assert.Empty(val)
+}
+
 func TestEntryPanicln(t *testing.T) {
 	errBoom := fmt.Errorf("boom time")
 
@@ -77,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"
@@ -120,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})
 
@@ -139,3 +251,51 @@ func TestEntryWithIncorrectField(t *testing.T) {
 	assert.Equal(eWithFunc.err, `can not add field "func"`)
 	assert.Equal(eWithFuncPtr.err, `can not add field "funcPtr"`)
 }
+
+func TestEntryLogfLevel(t *testing.T) {
+	logger := New()
+	buffer := &bytes.Buffer{}
+	logger.Out = buffer
+	logger.SetLevel(InfoLevel)
+	entry := NewEntry(logger)
+
+	entry.Logf(DebugLevel, "%s", "debug")
+	assert.NotContains(t, buffer.String(), "debug")
+
+	entry.Logf(WarnLevel, "%s", "warn")
+	assert.Contains(t, buffer.String(), "warn")
+}
+
+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)
+	}()
+	go func() {
+		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/example_custom_caller_test.go b/example_custom_caller_test.go
new file mode 100644
index 000000000..6749effbc
--- /dev/null
+++ example_custom_caller_test.go
@@ -0,0 +1,28 @@
+package logrus_test
+
+import (
+	"os"
+	"path"
+	"runtime"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+)
+
+func ExampleJSONFormatter_CallerPrettyfier() {
+	l := logrus.New()
+	l.SetReportCaller(true)
+	l.Out = os.Stdout
+	l.Formatter = &logrus.JSONFormatter{
+		DisableTimestamp: true,
+		CallerPrettyfier: func(f *runtime.Frame) (string, string) {
+			s := strings.Split(f.Function, ".")
+			funcname := s[len(s)-1]
+			_, filename := path.Split(f.File)
+			return funcname, filename
+		},
+	}
+	l.Info("example of custom format caller")
+	// Output:
+	// {"file":"example_custom_caller_test.go","func":"ExampleJSONFormatter_CallerPrettyfier","level":"info","msg":"example of custom format caller"}
+}
diff --git a/example_default_field_value_test.go b/example_default_field_value_test.go
new file mode 100644
index 000000000..e7edd1a8a
--- /dev/null
+++ example_default_field_value_test.go
@@ -0,0 +1,31 @@
+package logrus_test
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+type DefaultFieldHook struct {
+	GetValue func() string
+}
+
+func (h *DefaultFieldHook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+func (h *DefaultFieldHook) Fire(e *logrus.Entry) error {
+	e.Data["aDefaultField"] = h.GetValue()
+	return nil
+}
+
+func ExampleDefaultFieldHook() {
+	l := logrus.New()
+	l.Out = os.Stdout
+	l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
+
+	l.AddHook(&DefaultFieldHook{GetValue: func() string { return "with its default value" }})
+	l.Info("first log")
+	// Output:
+	// level=info msg="first log" aDefaultField="with its default value"
+}
diff --git a/example_function_test.go b/example_function_test.go
new file mode 100644
index 000000000..dda890d83
--- /dev/null
+++ example_function_test.go
@@ -0,0 +1,31 @@
+package logrus_test
+
+import (
+	"testing"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLogger_LogFn(t *testing.T) {
+	log.SetFormatter(&log.JSONFormatter{})
+	log.SetLevel(log.WarnLevel)
+
+	notCalled := 0
+	log.InfoFn(func() []interface{} {
+		notCalled++
+		return []interface{}{
+			"Hello",
+		}
+	})
+	assert.Equal(t, 0, notCalled)
+
+	called := 0
+	log.ErrorFn(func() []interface{} {
+		called++
+		return []interface{}{
+			"Oopsi",
+		}
+	})
+	assert.Equal(t, 1, called)
+}
diff --git example_global_hook_test.go example_global_hook_test.go
index c81e448c2..ff7b2559f 100644
--- example_global_hook_test.go
+++ example_global_hook_test.go
@@ -1,8 +1,9 @@
 package logrus_test
 
 import (
-	"github.com/sirupsen/logrus"
 	"os"
+
+	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -21,7 +22,7 @@ func (h *GlobalHook) Fire(e *logrus.Entry) error {
 	return nil
 }
 
-func Example() {
+func ExampleGlobalHook() {
 	l := logrus.New()
 	l.Out = os.Stdout
 	l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
diff --git exported.go exported.go
index 7342613c3..017c30ce6 100644
--- exported.go
+++ exported.go
@@ -1,6 +1,7 @@
 package logrus
 
 import (
+	"context"
 	"io"
 	"time"
 )
@@ -55,6 +56,11 @@ func WithError(err error) *Entry {
 	return std.WithField(ErrorKey, err)
 }
 
+// WithContext creates an entry from the standard logger and adds a context to it.
+func WithContext(ctx context.Context) *Entry {
+	return std.WithContext(ctx)
+}
+
 // WithField creates an entry from the standard logger and adds a field to
 // it. If you want multiple fields, use `WithFields`.
 //
@@ -74,7 +80,7 @@ func WithFields(fields Fields) *Entry {
 	return std.WithFields(fields)
 }
 
-// WithTime creats an entry from the standard logger and overrides the time of
+// WithTime creates an entry from the standard logger and overrides the time of
 // logs generated with it.
 //
 // Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
@@ -128,6 +134,51 @@ func Fatal(args ...interface{}) {
 	std.Fatal(args...)
 }
 
+// TraceFn logs a message from a func at level Trace on the standard logger.
+func TraceFn(fn LogFunction) {
+	std.TraceFn(fn)
+}
+
+// DebugFn logs a message from a func at level Debug on the standard logger.
+func DebugFn(fn LogFunction) {
+	std.DebugFn(fn)
+}
+
+// PrintFn logs a message from a func at level Info on the standard logger.
+func PrintFn(fn LogFunction) {
+	std.PrintFn(fn)
+}
+
+// InfoFn logs a message from a func at level Info on the standard logger.
+func InfoFn(fn LogFunction) {
+	std.InfoFn(fn)
+}
+
+// WarnFn logs a message from a func at level Warn on the standard logger.
+func WarnFn(fn LogFunction) {
+	std.WarnFn(fn)
+}
+
+// WarningFn logs a message from a func at level Warn on the standard logger.
+func WarningFn(fn LogFunction) {
+	std.WarningFn(fn)
+}
+
+// ErrorFn logs a message from a func at level Error on the standard logger.
+func ErrorFn(fn LogFunction) {
+	std.ErrorFn(fn)
+}
+
+// PanicFn logs a message from a func at level Panic on the standard logger.
+func PanicFn(fn LogFunction) {
+	std.PanicFn(fn)
+}
+
+// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
+func FatalFn(fn LogFunction) {
+	std.FatalFn(fn)
+}
+
 // Tracef logs a message at level Trace on the standard logger.
 func Tracef(format string, args ...interface{}) {
 	std.Tracef(format, args...)
diff --git go.mod go.mod
index 94574cc63..8b3f6d373 100644
--- go.mod
+++ go.mod
@@ -2,10 +2,8 @@ module github.com/sirupsen/logrus
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/konsorten/go-windows-terminal-sequences v1.0.1
-	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
+	github.com/stretchr/testify v1.7.0
+	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 )
+
+go 1.13
diff --git go.sum go.sum
index 133d34ae1..e5fdc85bf 100644
--- go.sum
+++ go.sum
@@ -1,15 +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/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/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=
-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=
+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 hook_test.go hook_test.go
index b9675935b..a2becc82a 100644
--- hook_test.go
+++ 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"
 )
 
@@ -190,3 +192,43 @@ func TestAddHookRace(t *testing.T) {
 		// actually assert on the hook
 	})
 }
+
+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()
+}
+
+func (h *HookCallFunc) Levels() []Level {
+	return AllLevels
+}
+
+func (h *HookCallFunc) Fire(e *Entry) error {
+	h.F()
+	return nil
+}
+
+func TestHookFireOrder(t *testing.T) {
+	checkers := []string{}
+	h := LevelHooks{}
+	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "first hook") }})
+	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "second hook") }})
+	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "third hook") }})
+
+	if err := h.Fire(InfoLevel, &Entry{}); err != nil {
+		t.Error("unexpected error:", err)
+	}
+	require.Equal(t, []string{"first hook", "second hook", "third hook"}, checkers)
+}
diff --git hooks/syslog/README.md hooks/syslog/README.md
index 1bbc0f72d..67cb5ea85 100644
--- hooks/syslog/README.md
+++ hooks/syslog/README.md
@@ -37,3 +37,45 @@ func main() {
   }
 }
 ```
+
+### Different log levels for local and remote logging
+
+By default `NewSyslogHook()` sends logs through the hook for all log levels. If you want to have
+different log levels between local logging and syslog logging (i.e. respect the `priority` argument
+passed to `NewSyslogHook()`), you need to implement the `logrus_syslog.SyslogHook` interface
+overriding `Levels()` to return only the log levels you're interested on.
+
+The following example shows how to log at **DEBUG** level for local logging and **WARN** level for
+syslog logging:
+
+```go
+package main
+
+import (
+	"log/syslog"
+
+	log "github.com/sirupsen/logrus"
+	logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
+)
+
+type customHook struct {
+	*logrus_syslog.SyslogHook
+}
+
+func (h *customHook) Levels() []log.Level {
+	return []log.Level{log.WarnLevel}
+}
+
+func main() {
+	log.SetLevel(log.DebugLevel)
+
+	hook, err := logrus_syslog.NewSyslogHook("tcp", "localhost:5140", syslog.LOG_WARNING, "myTag")
+	if err != nil {
+		panic(err)
+	}
+
+	log.AddHook(&customHook{hook})
+
+	//...
+}
+```
diff --git hooks/test/test.go hooks/test/test.go
index 234a17dfa..046f0bf6c 100644
--- hooks/test/test.go
+++ hooks/test/test.go
@@ -1,6 +1,5 @@
-// The Test package is used for testing logrus. It is here for backwards
-// compatibility from when logrus' organization was upper-case. Please use
-// lower-case logrus and the `null` package instead of this one.
+// The Test package is used for testing logrus.
+// It provides a simple hooks which register logged messages.
 package test
 
 import (
@@ -33,7 +32,7 @@ func NewGlobal() *Hook {
 func NewLocal(logger *logrus.Logger) *Hook {
 
 	hook := new(Hook)
-	logger.Hooks.Add(hook)
+	logger.AddHook(hook)
 
 	return hook
 
diff --git hooks/test/test_test.go hooks/test/test_test.go
index 636bad512..9601491aa 100644
--- hooks/test/test_test.go
+++ hooks/test/test_test.go
@@ -83,3 +83,22 @@ func TestFatalWithAlternateExit(t *testing.T) {
 	assert.Equal("something went very wrong", hook.LastEntry().Message)
 	assert.Equal(1, len(hook.Entries))
 }
+
+func TestNewLocal(t *testing.T) {
+	assert := assert.New(t)
+	logger := logrus.New()
+
+	var wg sync.WaitGroup
+	defer wg.Wait()
+
+	wg.Add(10)
+	for i := 0; i < 10; i++ {
+		go func(i int) {
+			logger.Info("info")
+			wg.Done()
+		}(i)
+	}
+
+	hook := NewLocal(logger)
+	assert.NotNil(hook)
+}
diff --git a/hooks/writer/README.md b/hooks/writer/README.md
new file mode 100644
index 000000000..69676309f
--- /dev/null
+++ hooks/writer/README.md
@@ -0,0 +1,43 @@
+# Writer Hooks for Logrus
+
+Send logs of given levels to any object with `io.Writer` interface.
+
+## Usage
+
+If you want for example send high level logs to `Stderr` and
+logs of  normal execution to `Stdout`, you could do it like this:
+
+```go
+package main
+
+import (
+	"io/ioutil"
+	"os"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/sirupsen/logrus/hooks/writer"
+)
+
+func main() {
+	log.SetOutput(ioutil.Discard) // Send all logs to nowhere by default
+
+	log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr
+		Writer: os.Stderr,
+		LogLevels: []log.Level{
+			log.PanicLevel,
+			log.FatalLevel,
+			log.ErrorLevel,
+			log.WarnLevel,
+		},
+	})
+	log.AddHook(&writer.Hook{ // Send info and debug logs to stdout
+		Writer: os.Stdout,
+		LogLevels: []log.Level{
+			log.InfoLevel,
+			log.DebugLevel,
+		},
+	})
+	log.Info("This will go to stdout")
+	log.Warn("This will go to stderr")
+}
+```
diff --git a/hooks/writer/writer.go b/hooks/writer/writer.go
new file mode 100644
index 000000000..1160c790e
--- /dev/null
+++ hooks/writer/writer.go
@@ -0,0 +1,29 @@
+package writer
+
+import (
+	"io"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// Hook is a hook that writes logs of specified LogLevels to specified Writer
+type Hook struct {
+	Writer    io.Writer
+	LogLevels []log.Level
+}
+
+// Fire will be called when some logging function is called with current hook
+// It will format log entry to string and write it to appropriate writer
+func (hook *Hook) Fire(entry *log.Entry) error {
+	line, err := entry.Bytes()
+	if err != nil {
+		return err
+	}
+	_, err = hook.Writer.Write(line)
+	return err
+}
+
+// Levels define on which log levels this hook would trigger
+func (hook *Hook) Levels() []log.Level {
+	return hook.LogLevels
+}
diff --git a/hooks/writer/writer_test.go b/hooks/writer/writer_test.go
new file mode 100644
index 000000000..a30d3b01a
--- /dev/null
+++ hooks/writer/writer_test.go
@@ -0,0 +1,38 @@
+package writer
+
+import (
+	"bytes"
+	"io/ioutil"
+	"testing"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDifferentLevelsGoToDifferentWriters(t *testing.T) {
+	var a, b bytes.Buffer
+
+	log.SetFormatter(&log.TextFormatter{
+		DisableTimestamp: true,
+		DisableColors:    true,
+	})
+	log.SetOutput(ioutil.Discard) // Send all logs to nowhere by default
+
+	log.AddHook(&Hook{
+		Writer: &a,
+		LogLevels: []log.Level{
+			log.WarnLevel,
+		},
+	})
+	log.AddHook(&Hook{ // Send info and debug logs to stdout
+		Writer: &b,
+		LogLevels: []log.Level{
+			log.InfoLevel,
+		},
+	})
+	log.Warn("send to a")
+	log.Info("send to b")
+
+	assert.Equal(t, a.String(), "level=warning msg=\"send to a\"\n")
+	assert.Equal(t, b.String(), "level=info msg=\"send to b\"\n")
+}
diff --git internal/testutils/testutils.go internal/testutils/testutils.go
index 20bc3c3b6..6e3a6203e 100644
--- internal/testutils/testutils.go
+++ internal/testutils/testutils.go
@@ -40,7 +40,7 @@ func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields ma
 	log(logger)
 
 	fields := make(map[string]string)
-	for _, kv := range strings.Split(buffer.String(), " ") {
+	for _, kv := range strings.Split(strings.TrimRight(buffer.String(), "\n"), " ") {
 		if !strings.Contains(kv, "=") {
 			continue
 		}
diff --git json_formatter.go json_formatter.go
index 260575359..c96dc5636 100644
--- json_formatter.go
+++ json_formatter.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"runtime"
 )
 
 type fieldKey string
@@ -22,11 +23,17 @@ 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
 	DisableTimestamp bool
 
+	// DisableHTMLEscape allows disabling html escaping in output
+	DisableHTMLEscape bool
+
 	// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
 	DataKey string
 
@@ -42,6 +49,12 @@ type JSONFormatter 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
+	// activated. If any of the returned value is the empty string the
+	// corresponding key will be removed from json fields.
+	CallerPrettyfier func(*runtime.Frame) (function string, file string)
+
 	// PrettyPrint will indent all json logs
 	PrettyPrint bool
 }
@@ -82,8 +95,17 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
 	data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
 	data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
 	if entry.HasCaller() {
-		data[f.FieldMap.resolve(FieldKeyFunc)] = entry.Caller.Function
-		data[f.FieldMap.resolve(FieldKeyFile)] = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
+		funcVal := entry.Caller.Function
+		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
+		if f.CallerPrettyfier != nil {
+			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
+		}
+		if funcVal != "" {
+			data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
+		}
+		if fileVal != "" {
+			data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
+		}
 	}
 
 	var b *bytes.Buffer
@@ -94,11 +116,12 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
 	}
 
 	encoder := json.NewEncoder(b)
+	encoder.SetEscapeHTML(!f.DisableHTMLEscape)
 	if f.PrettyPrint {
 		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 json_formatter_test.go json_formatter_test.go
index 695c36e54..7a48f2dc6 100644
--- json_formatter_test.go
+++ json_formatter_test.go
@@ -344,3 +344,29 @@ func TestJSONEnableTimestamp(t *testing.T) {
 		t.Error("Timestamp not present", s)
 	}
 }
+
+func TestJSONDisableHTMLEscape(t *testing.T) {
+	formatter := &JSONFormatter{DisableHTMLEscape: true}
+
+	b, err := formatter.Format(&Entry{Message: "& < >"})
+	if err != nil {
+		t.Fatal("Unable to format entry: ", err)
+	}
+	s := string(b)
+	if !strings.Contains(s, "& < >") {
+		t.Error("Message should not be HTML escaped", s)
+	}
+}
+
+func TestJSONEnableHTMLEscape(t *testing.T) {
+	formatter := &JSONFormatter{}
+
+	b, err := formatter.Format(&Entry{Message: "& < >"})
+	if err != nil {
+		t.Fatal("Unable to format entry: ", err)
+	}
+	s := string(b)
+	if !(strings.Contains(s, "u0026") && strings.Contains(s, "u003e") && strings.Contains(s, "u003c")) {
+		t.Error("Message should be HTML escaped", s)
+	}
+}
diff --git logger.go logger.go
index 9bf64e22a..5ff0aef6d 100644
--- logger.go
+++ logger.go
@@ -1,6 +1,7 @@
 package logrus
 
 import (
+	"context"
 	"io"
 	"os"
 	"sync"
@@ -8,6 +9,11 @@ import (
 	"time"
 )
 
+// 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 Logger struct {
 	// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
 	// file, or leave it default which is `os.Stderr`. You can also set this to
@@ -38,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)
@@ -67,10 +76,10 @@ func (mw *MutexWrap) Disable() {
 // `Out` and `Hooks` directly on the default logger instance. You can also just
 // instantiate your own:
 //
-//    var log = &Logger{
+//    var log = &logrus.Logger{
 //      Out: os.Stderr,
-//      Formatter: new(JSONFormatter),
-//      Hooks: make(LevelHooks),
+//      Formatter: new(logrus.TextFormatter),
+//      Hooks: make(logrus.LevelHooks),
 //      Level: logrus.DebugLevel,
 //    }
 //
@@ -99,8 +108,9 @@ func (logger *Logger) releaseEntry(entry *Entry) {
 	logger.entryPool.Put(entry)
 }
 
-// Adds a field to the log entry, note that it doesn't log until you call
-// Debug, Print, Info, Warn, Error, Fatal or Panic. It only creates a log entry.
+// WithField allocates a new entry and adds a field to it.
+// Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
+// this new returned entry.
 // If you want multiple fields, use `WithFields`.
 func (logger *Logger) WithField(key string, value interface{}) *Entry {
 	entry := logger.newEntry()
@@ -124,6 +134,13 @@ func (logger *Logger) WithError(err error) *Entry {
 	return entry.WithError(err)
 }
 
+// Add a context to the log entry.
+func (logger *Logger) WithContext(ctx context.Context) *Entry {
+	entry := logger.newEntry()
+	defer logger.releaseEntry(entry)
+	return entry.WithContext(ctx)
+}
+
 // Overrides the time of the log entry.
 func (logger *Logger) WithTime(t time.Time) *Entry {
 	entry := logger.newEntry()
@@ -178,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()
@@ -186,6 +206,14 @@ func (logger *Logger) Log(level Level, args ...interface{}) {
 	}
 }
 
+func (logger *Logger) LogFn(level Level, fn LogFunction) {
+	if logger.IsLevelEnabled(level) {
+		entry := logger.newEntry()
+		entry.Log(level, fn()...)
+		logger.releaseEntry(entry)
+	}
+}
+
 func (logger *Logger) Trace(args ...interface{}) {
 	logger.Log(TraceLevel, args...)
 }
@@ -200,7 +228,7 @@ func (logger *Logger) Info(args ...interface{}) {
 
 func (logger *Logger) Print(args ...interface{}) {
 	entry := logger.newEntry()
-	entry.Info(args...)
+	entry.Print(args...)
 	logger.releaseEntry(entry)
 }
 
@@ -225,6 +253,45 @@ func (logger *Logger) Panic(args ...interface{}) {
 	logger.Log(PanicLevel, args...)
 }
 
+func (logger *Logger) TraceFn(fn LogFunction) {
+	logger.LogFn(TraceLevel, fn)
+}
+
+func (logger *Logger) DebugFn(fn LogFunction) {
+	logger.LogFn(DebugLevel, fn)
+}
+
+func (logger *Logger) InfoFn(fn LogFunction) {
+	logger.LogFn(InfoLevel, fn)
+}
+
+func (logger *Logger), PrintFn(fn LogFunction) {
+	entry := logger.newEntry()
+	entry.Print(fn()...)
+	logger.releaseEntry(entry)
+}
+
+func (logger *Logger) WarnFn(fn LogFunction) {
+	logger.LogFn(WarnLevel, fn)
+}
+
+func (logger *Logger) WarningFn(fn LogFunction) {
+	logger.WarnFn(fn)
+}
+
+func (logger *Logger) ErrorFn(fn LogFunction) {
+	logger.LogFn(ErrorLevel, fn)
+}
+
+func (logger *Logger) FatalFn(fn LogFunction) {
+	logger.LogFn(FatalLevel, fn)
+	logger.Exit(1)
+}
+
+func (logger *Logger) PanicFn(fn LogFunction) {
+	logger.LogFn(PanicLevel, fn)
+}
+
 func (logger *Logger) Logln(level Level, args ...interface{}) {
 	if logger.IsLevelEnabled(level) {
 		entry := logger.newEntry()
@@ -256,7 +323,7 @@ func (logger *Logger) Warnln(args ...interface{}) {
 }
 
 func (logger *Logger) Warningln(args ...interface{}) {
-	logger.Warn(args...)
+	logger.Warnln(args...)
 }
 
 func (logger *Logger) Errorln(args ...interface{}) {
@@ -341,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 logger_bench_test.go logger_bench_test.go
index f0a768439..1699af5aa 100644
--- logger_bench_test.go
+++ logger_bench_test.go
@@ -6,14 +6,6 @@ import (
 	"testing"
 )
 
-// smallFields is a small size data set for benchmarking
-var loggerFields = Fields{
-	"foo":   "bar",
-	"baz":   "qux",
-	"one":   "two",
-	"three": "four",
-}
-
 func BenchmarkDummyLogger(b *testing.B) {
 	nullf, err := os.OpenFile("/dev/null", os.O_WRONLY, 0666)
 	if err != nil {
diff --git logger_test.go logger_test.go
index 73ba45061..595c68214 100644
--- logger_test.go
+++ logger_test.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -20,9 +21,11 @@ func TestFieldValueError(t *testing.T) {
 	l.WithField("func", func() {}).Info("test")
 	fmt.Println(buf.String())
 	var data map[string]interface{}
-	json.Unmarshal(buf.Bytes(), &data)
+	if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
+		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) {
@@ -36,7 +39,59 @@ func TestNoFieldValueError(t *testing.T) {
 	l.WithField("str", "str").Info("test")
 	fmt.Println(buf.String())
 	var data map[string]interface{}
-	json.Unmarshal(buf.Bytes(), &data)
+	if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
+		t.Error("unexpected error", err)
+	}
 	_, ok := data[FieldKeyLogrusError]
 	require.False(t, ok)
 }
+
+func TestWarninglnNotEqualToWarning(t *testing.T) {
+	buf := &bytes.Buffer{}
+	bufln := &bytes.Buffer{}
+
+	formatter := new(TextFormatter)
+	formatter.DisableTimestamp = true
+	formatter.DisableLevelTruncation = true
+
+	l := &Logger{
+		Out:       buf,
+		Formatter: formatter,
+		Hooks:     make(LevelHooks),
+		Level:     DebugLevel,
+	}
+	l.Warning("hello,", "world")
+
+	l.SetOutput(bufln)
+	l.Warningln("hello,", "world")
+
+	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 logrus.go logrus.go
index c1ca88990..2f16224cb 100644
--- logrus.go
+++ logrus.go
@@ -51,7 +51,7 @@ func (level *Level) UnmarshalText(text []byte) error {
 		return err
 	}
 
-	*level = Level(l)
+	*level = l
 
 	return nil
 }
@@ -74,7 +74,7 @@ func (level Level) MarshalText() ([]byte, error) {
 		return []byte("panic"), nil
 	}
 
-	return nil, fmt.Errorf("not a valid lorus level %q", level)
+	return nil, fmt.Errorf("not a valid logrus level %d", level)
 }
 
 // A constant exposing all logging levels
diff --git logrus_test.go logrus_test.go
index d857262a9..4edee2834 100644
--- logrus_test.go
+++ logrus_test.go
@@ -40,7 +40,29 @@ func TestReportCallerWhenConfigured(t *testing.T) {
 		assert.Equal(t, "testWithCaller", fields["msg"])
 		assert.Equal(t, "info", fields["level"])
 		assert.Equal(t,
-			"github.com/sirupsen/logrus_test.TestReportCallerWhenConfigured.func3", fields["func"])
+			"github.com/sirupsen/logrus_test.TestReportCallerWhenConfigured.func3", fields[FieldKeyFunc])
+	})
+
+	LogAndAssertJSON(t, func(log *Logger) {
+		log.ReportCaller = true
+		log.Formatter.(*JSONFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
+			return "somekindoffunc", "thisisafilename"
+		}
+		log.Print("testWithCallerPrettyfier")
+	}, func(fields Fields) {
+		assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
+		assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
+	})
+
+	LogAndAssertText(t, func(log *Logger) {
+		log.ReportCaller = true
+		log.Formatter.(*TextFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
+			return "somekindoffunc", "thisisafilename"
+		}
+		log.Print("testWithCallerPrettyfier")
+	}, func(fields map[string]string) {
+		assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
+		assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
 	})
 }
 
@@ -517,10 +539,17 @@ func TestParseLevel(t *testing.T) {
 	assert.Nil(t, err)
 	assert.Equal(t, TraceLevel, l)
 
-	l, err = ParseLevel("invalid")
+	_, err = ParseLevel("invalid")
 	assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
 }
 
+func TestLevelString(t *testing.T) {
+	var loggerlevel Level
+	loggerlevel = 32000
+
+	_ = loggerlevel.String()
+}
+
 func TestGetSetLevelRace(t *testing.T) {
 	wg := sync.WaitGroup{}
 	for i := 0; i < 100; i++ {
@@ -559,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() {
-			entry.Info("info")
+			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() {
+			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()
 }
 
@@ -627,11 +689,14 @@ func TestEntryWriter(t *testing.T) {
 	log := New()
 	log.Out = cw
 	log.Formatter = new(JSONFormatter)
-	log.WithField("foo", "bar").WriterLevel(WarnLevel).Write([]byte("hello\n"))
+	_, err := log.WithField("foo", "bar").WriterLevel(WarnLevel).Write([]byte("hello\n"))
+	if err != nil {
+		t.Error("unexecpted error", err)
+	}
 
 	bs := <-cw
 	var fields Fields
-	err := json.Unmarshal(bs, &fields)
+	err = json.Unmarshal(bs, &fields)
 	assert.Nil(t, err)
 	assert.Equal(t, fields["foo"], "bar")
 	assert.Equal(t, fields["level"], "warning")
@@ -714,3 +779,20 @@ func TestReportCallerOnTextFormatter(t *testing.T) {
 	l.Formatter.(*TextFormatter).DisableColors = true
 	l.WithFields(Fields{"func": "func", "file": "file"}).Info("test")
 }
+
+func TestSetReportCallerRace(t *testing.T) {
+	l := New()
+	l.Out = ioutil.Discard
+	l.SetReportCaller(true)
+
+	var wg sync.WaitGroup
+	wg.Add(100)
+
+	for i := 0; i < 100; i++ {
+		go func() {
+			l.Error("Some Error")
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+}
diff --git a/terminal_check_bsd.go b/terminal_check_bsd.go
new file mode 100644
index 000000000..499789984
--- /dev/null
+++ terminal_check_bsd.go
@@ -0,0 +1,13 @@
+// +build darwin dragonfly freebsd netbsd openbsd
+// +build !js
+
+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 terminal_check_js.go terminal_check_js.go
index 0c209750a..ebdae3ec6 100644
--- terminal_check_js.go
+++ terminal_check_js.go
@@ -2,10 +2,6 @@
 
 package logrus
 
-import (
-	"io"
-)
-
-func checkIfTerminal(w io.Writer) bool {
+func isTerminal(fd int) bool {
 	return false
 }
diff --git terminal_check_aix.go terminal_check_no_terminal.go
similarity index 60%
rename from terminal_check_aix.go
rename to terminal_check_no_terminal.go
index 04fdb7ba3..97af92c68 100644
--- terminal_check_aix.go
+++ terminal_check_no_terminal.go
@@ -1,8 +1,10 @@
-// +build !appengine,!js,!windows,aix
+// +build js nacl plan9
 
 package logrus
 
-import "io"
+import (
+	"io"
+)
 
 func checkIfTerminal(w io.Writer) bool {
 	return false
diff --git terminal_check_notappengine.go terminal_check_notappengine.go
index d46556509..3293fb3ca 100644
--- terminal_check_notappengine.go
+++ 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 000000000..f6710b3bd
--- /dev/null
+++ 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 000000000..04748b851
--- /dev/null
+++ terminal_check_unix.go
@@ -0,0 +1,13 @@
+// +build linux aix zos
+// +build !js
+
+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 terminal_check_windows.go terminal_check_windows.go
index 3b9d2864c..2879eb50e 100644
--- terminal_check_windows.go
+++ terminal_check_windows.go
@@ -5,16 +5,23 @@ package logrus
 import (
 	"io"
 	"os"
-	"syscall"
+
+	"golang.org/x/sys/windows"
 )
 
 func checkIfTerminal(w io.Writer) bool {
 	switch v := w.(type) {
 	case *os.File:
+		handle := windows.Handle(v.Fd())
 		var mode uint32
-		err := syscall.GetConsoleMode(syscall.Handle(v.Fd()), &mode)
-		return err == nil
-	default:
-		return false
+		if err := windows.GetConsoleMode(handle, &mode); err != nil {
+			return false
+		}
+		mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
+		if err := windows.SetConsoleMode(handle, mode); err != nil {
+			return false
+		}
+		return true
 	}
+	return false
 }
diff --git terminal_notwindows.go terminal_notwindows.go
deleted file mode 100644
index 3dbd23720..000000000
--- terminal_notwindows.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// +build !windows
-
-package logrus
-
-import "io"
-
-func initTerminal(w io.Writer) {
-}
diff --git terminal_windows.go terminal_windows.go
deleted file mode 100644
index b4ef5286c..000000000
--- 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 text_formatter.go text_formatter.go
index fb21649c9..be2c6efe5 100644
--- text_formatter.go
+++ text_formatter.go
@@ -6,24 +6,21 @@ import (
 	"os"
 	"runtime"
 	"sort"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
+	"unicode/utf8"
 )
 
 const (
-	nocolor = 0
-	red     = 31
-	green   = 32
-	yellow  = 33
-	blue    = 36
-	gray    = 37
+	red    = 31
+	yellow = 33
+	blue   = 36
+	gray   = 37
 )
 
-var (
-	baseTimestamp time.Time
-	emptyFieldMap FieldMap
-)
+var baseTimestamp time.Time
 
 func init() {
 	baseTimestamp = time.Now()
@@ -37,6 +34,14 @@ type TextFormatter struct {
 	// Force disabling colors.
 	DisableColors bool
 
+	// Force quoting of all values
+	ForceQuote bool
+
+	// DisableQuote disables quoting for all values.
+	// DisableQuote will have a lower priority than ForceQuote.
+	// If both of them are set to true, quote will be forced on all values.
+	DisableQuote bool
+
 	// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
 	EnvironmentOverrideColors bool
 
@@ -48,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
@@ -62,6 +70,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
 
@@ -77,15 +89,27 @@ type TextFormatter struct {
 	//         FieldKeyMsg:   "@message"}}
 	FieldMap FieldMap
 
+	// CallerPrettyfier can be set by the user to modify the content
+	// 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 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
 		}
 	}
 }
@@ -94,11 +118,10 @@ func (f *TextFormatter) isColored() bool {
 	isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
 
 	if f.EnvironmentOverrideColors {
-		if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
+		switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
+		case ok && force != "0":
 			isColored = true
-		} else if ok && force == "0" {
-			isColored = false
-		} else if os.Getenv("CLICOLOR") == "0" {
+		case ok && force == "0", os.Getenv("CLICOLOR") == "0":
 			isColored = false
 		}
 	}
@@ -118,6 +141,8 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
 		keys = append(keys, k)
 	}
 
+	var funcVal, fileVal string
+
 	fixedKeys := make([]string, 0, 4+len(data))
 	if !f.DisableTimestamp {
 		fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
@@ -130,8 +155,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 {
@@ -166,6 +202,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
 	if f.isColored() {
 		f.printColored(b, entry, keys, data, timestampFormat)
 	} else {
+
 		for _, key := range fixedKeys {
 			var value interface{}
 			switch {
@@ -178,9 +215,9 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
 			case key == f.FieldMap.resolve(FieldKeyLogrusError):
 				value = entry.err
 			case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
-				value = entry.Caller.Function
+				value = funcVal
 			case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
-				value = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
+				value = fileVal
 			default:
 				value = data[key]
 			}
@@ -201,31 +238,54 @@ 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
 	}
 
 	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() {
-		caller = fmt.Sprintf("%s:%d %s()",
-			entry.Caller.File, entry.Caller.Line, entry.Caller.Function)
+		funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
+		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
+
+		if f.CallerPrettyfier != nil {
+			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
+		}
+
+		if fileVal == "" {
+			caller = funcVal
+		} else if funcVal == "" {
+			caller = fileVal
+		} else {
+			caller = fileVal + " " + funcVal
+		}
 	}
 
-	if f.DisableTimestamp {
+	switch {
+	case f.DisableTimestamp:
 		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
-	} else if !f.FullTimestamp {
+	case !f.FullTimestamp:
 		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
-	} else {
+	default:
 		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
 	}
 	for _, k := range keys {
@@ -236,9 +296,15 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
 }
 
 func (f *TextFormatter) needsQuoting(text string) bool {
+	if f.ForceQuote {
+		return true
+	}
 	if f.QuoteEmptyFields && len(text) == 0 {
 		return true
 	}
+	if f.DisableQuote {
+		return false
+	}
 	for _, ch := range text {
 		if !((ch >= 'a' && ch <= 'z') ||
 			(ch >= 'A' && ch <= 'Z') ||
diff --git text_formatter_test.go text_formatter_test.go
index 9c5e6f0a1..5b1cc0ab5 100644
--- text_formatter_test.go
+++ text_formatter_test.go
@@ -59,6 +59,7 @@ func TestQuoting(t *testing.T) {
 	checkQuoting(false, "foo@bar")
 	checkQuoting(false, "foobar^")
 	checkQuoting(false, "+/-_^@f.oobar")
+	checkQuoting(true, "foo\n\rbar")
 	checkQuoting(true, "foobar$")
 	checkQuoting(true, "&foobar")
 	checkQuoting(true, "x y")
@@ -70,7 +71,30 @@ func TestQuoting(t *testing.T) {
 	tf.QuoteEmptyFields = true
 	checkQuoting(true, "")
 	checkQuoting(false, "abcd")
+	checkQuoting(true, "foo\n\rbar")
 	checkQuoting(true, errors.New("invalid argument"))
+
+	// Test forcing quotes.
+	tf.ForceQuote = true
+	checkQuoting(true, "")
+	checkQuoting(true, "abcd")
+	checkQuoting(true, "foo\n\rbar")
+	checkQuoting(true, errors.New("invalid argument"))
+
+	// Test forcing quotes when also disabling them.
+	tf.DisableQuote = true
+	checkQuoting(true, "")
+	checkQuoting(true, "abcd")
+	checkQuoting(true, "foo\n\rbar")
+	checkQuoting(true, errors.New("invalid argument"))
+
+	// Test disabling quotes
+	tf.ForceQuote = false
+	tf.QuoteEmptyFields = false
+	checkQuoting(false, "")
+	checkQuoting(false, "abcd")
+	checkQuoting(false, "foo\n\rbar")
+	checkQuoting(false, errors.New("invalid argument"))
 }
 
 func TestEscaping(t *testing.T) {
@@ -172,6 +196,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 000000000..5254435ca
--- /dev/null
+++ travis/cross_build.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$GO111MODULE" == "on" ]]; then
+    $(go env GOPATH)/bin/gox -build-lib
+fi
diff --git a/travis/install.sh b/travis/install.sh
new file mode 100755
index 000000000..837c82d06
--- /dev/null
+++ travis/install.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+set -e
+
+# 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 writer.go writer.go
index 9e1f75135..074fd4b8b 100644
--- writer.go
+++ writer.go
@@ -4,25 +4,35 @@ import (
 	"bufio"
 	"io"
 	"runtime"
+	"strings"
 )
 
+// Writer at INFO level. See WriterLevel for details.
 func (logger *Logger) Writer() *io.PipeWriter {
 	return logger.WriterLevel(InfoLevel)
 }
 
+// WriterLevel returns an io.Writer that can be used to write arbitrary text to
+// the logger at the given log level. Each line written to the writer will be
+// printed in the usual way using formatters and hooks. The writer is part of an
+// io.Pipe and it is the callers responsibility to close the writer when done.
+// This can be used to override the standard library logger easily.
 func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
 	return NewEntry(logger).WriterLevel(level)
 }
 
+// Writer returns an io.Writer that writes to the logger at the info log level
 func (entry *Entry) Writer() *io.PipeWriter {
 	return entry.WriterLevel(InfoLevel)
 }
 
+// WriterLevel returns an io.Writer that writes to the logger at the given log level
 func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
 	reader, writer := io.Pipe()
 
 	var printFunc func(args ...interface{})
 
+	// Determine which log function to use based on the specified log level
 	switch level {
 	case TraceLevel:
 		printFunc = entry.Trace
@@ -42,23 +52,51 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
 		printFunc = entry.Print
 	}
 
+	// Start a new goroutine to scan the input and write it to the logger using the specified print function.
+	// It splits the input into chunks of up to 64KB to avoid buffer overflows.
 	go entry.writerScanner(reader, printFunc)
+
+	// Set a finalizer function to close the writer when it is garbage collected
 	runtime.SetFinalizer(writer, writerFinalizer)
 
 	return writer
 }
 
+// writerScanner scans the input from the reader and writes it to the logger
 func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
 	scanner := bufio.NewScanner(reader)
+
+	// Set the buffer size to the maximum token size to avoid buffer overflows
+	scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize)
+
+	// Define a split function to split the input into chunks of up to 64KB
+	chunkSize := bufio.MaxScanTokenSize // 64KB
+	splitFunc := func(data []byte, atEOF bool) (int, []byte, error) {
+		if len(data) >= chunkSize {
+			return chunkSize, data[:chunkSize], nil
+		}
+
+		return bufio.ScanLines(data, atEOF)
+	}
+
+	// Use the custom split function to split the input
+	scanner.Split(splitFunc)
+
+	// Scan the input and write it to the logger using the specified print function
 	for scanner.Scan() {
-		printFunc(scanner.Text())
+		printFunc(strings.TrimRight(scanner.Text(), "\r\n"))
 	}
+
+	// If there was an error while scanning the input, log an error
 	if err := scanner.Err(); err != nil {
 		entry.Errorf("Error while reading from Writer: %s", err)
 	}
+
+	// Close the reader when we are done
 	reader.Close()
 }
 
+// WriterFinalizer is a finalizer function that closes then given writer when it is garbage collected
 func writerFinalizer(writer *io.PipeWriter) {
 	writer.Close()
 }
diff --git a/writer_test.go b/writer_test.go
new file mode 100644
index 000000000..5b6261bd1
--- /dev/null
+++ writer_test.go
@@ -0,0 +1,98 @@
+package logrus_test
+
+import (
+	"bufio"
+	"bytes"
+	"log"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func ExampleLogger_Writer_httpServer() {
+	logger := logrus.New()
+	w := logger.Writer()
+	defer w.Close()
+
+	srv := http.Server{
+		// create a stdlib log.Logger that writes to
+		// logrus.Logger.
+		ErrorLog: log.New(w, "", 0),
+	}
+
+	if err := srv.ListenAndServe(); err != nil {
+		logger.Fatal(err)
+	}
+}
+
+func ExampleLogger_Writer_stdlib() {
+	logger := logrus.New()
+	logger.Formatter = &logrus.JSONFormatter{}
+
+	// Use logrus for standard log output
+	// Note that `log` here references stdlib's log
+	// Not logrus imported under the name `log`.
+	log.SetOutput(logger.Writer())
+}
+
+func TestWriterSplitNewlines(t *testing.T) {
+	buf := bytes.NewBuffer(nil)
+	logger := logrus.New()
+	logger.Formatter = &logrus.TextFormatter{
+		DisableColors:    true,
+		DisableTimestamp: true,
+	}
+	logger.SetOutput(buf)
+	writer := logger.Writer()
+
+	const logNum = 10
+
+	for i := 0; i < logNum; i++ {
+		_, err := writer.Write([]byte("bar\nfoo\n"))
+		assert.NoError(t, err, "writer.Write failed")
+	}
+	writer.Close()
+	// Test is flaky because it writes in another goroutine,
+	// we need to make sure to wait a bit so all write are done.
+	time.Sleep(500 * time.Millisecond)
+
+	lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
+	assert.Len(t, lines, logNum*2, "logger printed incorrect number of lines")
+}
+
+func TestWriterSplitsMax64KB(t *testing.T) {
+	buf := bytes.NewBuffer(nil)
+	logger := logrus.New()
+	logger.Formatter = &logrus.TextFormatter{
+		DisableColors:    true,
+		DisableTimestamp: true,
+	}
+	logger.SetOutput(buf)
+	writer := logger.Writer()
+
+	// write more than 64KB
+	const bigWriteLen = bufio.MaxScanTokenSize + 100
+	output := make([]byte, bigWriteLen)
+	// lets not write zero bytes
+	for i := 0; i < bigWriteLen; i++ {
+		output[i] = 'A'
+	}
+
+	for i := 0; i < 3; i++ {
+		len, err := writer.Write(output)
+		assert.NoError(t, err, "writer.Write failed")
+		assert.Equal(t, bigWriteLen, len, "bytes written")
+	}
+	writer.Close()
+	// Test is flaky because it writes in another goroutine,
+	// we need to make sure to wait a bit so all write are done.
+	time.Sleep(500 * time.Millisecond)
+
+	lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
+	// we should have 4 lines because we wrote more than 64 KB each time
+	assert.Len(t, lines, 4, "logger printed incorrect number of lines")
+}

Here's my review of the PR:

Description

This PR introduces several major updates to the Logrus logging library including:

  1. Migration from Travis CI to GitHub Actions
  2. Buffer pool management improvements
  3. New JSON/Text formatter features
  4. Race condition fixes
  5. Enhanced terminal color support
  6. Windows terminal improvements
  7. New functionality for logging levels and hooks

The motivation appears to be modernizing the codebase and fixing several longstanding issues.

Possible Issues

  1. The buffer size for writer scanning is set to bufio.MaxScanTokenSize (64KB) which could be problematic for memory-constrained environments
  2. Multiple synchronization mechanisms (mutex, sync.Pool) are used across different components which could lead to subtle deadlock issues if not handled carefully

Security Hotspots

  1. Terminal detection/color support changes could potentially expose terminal capabilities in logs which should be reviewed for sensitive environments
  2. File permissions for log file creation are not validated which could lead to privilege escalation in some scenarios
Changes

Changes

By filename:

  • buffer_pool.go: New buffer pool management API
  • entry.go: Race condition fixes and context support
  • json_formatter.go/text_formatter.go: New formatting options and customization
  • terminal_check_*.go: Improved terminal detection across platforms
  • writer.go: Enhanced writer with chunking support
  • CI files: Migration to GitHub Actions

The diagram shows the basic flow of a log operation with the new buffer pool management.

Let me know if you would like me to expand on any part of this review.```mermaid
sequenceDiagram
participant App
participant Logger
participant Entry
participant BufferPool
participant Formatter

App->>Logger: Log message
Logger->>BufferPool: Get buffer
Logger->>Entry: Create entry
Entry->>Formatter: Format entry
Formatter->>BufferPool: Return buffer
Logger-->>App: Done
</details>

<!-- Generated by claude-3-5-sonnet-20241022 -->

@renovate renovate bot changed the title fix(deps): update module github.com/sirupsen/logrus to v1.9.3 fix(deps): update module github.com/sirupsen/logrus to v1.9.3 - autoclosed Apr 15, 2025
@renovate renovate bot closed this Apr 15, 2025
@renovate renovate bot deleted the renovate/github.colasdn.workers.dev-sirupsen-logrus-1.x branch April 15, 2025 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants