vendor: github.com/sirupsen/logrus v1.10.1

Notable changes:

- Add bidirectional `log/slog` integration with a Logrus hook and `slog.Handler`.
- Add minimal, composable logging interfaces for individual log levels.
- Fix reentrant logging deadlocks and improve concurrency safety around formatters, hooks, and entries.
- Fix generic `Log`, `Logf`, `Logln`, and `LogFn` methods unexpectedly panicking at `PanicLevel`.
- Allow `Entry.Caller` to be set explicitly and preserved across derived entries.
- Improve `TextFormatter` performance and reduce allocations significantly.
- Improve common Logger and Entry hot paths and caller-reporting performance.
- Update `TextFormatter` handling for `[]byte`, debug/trace colors, and Windows ANSI terminals.
- Raise the minimum supported Go version to Go 1.23.
- Deprecate `Entry.HasCaller` and `MutexWrap`.

release-notes: https://github.com/sirupsen/logrus/releases/tag/v1.10.0
release-notes: https://github.com/sirupsen/logrus/releases/tag/v1.10.1
full diff: https://github.com/sirupsen/logrus/compare/v1.9.4...v1.10.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-08-19 11:34:53 +02:00
parent 8659aa5d4c
commit 58d9cdd2d9
29 changed files with 1354 additions and 809 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/sirupsen/logrus v1.9.4
github.com/sirupsen/logrus v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
+4 -6
View File
@@ -32,7 +32,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
@@ -153,7 +152,6 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -176,8 +174,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -187,8 +185,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 h1:TvtdmeYsYEij78hS4oxnwikoiLdIrgav3BA+CbhaDAI=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+10 -38
View File
@@ -1,12 +1,9 @@
version: "2"
run:
tests: false
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errchkjson
@@ -22,46 +19,21 @@ linters:
- nilerr
- nilnesserr
- noctx
- protogetter
- reassign
- recvcheck
- rowserrcheck
- spancheck
- sqlclosecheck
- testifylint
- unparam
- zerologlint
disable:
- prealloc
settings:
errcheck:
check-type-assertions: false
check-blank: false
lll:
line-length: 100
tab-width: 4
prealloc:
simple: false
range-loops: false
for-loops: false
whitespace:
multi-if: false
multi-func: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gosec
- musttag
- noctx # TODO: enable once we switch to Go 1.24+.
- linters: # TODO: remove once golangci-lint is updated with https://github.com/golangci/golangci-lint/pull/6584
- gocheckcompilerdirectives
text: 'compiler directive unrecognized: //go:fix'
-15
View File
@@ -1,15 +0,0 @@
language: go
go_import_path: github.com/sirupsen/logrus
git:
depth: 1
env:
- 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
+201 -38
View File
@@ -1,90 +1,246 @@
# 1.8.1
# Changelog
All notable changes to this project will be documented in this file.
## 1.10.1
Fixes:
* Fix a regression introduced in v1.10.0 where `TextFormatter` could panic
when formatting nil or panicking `error` and `fmt.Stringer` values.
* Allow function-backed implementations of `error` as field values.
## 1.10.0
Fixes:
* Fix reentrant logging deadlocks in formatter paths.
* Fix race conditions in formatter and entry handling.
* Fix generic `Log`, `Logf`, `Logln`, and `LogFn` methods unexpectedly
panicking when called with `PanicLevel`. Use the corresponding `Panic`
methods when panic behavior is desired.
* Improve concurrency safety around formatter and hook access.
Features:
* Add `slog` hook for forwarding Logrus entries to `log/slog`.
* Add `slog.Handler` for forwarding `log/slog` records to a Logrus logger,
including levels, fields, groups, context, time, and optional caller
reporting. The hook and handler can also be combined to help migrate
between Logrus and `log/slog`.
* Add minimal, composable logging interfaces for each log level. This enables
consumers to depend on narrower interfaces, making it easier to substitute
or adapt logging implementations.
* Allow `Entry.Caller` to be set explicitly and preserve it across derived
entries, enabling custom caller detection without Logrus overwriting
caller information when `ReportCaller` is enabled.
Changed:
* Raise minimum supported Go version to 1.23.
* TextFormatter now renders `[]byte` values as raw/quoted strings instead of slice-of-ints.
* TextFormatter now uses distinct dimmed colors for debug and trace output.
* TextFormatter now automatically enables colors on Windows terminals with ANSI support,
matching the behavior on other platforms.
* `Entry.HasCaller` is now deprecated in favor of checking `Entry.Caller` directly.
* Deprecated `MutexWrap`, which was unintentionally exposed as public API.
It remains available as an alias for compatibility but should not be used
directly.
Performance:
* Significantly improve TextFormatter performance and reduce allocations.
* Optimize common Entry and Logger hot paths.
* Reduce allocations in caller reporting.
* ~17% lower geomean runtime and ~27% higher formatter throughput overall.
* Common enabled logging paths are ~3044% faster.
* TextFormatter paths are up to ~40% faster, with allocation counts reduced
by 2574% across the measured formatter cases.
## 1.9.4
Fixes:
* Remove uses of deprecated `ioutil` package
Features:
* Add GNU/Hurd support
* Add WASI wasip1 support
Code quality:
* Update minimum supported Go version to 1.17
* Documentation updates
## 1.9.3
Fixes:
* Re-apply fix for potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
* Fix panic in Writer
## 1.9.2
Fixes:
* Revert Writer DoS fix (#1376) due to regression
## 1.9.1
Fixes:
* Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
## 1.9.0
Fixes:
* Multiple concurrency and race condition fixes
* Improve Windows terminal and ANSI handling
Code quality:
* Internal cleanups and modernization
## 1.8.3
Fixes:
* Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
## 1.8.2
Features:
* Add support for the logger private buffer pool (#1253)
Fixes:
* Fix race condition for SetFormatter and SetReportCaller
* Fix data race in hooks test package
## 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
## 1.8.0
Correct versioning number replacing v1.7.1.
# 1.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
## 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
## 1.6.0
Fixes:
* end of line cleanup
* revert the entry concurrency bug fix which 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
## 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
## 1.4.2
* Fixes build break for plan9, nacl, solaris
# 1.4.1
## 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
## 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
## 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)
@@ -92,20 +248,26 @@ Fixes:
* Pointer to func as field in JSONFormatter (#870)
* Properly marshal Levels (#873)
# 1.2.0
## 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
* A new trace level named `Trace` whose level is below `Debug`
* A configurable exit function to be called upon a Fatal trace
* The `Level` object now implements `encoding.TextUnmarshaler` interface
# 1.1.1
## 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
## 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
@@ -122,9 +284,10 @@ This new release introduces:
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
## 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
@@ -134,71 +297,71 @@ This new release introduces:
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5
## 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
## 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3
## 1.0.3
* Replace example files with testable examples
# 1.0.2
## 1.0.2
* bug: quote non-string values in text formatter (#583)
* Make (*Logger) SetLevel a public method
# 1.0.1
## 1.0.1
* bug: fix escaping in text formatter (#575)
# 1.0.0
## 1.0.0
* Officially changed name to lower-case
* bug: colors on Windows 10 (#541)
* bug: fix race in accessing level (#512)
# 0.11.5
## 0.11.5
* feature: add writer and writerlevel to entry (#372)
# 0.11.4
## 0.11.4
* bug: fix undefined variable on solaris (#493)
# 0.11.3
## 0.11.3
* formatter: configure quoting of empty values (#484)
* formatter: configure quoting character (default is `"`) (#484)
* bug: fix not importing io correctly in non-linux environments (#481)
# 0.11.2
## 0.11.2
* bug: fix windows terminal detection (#476)
# 0.11.1
## 0.11.1
* bug: fix tty detection with custom out (#471)
# 0.11.0
## 0.11.0
* performance: Use bufferpool to allocate (#370)
* terminal: terminal detection for app-engine (#343)
* feature: exit handler (#375)
# 0.10.0
## 0.10.0
* feature: Add a test hook (#180)
* feature: `ParseLevel` is now case-insensitive (#326)
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
* performance: avoid re-allocations on `WithFields` (#335)
# 0.9.0
## 0.9.0
* logrus/text_formatter: don't emit empty msg
* logrus/hooks/airbrake: move out of main repository
@@ -210,25 +373,25 @@ This new release introduces:
* logrus/core: support `WithError` on logger
* logrus/core: Solaris support
# 0.8.7
## 0.8.7
* logrus/core: fix possible race (#216)
* logrus/doc: small typo fixes and doc improvements
# 0.8.6
## 0.8.6
* hooks/raven: allow passing an initialized client
# 0.8.5
## 0.8.5
* logrus/core: revert #208
# 0.8.4
## 0.8.4
* formatter/text: fix data race (#218)
# 0.8.3
## 0.8.3
* logrus/core: fix entry log level (#208)
* logrus/core: improve performance of text formatter by 40%
@@ -236,24 +399,24 @@ This new release introduces:
* logrus/core: add support for DragonflyBSD and NetBSD
* formatter/text: print structs more verbosely
# 0.8.2
## 0.8.2
* logrus: fix more Fatal family functions
# 0.8.1
## 0.8.1
* logrus: fix not exiting on `Fatalf` and `Fatalln`
# 0.8.0
## 0.8.0
* logrus: defaults to stderr instead of stdout
* hooks/sentry: add special field for `*http.Request`
* formatter/text: ignore Windows for colors
# 0.7.3
## 0.7.3
* formatter/\*: allow configuration of timestamp layout
# 0.7.2
## 0.7.2
* formatter/text: Add configuration option for time format (#158)
+39 -53
View File
@@ -3,13 +3,10 @@
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).
**Logrus is in maintenance mode.** The project focuses on security, bug fixes,
and performance improvements. New features are not planned, aside from changes
required to provide interoperability with other logging ecosystems (e.g., Go's
[log/slog](https://pkg.go.dev/log/slog)).
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
@@ -23,18 +20,6 @@ about structured logging in Go today. Check out, for example,
[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
experienced problems with the upper-case variant, so the lower-case was decided.
Everything using `logrus` will need to use the lower-case:
`github.com/sirupsen/logrus`. Any package that isn't, should be changed.
To fix Glide, see [these
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).
Nicely color-coded in development (when a TTY is attached, otherwise just
plain text):
@@ -43,35 +28,27 @@ plain text):
With `logrus.SetFormatter(&logrus.JSONFormatter{})`, for easy parsing by logstash
or Splunk:
```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"}
{"level":"warning","msg":"The group's number increased tremendously!",
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```json lines
{"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"}
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!","size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.","size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```
With the default `logrus.SetFormatter(&logrus.TextFormatter{})` when a TTY is not
attached, the output is compatible with the
[logfmt](https://pkg.go.dev/github.com/kr/logfmt) format:
```text
```bash
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" animal=orca err="It's over 9000!" number=100 omg=true size=9009
```
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
```go
@@ -88,29 +65,32 @@ If you wish to add the calling method as a field, instruct the logger via:
```go
logrus.SetReportCaller(true)
```
This adds the caller as 'method' like so:
```json
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by","time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
```
```text
```bash
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
```
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:
```bash
go test -bench=.*CallerTracing
go test -bench=ReportCaller
```
#### Case-sensitivity
The organization's name was changed to lower-case--and this will not be changed
back. If you are getting import conflicts due to case sensitivity, please use
the lower-case import: `github.com/sirupsen/logrus`.
The organization's name was [changed to lower-case][1]. If you are getting import
conflicts due to case sensitivity, please use the lower-case import:
`github.com/sirupsen/logrus`.
[1]: https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276
#### Example
@@ -289,6 +269,7 @@ func init() {
}
}
```
Note: Syslog hooks also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
@@ -367,18 +348,21 @@ Splunk or Logstash.
The built-in logging formatters are:
* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
without colors.
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
* [`logrus.TextFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter)
logs the event in colors if the logger output is a TTY, otherwise without colors.
* To force colored output when there is no TTY, set the `ForceColors`
field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
`DisableColors` field to `true`.
* On modern Windows terminals with ANSI (Virtual Terminal) support, TextFormatter
automatically enables colored output.
* If your environment does not support ANSI escape sequences, wrap the logger output
using [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable)
and set `ForceColors` (or `CLICOLOR_FORCE=1`) to enable colors through the wrapper.
* 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://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter).
* [`logrus.JSONFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter)
logs fields as JSON.
Third-party logging formatters:
@@ -390,10 +374,12 @@ Third-party logging formatters:
* [`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; Save log to files.
* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
* [`easy-logrus-formatter`](https://github.com/WeiZhixiong/easy-logrus-formatter). Provide a user-friendly formatter for logrus.
* [`redactrus`](https://github.com/ibreakthecloud/redactrus). Redacts sensitive information like password, apikeys, email, etc. from logs.
You can define your formatter by implementing the `Formatter` interface,
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
`Fields` type (`map[string]interface{}`) with all your fields as well as the
`Fields` type (`map[string]any`) with all your fields as well as the
default ones (see Entries section above):
```go
@@ -516,4 +502,4 @@ Situations when locking is not needed include:
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/)
(Refer to <http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/>)
+2 -2
View File
@@ -57,7 +57,7 @@ func Exit(code int) {
//
// 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 database connections, or sending an alert that the application is
// closing.
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
@@ -69,7 +69,7 @@ func RegisterExitHandler(handler func()) {
//
// 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 database connections, or sending an alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
+8 -14
View File
@@ -5,9 +5,13 @@ import (
"sync"
)
var (
bufferPool BufferPool
)
var bufferPool BufferPool = &defaultPool{
pool: &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
type BufferPool interface {
Put(*bytes.Buffer)
@@ -27,17 +31,7 @@ func (p *defaultPool) Get() *bytes.Buffer {
}
// SetBufferPool allows to replace the default logrus buffer pool
// to better meets the specific needs of an application.
// to better meet 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)
},
},
})
}
+13 -13
View File
@@ -1,25 +1,25 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
package main
import (
log "github.com/sirupsen/logrus"
)
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/sirupsen/logrus
*/
+268 -146
View File
@@ -4,9 +4,11 @@ import (
"bytes"
"context"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
@@ -17,8 +19,10 @@ var (
// qualified package name, cached at first use
logrusPackage string
// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// Positions in the call stack when tracing to report the calling method.
//
// Start at the bottom of the stack before the package-name cache is primed.
minimumCallerDepth = 1
// Used for caller information initialisation
callerInitOnce sync.Once
@@ -29,68 +33,116 @@ const (
knownLogrusFrames int = 4
)
func init() {
// start at the bottom of the stack before the package-name cache is primed
minimumCallerDepth = 1
}
// ErrorKey defines the key when adding errors using [WithError], [Logger.WithError].
var ErrorKey = "error"
// Entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// reused and passed around as much as you wish to avoid field duplication.
// Entry represents a single log event. It may be either an intermediate
// entry (created via WithField(s), WithContext, etc.) or a final entry
// that is emitted when one of the level methods (Trace, Debug, Info,
// Warn, Error, Fatal, Panic) is called.
//
//nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver.
// An Entry always belongs to a Logger. A nil Logger is invalid and will
// cause a panic when the entry is logged. Use [NewEntry] or Logger methods
// to construct entries.
//
// Entries are safe to reuse for adding fields and may be passed around
// to avoid field duplication. Each log operation operates on a copy
// of the Entrys data to avoid mutation during formatting.
//
//nolint:recvcheck // Entry methods intentionally use both pointer and value receivers.
type Entry struct {
// Logger is the Logger that owns this entry and is responsible for
// formatting, hooks, and output. It must not be nil. An Entry without
// a Logger is invalid and will panic when logged.
Logger *Logger
// Contains all the fields set by the user.
// Data contains all user-defined fields attached to this entry.
Data Fields
// Time at which the log entry was created
// Time is the timestamp for the log event. If zero when the entry is
// logged, it defaults to the current time.
Time time.Time
// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
// Level is the severity of the log entry. It is set when the entry
// is fired and reflects the level used for that log call.
Level Level
// Calling method, with package name
// Caller contains the calling method information.
//
// When [Logger.ReportCaller] is enabled, Caller is populated automatically at
// log time if it is nil. Hooks and formatters may inspect Caller.
//
// Applications generally should not modify Caller unless they intentionally
// want to provide custom caller information.
Caller *runtime.Frame
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
// Message is the log message supplied to one of the logging methods
// (Trace, Debug, Info, Warn, Error, Fatal, or Panic). It is set when
// the entry is logged.
Message string
// When formatter is called in entry.log(), a Buffer may be set to entry
// Buffer is a reusable buffer provided to the formatter. It is set
// before formatting in the normal log path; when nil, formatters
// allocate their own.
Buffer *bytes.Buffer
// Contains the context set by the user. Useful for hook processing etc.
// Context carries user-provided context for hooks and formatters.
Context context.Context
// err may contain a field formatting error
// err contains internal field-formatting errors.
err string
}
// NewEntry creates a new [Entry] associated with the provided Logger.
// The logger must not be nil. Passing a nil logger results in a
// panic when a logging method (e.g., [Entry.Info], [Entry.Error], etc.)
// is called.
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, plus one optional. Give a little extra room.
Data: make(Fields, 6),
// Reserve default predefined fields and a little extra room.
Data: make(Fields, defaultFields+3),
}
}
// Dup creates a copy of the entry for further modification.
//
// Data is cloned to avoid mutating the original entry. Other fields
// (Logger, Time, Context, etc.) are copied by value.
func (entry *Entry) Dup() *Entry {
data := make(Fields, len(entry.Data))
for k, v := range entry.Data {
data[k] = v
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
return dup
}
// dup copies the entry fields shared by derived entries except Data, which
// callers must copy or initialize as appropriate for their use.
func (entry *Entry) dup() *Entry {
return &Entry{
Logger: entry.Logger,
Time: entry.Time,
Caller: entry.Caller,
Context: entry.Context,
err: entry.err,
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
}
// Bytes returns the bytes representation of this entry from the formatter.
func (entry *Entry) Bytes() ([]byte, error) {
return entry.Logger.Formatter.Format(entry)
// Snapshot the formatter under the lock to protect against concurrent
// SetFormatter calls, then release the lock before formatting.
// This avoids a data race and prevents a deadlock if Format() triggers
// reentrant logging (e.g., a field's MarshalJSON calls logrus).
//
// See:
//
// - https://github.com/sirupsen/logrus/issues/1440
// - https://github.com/sirupsen/logrus/issues/1448
entry.Logger.mu.Lock()
formatter := entry.Logger.Formatter
entry.Logger.mu.Unlock()
return formatter.Format(entry)
}
// String returns the string representation from the reader and ultimately the
@@ -112,58 +164,61 @@ func (entry *Entry) WithError(err error) *Entry {
// WithContext adds 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}
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.Context = ctx
return dup
}
// WithField adds a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
func (entry *Entry) WithField(key string, value any) *Entry {
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.addField(key, value)
return dup
}
// WithFields adds a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
dup := entry.dup()
dup.Data = make(Fields, len(entry.Data)+len(fields))
maps.Copy(dup.Data, entry.Data)
for key, value := range fields {
dup.addField(key, value)
}
fieldErr := entry.err
for k, v := range fields {
isErrField := false
if t := reflect.TypeOf(v); t != nil {
switch {
case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
isErrField = true
}
}
if isErrField {
tmp := fmt.Sprintf("can not add field %q", k)
if fieldErr != "" {
fieldErr = entry.err + ", " + tmp
} else {
fieldErr = tmp
}
} else {
data[k] = v
}
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
return dup
}
// WithTime overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.Time = t
return dup
}
func (entry *Entry) addField(key string, value any) {
if _, ok := value.(error); !ok {
t := reflect.TypeOf(value)
if t != nil && (t.Kind() == reflect.Func || t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Func) {
if entry.err != "" {
entry.err += ", skipping unsupported field " + strconv.Quote(key)
} else {
entry.err = "skipping unsupported field " + strconv.Quote(key)
}
return
}
}
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
if entry.Data == nil {
entry.Data = make(Fields, 1)
}
entry.Data[key] = value
}
// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
// There really ought to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
@@ -186,7 +241,7 @@ func getCaller() *runtime.Frame {
_ = runtime.Callers(0, pcs)
// dynamic get the package name and the minimum caller depth
for i := 0; i < maximumCallerDepth; i++ {
for i := range maximumCallerDepth {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
logrusPackage = getPackageName(funcName)
@@ -215,16 +270,47 @@ func getCaller() *runtime.Frame {
return nil
}
func (entry Entry) HasCaller() (has bool) {
return entry.Logger != nil &&
entry.Logger.ReportCaller &&
entry.Caller != nil
// HasCaller reports whether this Entry contains caller information.
//
// Caller may be set explicitly, or populated at log time when
// [Logger.ReportCaller] is enabled.
//
// Deprecated: use [Entry.Caller] != nil instead.
//
//go:fix inline
func (entry Entry) HasCaller() bool {
return entry.Caller != nil
}
func (entry *Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
func (entry *Entry) logArgs(level Level, panicAfter bool, args ...any) {
entry.log(level, panicAfter, sprint(args...))
}
newEntry := entry.Dup()
func (entry *Entry) logf(level Level, panicAfter bool, format string, args ...any) {
entry.log(level, panicAfter, fmt.Sprintf(format, args...))
}
// logln uses Sprintln for multiple arguments to preserve Println-style
// spacing between args, then trims the trailing newline.
func (entry *Entry) logln(level Level, panicAfter bool, args ...any) {
if len(args) <= 1 {
entry.log(level, panicAfter, sprint(args...))
return
}
msg := fmt.Sprintln(args...)
msg = msg[:len(msg)-1] // Trim the newline added by Sprintln; logging adds its own.
entry.log(level, panicAfter, msg)
}
// log writes msg at level. If panicAfter is true, it panics with the fully
// populated entry after hooks and output have completed.
//
// The explicit flag keeps panic behavior limited to Panic, Panicf, and
// Panicln while avoiding a return value used only as the panic value.
// See #1283 and commits f96066e and 5f8c666.
func (entry *Entry) log(level Level, panicAfter bool, msg string) {
newEntry := entry.dup()
newEntry.Data = maps.Clone(entry.Data)
if newEntry.Time.IsZero() {
newEntry.Time = time.Now()
@@ -233,17 +319,24 @@ func (entry *Entry) log(level Level, msg string) {
newEntry.Level = level
newEntry.Message = msg
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
logger := newEntry.Logger
logger.mu.Lock()
reportCaller := logger.ReportCaller
bufPool := newEntry.getBufferPool()
newEntry.Logger.mu.Unlock()
logger.mu.Unlock()
if reportCaller {
// Preserve explicitly set caller information.
if reportCaller && newEntry.Caller == nil {
newEntry.Caller = getCaller()
}
newEntry.fireHooks()
buffer = bufPool.Get()
// Select hooks based on the level for this log call. Hooks receive the
// Entry and may mutate it, but that does not affect which hooks are
// fired for this event.
hooks := logger.hooksForLevel(level)
newEntry.fireHooks(hooks)
buffer := bufPool.Get()
defer func() {
newEntry.Buffer = nil
buffer.Reset()
@@ -251,15 +344,12 @@ func (entry *Entry) log(level Level, msg string) {
}()
buffer.Reset()
newEntry.Buffer = buffer
newEntry.write()
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 here so the panic value contains the fully populated entry without
// requiring log to return it to the caller.
if panicAfter {
panic(newEntry)
}
}
@@ -271,175 +361,207 @@ func (entry *Entry) getBufferPool() (pool BufferPool) {
return bufferPool
}
func (entry *Entry) fireHooks() {
var tmpHooks LevelHooks
entry.Logger.mu.Lock()
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)
func (entry *Entry) fireHooks(hooks []Hook) {
for _, hook := range hooks {
if err := hook.Fire(entry); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to fire hook:", err)
return
}
}
}
func (entry *Entry) write() {
// Snapshot the formatter under the lock to protect against concurrent
// SetFormatter calls, then release the lock before formatting.
// This avoids a deadlock when Format() triggers reentrant logging (e.g.,
// a field's MarshalJSON calls logrus). See #1448, #1440.
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
formatter := entry.Logger.Formatter
entry.Logger.mu.Unlock()
serialized, err := formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
_, _ = fmt.Fprintln(os.Stderr, "Failed to format entry:", err)
return
}
// Re-acquire the lock to serialize writes to the underlying io.Writer.
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
_, _ = fmt.Fprintln(os.Stderr, "Failed to write to log:", 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{}) {
// Log logs a message at the specified level.
//
// Using Log with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Log treats the level as logging severity only;
// use [Entry.Panic] or [Entry.Fatal] when those side effects are desired.
func (entry *Entry) Log(level Level, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.log(level, fmt.Sprint(args...))
entry.logArgs(level, panicAfter, args...)
}
}
func (entry *Entry) Trace(args ...interface{}) {
func (entry *Entry) Trace(args ...any) {
entry.Log(TraceLevel, args...)
}
func (entry *Entry) Debug(args ...interface{}) {
func (entry *Entry) Debug(args ...any) {
entry.Log(DebugLevel, args...)
}
func (entry *Entry) Print(args ...interface{}) {
func (entry *Entry) Print(args ...any) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
func (entry *Entry) Info(args ...any) {
entry.Log(InfoLevel, args...)
}
func (entry *Entry) Warn(args ...interface{}) {
func (entry *Entry) Warn(args ...any) {
entry.Log(WarnLevel, args...)
}
func (entry *Entry) Warning(args ...interface{}) {
func (entry *Entry) Warning(args ...any) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
func (entry *Entry) Error(args ...any) {
entry.Log(ErrorLevel, args...)
}
func (entry *Entry) Fatal(args ...interface{}) {
func (entry *Entry) Fatal(args ...any) {
entry.Log(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
entry.Log(PanicLevel, args...)
func (entry *Entry) Panic(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logArgs(PanicLevel, panicAfter, args...)
}
}
// Entry Printf family functions
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
// Logf logs a formatted message at the specified level.
//
// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logf treats the level as logging severity only;
// use [Entry.Panicf] or [Entry.Fatalf] when those side effects are desired.
func (entry *Entry) Logf(level Level, format string, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, fmt.Sprintf(format, args...))
entry.logf(level, panicAfter, format, args...)
}
}
func (entry *Entry) Tracef(format string, args ...interface{}) {
func (entry *Entry) Tracef(format string, args ...any) {
entry.Logf(TraceLevel, format, args...)
}
func (entry *Entry) Debugf(format string, args ...interface{}) {
func (entry *Entry) Debugf(format string, args ...any) {
entry.Logf(DebugLevel, format, args...)
}
func (entry *Entry) Infof(format string, args ...interface{}) {
func (entry *Entry) Infof(format string, args ...any) {
entry.Logf(InfoLevel, format, args...)
}
func (entry *Entry) Printf(format string, args ...interface{}) {
func (entry *Entry) Printf(format string, args ...any) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
func (entry *Entry) Warnf(format string, args ...any) {
entry.Logf(WarnLevel, format, args...)
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
func (entry *Entry) Warningf(format string, args ...any) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
func (entry *Entry) Errorf(format string, args ...any) {
entry.Logf(ErrorLevel, format, args...)
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
func (entry *Entry) Fatalf(format string, args ...any) {
entry.Logf(FatalLevel, format, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
entry.Logf(PanicLevel, format, args...)
func (entry *Entry) Panicf(format string, args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logf(PanicLevel, panicAfter, format, args...)
}
}
// Entry Println family functions
func (entry *Entry) Logln(level Level, args ...interface{}) {
// Logln logs a message at the specified level with Println-style spacing.
//
// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logln treats the level as logging severity only;
// use [Entry.Panicln] or [Entry.Fatalln] when those side effects are desired.
func (entry *Entry) Logln(level Level, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, entry.sprintlnn(args...))
entry.logln(level, panicAfter, args...)
}
}
func (entry *Entry) Traceln(args ...interface{}) {
func (entry *Entry) Traceln(args ...any) {
entry.Logln(TraceLevel, args...)
}
func (entry *Entry) Debugln(args ...interface{}) {
func (entry *Entry) Debugln(args ...any) {
entry.Logln(DebugLevel, args...)
}
func (entry *Entry) Infoln(args ...interface{}) {
func (entry *Entry) Infoln(args ...any) {
entry.Logln(InfoLevel, args...)
}
func (entry *Entry) Println(args ...interface{}) {
func (entry *Entry) Println(args ...any) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
func (entry *Entry) Warnln(args ...any) {
entry.Logln(WarnLevel, args...)
}
func (entry *Entry) Warningln(args ...interface{}) {
func (entry *Entry) Warningln(args ...any) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
func (entry *Entry) Errorln(args ...any) {
entry.Logln(ErrorLevel, args...)
}
func (entry *Entry) Fatalln(args ...interface{}) {
func (entry *Entry) Fatalln(args ...any) {
entry.Logln(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
entry.Logln(PanicLevel, args...)
func (entry *Entry) Panicln(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logln(PanicLevel, panicAfter, args...)
}
}
// sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
// sprint is fmt.Sprint with fast paths for zero or one string argument.
func sprint(args ...any) string {
switch len(args) {
case 0:
return ""
case 1:
if msg, ok := args[0].(string); ok {
return msg
}
}
return fmt.Sprint(args...)
}
+82 -87
View File
@@ -6,11 +6,12 @@ import (
"time"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
// std is the package-level standard logger, similar to the default logger
// in the stdlib [log] package.
var std = New()
// StandardLogger returns the package-level standard logger used by
// the top-level logging functions.
func StandardLogger() *Logger {
return std
}
@@ -41,7 +42,7 @@ func GetLevel() Level {
return std.GetLevel()
}
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
// IsLevelEnabled checks if logging for the given level is enabled for the standard logger.
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
}
@@ -51,9 +52,10 @@ func AddHook(hook Hook) {
std.AddHook(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
// WithError creates an entry from the standard logger and adds an error to it,
// using the value defined in [ErrorKey] as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
return std.WithError(err)
}
// WithContext creates an entry from the standard logger and adds a context to it.
@@ -61,210 +63,203 @@ 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`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
// WithField creates an entry from the standard logger and adds a single field.
// For multiple fields, prefer [WithFields] over chaining WithField calls.
func WithField(key string, value any) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
// WithFields creates an entry from the standard logger and adds the fields to it.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// 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
// or Panic on the Entry it returns.
// WithTime creates an entry from the standard logger and overrides the time
// used for logs generated with it.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
// Trace logs a message at level [TraceLevel] on the standard logger.
func Trace(args ...any) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
// Debug logs a message at level [DebugLevel] on the standard logger.
func Debug(args ...any) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
// Print logs a message at level [InfoLevel] on the standard logger.
func Print(args ...any) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
// Info logs a message at level [InfoLevel] on the standard logger.
func Info(args ...any) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
// Warn logs a message at level [WarnLevel] on the standard logger.
func Warn(args ...any) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
// Warning logs a message at level [WarnLevel] on the standard logger.
func Warning(args ...any) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
// Error logs a message at level [ErrorLevel] on the standard logger.
func Error(args ...any) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
// Panic logs a message at level [PanicLevel] on the standard logger.
func Panic(args ...any) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) {
// Fatal logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatal(args ...any) {
std.Fatal(args...)
}
// TraceFn logs a message from a func at level Trace on the standard logger.
// TraceFn logs a message from a func at level [TraceLevel] 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.
// DebugFn logs a message from a func at level [DebugLevel] 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.
// PrintFn logs a message from a func at level [InfoLevel] 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.
// InfoFn logs a message from a func at level [InfoLevel] 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.
// WarnFn logs a message from a func at level [WarnLevel] 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.
// WarningFn logs a message from a func at level [WarnLevel] 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.
// ErrorFn logs a message from a func at level [ErrorLevel] 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.
// PanicFn logs a message from a func at level [PanicLevel] 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.
// FatalFn logs a message from a func at level [FatalLevel] on the standard logger,
// then exits the process with status 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{}) {
// Tracef logs a message at level [TraceLevel] on the standard logger.
func Tracef(format string, args ...any) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
// Debugf logs a message at level [DebugLevel] on the standard logger.
func Debugf(format string, args ...any) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
// Printf logs a message at level [InfoLevel] on the standard logger.
func Printf(format string, args ...any) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
// Infof logs a message at level [InfoLevel] on the standard logger.
func Infof(format string, args ...any) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
// Warnf logs a message at level [WarnLevel] on the standard logger.
func Warnf(format string, args ...any) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
// Warningf logs a message at level [WarnLevel] on the standard logger.
func Warningf(format string, args ...any) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
// Errorf logs a message at level [ErrorLevel] on the standard logger.
func Errorf(format string, args ...any) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
// Panicf logs a message at level [PanicLevel] on the standard logger.
func Panicf(format string, args ...any) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) {
// Fatalf logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalf(format string, args ...any) {
std.Fatalf(format, args...)
}
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
// Traceln logs a message at level [TraceLevel] on the standard logger.
func Traceln(args ...any) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
// Debugln logs a message at level [DebugLevel] on the standard logger.
func Debugln(args ...any) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
// Println logs a message at level [InfoLevel] on the standard logger.
func Println(args ...any) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
// Infoln logs a message at level [InfoLevel] on the standard logger.
func Infoln(args ...any) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
// Warnln logs a message at level [WarnLevel] on the standard logger.
func Warnln(args ...any) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
// Warningln logs a message at level [WarnLevel] on the standard logger.
func Warningln(args ...any) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
// Errorln logs a message at level [ErrorLevel] on the standard logger.
func Errorln(args ...any) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
// Panicln logs a message at level [PanicLevel] on the standard logger.
func Panicln(args ...any) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) {
// Fatalln logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalln(args ...any) {
std.Fatalln(args...)
}
+30 -17
View File
@@ -2,27 +2,40 @@ package logrus
import "time"
// Default key names for the default fields
const (
// defaultTimestampFormat is the layout used to format entry timestamps
// when a formatter has not specified a custom TimestampFormat.
// It follows time.RFC3339 and is applied unless timestamps are disabled.
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
// defaultFields is the number of commonly included predefined log entry fields
// (msg, level, time). It is used as a capacity hint when constructing
// intermediate collections during formatting (for example, the fixed key list).
//
// It does not include the optional "logrus_error", "func", or "file" fields.
defaultFields = 3
)
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
)
// Formatter is implemented by types that format log entries. It receives an
// [*Entry], which contains:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
// - entry.Message: the message passed to logging methods such as [Info], [Warn], [Error]
// - entry.Time: the timestamp
// - entry.Level: the log level
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
// Additional fields added with [WithField] or [WithFields] are available in
// [Entry.Data]. Format should return the formatted log entry as a byte slice,
// which is written to [Logger.Out].
type Formatter interface {
Format(*Entry) ([]byte, error)
}
@@ -30,12 +43,12 @@ type Formatter interface {
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
+23 -14
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"runtime"
"strconv"
)
type fieldKey string
@@ -20,7 +21,13 @@ func (f FieldMap) resolve(key fieldKey) string {
return string(key)
}
// JSONFormatter formats logs into parsable json
// JSONFormatter formats logs into parsable JSON.
//
// Fields from [Entry.Data] are included in the JSON object together with the
// standard fields derived from the entry. If a field conflicts with a standard
// field, it is prefixed with "fields.". Standard field names can be customized
// through FieldMap. When DataKey is set, fields from [Entry.Data] are nested
// under that key instead.
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
@@ -61,7 +68,8 @@ type JSONFormatter struct {
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+4)
caller := entry.Caller
data := make(Fields, len(entry.Data)+defaultFields)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
@@ -73,13 +81,14 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
if f.DataKey != "" {
newData := make(Fields, 4)
if f.DataKey != "" && len(entry.Data) > 0 {
newData := make(Fields, defaultFields+1)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
@@ -94,11 +103,13 @@ 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() {
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if caller != nil {
var funcVal, fileVal string
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
@@ -108,11 +119,9 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
encoder := json.NewEncoder(b)
+101
View File
@@ -0,0 +1,101 @@
package logrus
import (
"strings"
"sync"
)
const (
ansiReset = "\x1b[0m" // reset attributes
ansiRed = "\x1b[31m" // red
ansiYellow = "\x1b[33m" // yellow
ansiCyan = "\x1b[36m" // cyan
ansiDimCyan = "\x1b[2;36m" // dim cyan
ansiDimWhite = "\x1b[2;37m" // dim white (light gray)
)
type lvlPrefix struct {
full string
truncated string
padded string
}
func colorize(level Level, s string) string {
color := ansiCyan
switch level {
case TraceLevel:
color = ansiDimWhite
case DebugLevel:
color = ansiDimCyan
case WarnLevel:
color = ansiYellow
case ErrorLevel, FatalLevel, PanicLevel:
color = ansiRed
case InfoLevel:
color = ansiCyan
}
return color + s + ansiReset
}
func formatLevel(level Level, disableTrunc, pad bool, maxLen int) string {
upper := strings.ToUpper(level.String())
if pad && maxLen > len(upper) {
upper += strings.Repeat(" ", maxLen-len(upper))
}
if !pad && !disableTrunc && len(upper) > 4 {
upper = upper[:4]
}
return colorize(level, upper)
}
var levelPrefixOnce = sync.OnceValues(func() (map[Level]lvlPrefix, lvlPrefix) {
var maxLevel Level
maxLen := 0
for _, lvl := range AllLevels {
if lvl > maxLevel {
maxLevel = lvl
}
if l := len(lvl.String()); l > maxLen {
maxLen = l
}
}
prefix := make(map[Level]lvlPrefix, len(AllLevels))
for _, lvl := range AllLevels {
prefix[lvl] = lvlPrefix{
full: formatLevel(lvl, true, false, maxLen),
truncated: formatLevel(lvl, false, false, maxLen),
padded: formatLevel(lvl, true, true, maxLen),
}
}
unknownLevel := maxLevel + 1
unknown := lvlPrefix{
full: formatLevel(unknownLevel, true, false, maxLen),
truncated: formatLevel(unknownLevel, false, false, maxLen),
padded: formatLevel(unknownLevel, true, true, maxLen),
}
return prefix, unknown
})
func levelPrefix(level Level, disableTrunc, pad bool) string {
prefix, unknown := levelPrefixOnce()
p, ok := prefix[level]
if !ok {
p = unknown
}
switch {
case pad:
return p.padded
case !disableTrunc:
return p.truncated
default:
return p.full
}
}
+107 -49
View File
@@ -12,17 +12,19 @@ import (
// LogFunction For big messages, it can be more efficient to pass a function
// and only call it if the log level is actually enables rather than
// generating the log message and then checking if the level is enabled
type LogFunction func() []interface{}
type LogFunction func() []any
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
// something more adventurous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
@@ -38,37 +40,44 @@ type Logger struct {
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged.
Level Level
// Used to sync writing to the log. Locking is enabled by Default
mu MutexWrap
mu mutexWrap
// Reusable empty entry
entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
ExitFunc func(int)
// 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)
// MutexWrap is the mutex implementation used by [Logger].
//
// Deprecated: MutexWrap is an implementation detail of Logger and should not be used directly.
type MutexWrap = mutexWrap
type MutexWrap struct {
type mutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
func (mw *mutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
func (mw *mutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
func (mw *mutexWrap) Disable() {
mw.disabled = true
}
@@ -104,7 +113,7 @@ func (logger *Logger) newEntry() *Entry {
}
func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
entry.Data = map[string]any{}
logger.entryPool.Put(entry)
}
@@ -112,7 +121,7 @@ func (logger *Logger) releaseEntry(entry *Entry) {
// 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 {
func (logger *Logger) WithField(key string, value any) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
@@ -148,7 +157,12 @@ func (logger *Logger) WithTime(t time.Time) *Entry {
return entry.WithTime(t)
}
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
// Logf logs a formatted message at the specified level.
//
// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logf treats the level as logging severity only;
// use [Logger.Panicf] or [Logger.Fatalf] when those side effects are desired.
func (logger *Logger) Logf(level Level, format string, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logf(level, format, args...)
@@ -156,49 +170,55 @@ func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
}
}
func (logger *Logger) Tracef(format string, args ...interface{}) {
func (logger *Logger) Tracef(format string, args ...any) {
logger.Logf(TraceLevel, format, args...)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
func (logger *Logger) Debugf(format string, args ...any) {
logger.Logf(DebugLevel, format, args...)
}
func (logger *Logger) Infof(format string, args ...interface{}) {
func (logger *Logger) Infof(format string, args ...any) {
logger.Logf(InfoLevel, format, args...)
}
func (logger *Logger) Printf(format string, args ...interface{}) {
func (logger *Logger) Printf(format string, args ...any) {
entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
func (logger *Logger) Warnf(format string, args ...any) {
logger.Logf(WarnLevel, format, args...)
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
func (logger *Logger) Warningf(format string, args ...any) {
logger.Warnf(format, args...)
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
func (logger *Logger) Errorf(format string, args ...any) {
logger.Logf(ErrorLevel, format, args...)
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
func (logger *Logger) Fatalf(format string, args ...any) {
logger.Logf(FatalLevel, format, args...)
logger.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.Logf(PanicLevel, format, args...)
func (logger *Logger) Panicf(format string, args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicf(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{}) {
// Log logs a message at the specified level.
//
// Using Log with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Log treats the level as logging severity only;
// use [Logger.Panic] or [Logger.Fatal] when those side effects are desired.
func (logger *Logger) Log(level Level, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Log(level, args...)
@@ -206,6 +226,11 @@ func (logger *Logger) Log(level Level, args ...interface{}) {
}
}
// LogFn logs a message returned by fn at the specified level.
//
// Using LogFn with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. LogFn treats the level as logging severity only;
// use [Logger.PanicFn] or [Logger.FatalFn] when those side effects are desired.
func (logger *Logger) LogFn(level Level, fn LogFunction) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
@@ -214,43 +239,47 @@ func (logger *Logger) LogFn(level Level, fn LogFunction) {
}
}
func (logger *Logger) Trace(args ...interface{}) {
func (logger *Logger) Trace(args ...any) {
logger.Log(TraceLevel, args...)
}
func (logger *Logger) Debug(args ...interface{}) {
func (logger *Logger) Debug(args ...any) {
logger.Log(DebugLevel, args...)
}
func (logger *Logger) Info(args ...interface{}) {
func (logger *Logger) Info(args ...any) {
logger.Log(InfoLevel, args...)
}
func (logger *Logger) Print(args ...interface{}) {
func (logger *Logger) Print(args ...any) {
entry := logger.newEntry()
entry.Print(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warn(args ...interface{}) {
func (logger *Logger) Warn(args ...any) {
logger.Log(WarnLevel, args...)
}
func (logger *Logger) Warning(args ...interface{}) {
func (logger *Logger) Warning(args ...any) {
logger.Warn(args...)
}
func (logger *Logger) Error(args ...interface{}) {
func (logger *Logger) Error(args ...any) {
logger.Log(ErrorLevel, args...)
}
func (logger *Logger) Fatal(args ...interface{}) {
func (logger *Logger) Fatal(args ...any) {
logger.Log(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
logger.Log(PanicLevel, args...)
func (logger *Logger) Panic(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(args...)
}
}
func (logger *Logger) TraceFn(fn LogFunction) {
@@ -289,10 +318,19 @@ func (logger *Logger) FatalFn(fn LogFunction) {
}
func (logger *Logger) PanicFn(fn LogFunction) {
logger.LogFn(PanicLevel, fn)
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(fn()...)
}
}
func (logger *Logger) Logln(level Level, args ...interface{}) {
// Logln logs a message at the specified level with Println-style spacing.
//
// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logln treats the level as logging severity only;
// use [Logger.Panicln] or [Logger.Fatalln] when those side effects are desired.
func (logger *Logger) Logln(level Level, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logln(level, args...)
@@ -300,43 +338,47 @@ func (logger *Logger) Logln(level Level, args ...interface{}) {
}
}
func (logger *Logger) Traceln(args ...interface{}) {
func (logger *Logger) Traceln(args ...any) {
logger.Logln(TraceLevel, args...)
}
func (logger *Logger) Debugln(args ...interface{}) {
func (logger *Logger) Debugln(args ...any) {
logger.Logln(DebugLevel, args...)
}
func (logger *Logger) Infoln(args ...interface{}) {
func (logger *Logger) Infoln(args ...any) {
logger.Logln(InfoLevel, args...)
}
func (logger *Logger) Println(args ...interface{}) {
func (logger *Logger) Println(args ...any) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnln(args ...interface{}) {
func (logger *Logger) Warnln(args ...any) {
logger.Logln(WarnLevel, args...)
}
func (logger *Logger) Warningln(args ...interface{}) {
func (logger *Logger) Warningln(args ...any) {
logger.Warnln(args...)
}
func (logger *Logger) Errorln(args ...interface{}) {
func (logger *Logger) Errorln(args ...any) {
logger.Logln(ErrorLevel, args...)
}
func (logger *Logger) Fatalln(args ...interface{}) {
func (logger *Logger) Fatalln(args ...any) {
logger.Logln(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
logger.Logln(PanicLevel, args...)
func (logger *Logger) Panicln(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicln(args...)
}
}
func (logger *Logger) Exit(code int) {
@@ -375,7 +417,22 @@ func (logger *Logger) AddHook(hook Hook) {
logger.Hooks.Add(hook)
}
// IsLevelEnabled checks if the log level of the logger is greater than the level param
// hooksForLevel returns a snapshot of the hooks registered for the given level.
// The returned slice is a shallow copy and may be used without holding logger.mu.
func (logger *Logger) hooksForLevel(level Level) []Hook {
logger.mu.Lock()
hooks := logger.Hooks[level]
if len(hooks) == 0 {
logger.mu.Unlock()
return nil
}
out := make([]Hook, len(hooks))
copy(out, hooks)
logger.mu.Unlock()
return out
}
// IsLevelEnabled checks if logging for the given level is enabled.
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
@@ -394,6 +451,7 @@ func (logger *Logger) SetOutput(output io.Writer) {
logger.Out = output
}
// SetReportCaller sets whether the caller stack frame must be logged.
func (logger *Logger) SetReportCaller(reportCaller bool) {
logger.mu.Lock()
defer logger.mu.Unlock()
@@ -403,9 +461,9 @@ func (logger *Logger) SetReportCaller(reportCaller bool) {
// ReplaceHooks replaces the logger hooks and returns the old ones
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Lock()
defer logger.mu.Unlock()
oldHooks := logger.Hooks
logger.Hooks = hooks
logger.mu.Unlock()
return oldHooks
}
+129 -92
View File
@@ -1,13 +1,13 @@
package logrus
import (
"bytes"
"fmt"
"log"
"strings"
)
// Fields type, used to pass to [WithFields].
type Fields map[string]interface{}
type Fields map[string]any
// Level type
//
@@ -16,39 +16,56 @@ type Level uint32
// Convert the Level to a string. E.g. [PanicLevel] becomes "panic".
func (level Level) String() string {
if b, err := level.MarshalText(); err == nil {
return string(b)
} else {
switch level {
case TraceLevel:
return "trace"
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
default:
return "unknown"
}
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
case "trace":
return TraceLevel, nil
}
return parseLevel([]byte(lvl))
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
func parseLevel(b []byte) (Level, error) {
switch {
case bytes.EqualFold(b, []byte("panic")):
return PanicLevel, nil
case bytes.EqualFold(b, []byte("fatal")):
return FatalLevel, nil
case bytes.EqualFold(b, []byte("error")):
return ErrorLevel, nil
case bytes.EqualFold(b, []byte("warn")),
bytes.EqualFold(b, []byte("warning")):
return WarnLevel, nil
case bytes.EqualFold(b, []byte("info")):
return InfoLevel, nil
case bytes.EqualFold(b, []byte("debug")):
return DebugLevel, nil
case bytes.EqualFold(b, []byte("trace")):
return TraceLevel, nil
default:
return 0, fmt.Errorf("not a valid logrus Level: %q", b)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := ParseLevel(string(text))
l, err := parseLevel(text)
if err != nil {
return err
}
@@ -60,23 +77,11 @@ func (level *Level) UnmarshalText(text []byte) error {
func (level Level) MarshalText() ([]byte, error) {
switch level {
case TraceLevel:
return []byte("trace"), nil
case DebugLevel:
return []byte("debug"), nil
case InfoLevel:
return []byte("info"), nil
case WarnLevel:
return []byte("warning"), nil
case ErrorLevel:
return []byte("error"), nil
case FatalLevel:
return []byte("fatal"), nil
case PanicLevel:
return []byte("panic"), nil
case TraceLevel, DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel, PanicLevel:
return []byte(level.String()), nil
default:
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// AllLevels exposing all logging levels.
@@ -91,7 +96,7 @@ var AllLevels = []Level{
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
// on your instance of logger, obtained with [logrus.New].
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
@@ -113,78 +118,110 @@ const (
TraceLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
// Compile-time interface assertions.
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
_ StdLogger = (*log.Logger)(nil)
_ StdLogger = (*Entry)(nil)
_ StdLogger = (*Logger)(nil)
_ FieldLogger = (*Logger)(nil)
_ FieldLogger = (*Entry)(nil)
_ FieldLogger = Ext1FieldLogger(nil)
_ DebugLogger = (*Logger)(nil)
_ InfoLogger = (*Logger)(nil)
_ WarnLogger = (*Logger)(nil)
_ ErrorLogger = (*Logger)(nil)
_ TraceLogger = (*Logger)(nil)
_ DebugLogger = (*Entry)(nil)
_ InfoLogger = (*Entry)(nil)
_ WarnLogger = (*Entry)(nil)
_ ErrorLogger = (*Entry)(nil)
_ TraceLogger = (*Entry)(nil)
_ Ext1FieldLogger = (*Logger)(nil)
_ Ext1FieldLogger = (*Entry)(nil)
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger ([log.Logger]) and a logrus logger.
// There's no standard interface, so this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Print(args ...any)
Printf(format string, args ...any)
Println(args ...any)
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Fatal(args ...any)
Fatalf(format string, args ...any)
Fatalln(args ...any)
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Panic(args ...any)
Panicf(format string, args ...any)
Panicln(args ...any)
}
// FieldLogger extends the [StdLogger] interface, generalizing
// the [Entry] and [Logger] types.
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithField(key string, value any) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
StdLogger
DebugLogger
InfoLogger
WarnLogger
ErrorLogger
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
// Legacy warning aliases. These are kept on FieldLogger for backwards
// compatibility, but are intentionally omitted from [WarnLogger].
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
Warning(args ...any)
Warningf(format string, args ...any)
Warningln(args ...any)
}
// Ext1FieldLogger (the first extension to [FieldLogger]) is superfluous, it is
// here for consistency. Do not use. Use [FieldLogger], [Logger] or [Entry]
// instead.
// DebugLogger provides convenience functions to log messages at level [DebugLevel].
type DebugLogger interface {
Debug(args ...any)
Debugf(format string, args ...any)
Debugln(args ...any)
}
// InfoLogger provides convenience functions to log messages at level [InfoLevel].
type InfoLogger interface {
Info(args ...any)
Infof(format string, args ...any)
Infoln(args ...any)
}
// WarnLogger provides convenience functions to log messages at level [WarnLevel].
type WarnLogger interface {
Warn(args ...any)
Warnf(format string, args ...any)
Warnln(args ...any)
}
// ErrorLogger provides convenience functions to log messages at level [ErrorLevel].
type ErrorLogger interface {
Error(args ...any)
Errorf(format string, args ...any)
Errorln(args ...any)
}
// TraceLogger provides convenience functions to log messages at level [TraceLevel].
type TraceLogger interface {
Trace(args ...any)
Tracef(format string, args ...any)
Traceln(args ...any)
}
// Ext1FieldLogger is FieldLogger extended with Trace-level methods.
//
// New code should prefer the smallest applicable interface, such as
// [FieldLogger] or [TraceLogger], or use [Logger] or [Entry] directly.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
TraceLogger
}
+2 -6
View File
@@ -1,11 +1,7 @@
// +build appengine
//go:build appengine
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
func checkIfTerminal(_ any) bool {
return true
}
+1 -2
View File
@@ -1,5 +1,4 @@
// +build darwin dragonfly freebsd netbsd openbsd hurd
// +build !js
//go:build (darwin || dragonfly || freebsd || netbsd || openbsd || hurd) && !tinygo
package logrus
-7
View File
@@ -1,7 +0,0 @@
// +build js
package logrus
func isTerminal(fd int) bool {
return false
}
+2 -6
View File
@@ -1,11 +1,7 @@
// +build js nacl plan9
//go:build js || nacl || plan9 || wasi || wasip1 || tinygo
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
func checkIfTerminal(_ any) bool {
return false
}
+6 -2
View File
@@ -1,4 +1,4 @@
// +build !appengine,!js,!windows,!nacl,!plan9
//go:build !appengine && !js && !windows && !nacl && !plan9 && !wasi && !wasip1 && !tinygo
package logrus
@@ -10,7 +10,11 @@ import (
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return isTerminal(int(v.Fd()))
fd := v.Fd()
if fd > uintptr(^uint(0)>>1) {
return false
}
return isTerminal(int(fd))
default:
return false
}
+2
View File
@@ -1,3 +1,5 @@
//go:build solaris && !tinygo
package logrus
import (
+1 -4
View File
@@ -1,7 +1,4 @@
//go:build (linux || aix || zos) && !js && !wasi
// +build linux aix zos
// +build !js
// +build !wasi
//go:build (linux || aix || zos) && !tinygo
package logrus
-8
View File
@@ -1,8 +0,0 @@
//go:build wasi
// +build wasi
package logrus
func isTerminal(fd int) bool {
return false
}
-8
View File
@@ -1,8 +0,0 @@
//go:build wasip1
// +build wasip1
package logrus
func isTerminal(fd int) bool {
return false
}
+1 -1
View File
@@ -1,4 +1,4 @@
// +build !appengine,!js,windows
//go:build windows && !appengine
package logrus
+317 -174
View File
@@ -3,30 +3,32 @@ package logrus
import (
"bytes"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"sort"
"slices"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
)
const (
red = 31
yellow = 33
blue = 36
gray = 37
)
var baseTimestamp = time.Now()
var baseTimestamp time.Time
func init() {
baseTimestamp = time.Now()
}
// TextFormatter formats logs into text
// TextFormatter formats logs into text.
//
// Output is logfmt-like: key=value pairs separated by spaces. Fields from
// [Entry.Data] are included together with the standard fields derived from the
// entry. If a field conflicts with a standard field, it is prefixed with
// "fields.". Standard field names can be customized through FieldMap.
//
// Field keys are written as-is (unquoted and unescaped) in the plain
// (non-colored) format; only field values may be quoted depending on
// DisableQuote, ForceQuote, QuoteEmptyFields, and the value content.
//
// When colors are enabled, ANSI escape sequences may be added for presentation.
// For fully escaped structured output (including safe keys), use JSONFormatter.
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
@@ -64,7 +66,7 @@ type TextFormatter struct {
// be desired.
DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
// The keys sorting function, when uninitialized it uses slices.Sort.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
@@ -77,16 +79,22 @@ type TextFormatter struct {
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
// Whether the logger's out is to a terminal. Don't use this field
// directly; use TextFormatter.isTerminal instead.
terminal bool
// FieldMap allows users to customize the names of keys for default fields.
// Mapped keys are written as-is, so they should be safe for plain-text output.
//
// As an example:
//
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// },
// }
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
@@ -96,54 +104,78 @@ type TextFormatter struct {
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)
}
// 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
}
func (f *TextFormatter) isTerminal(entry *Entry) bool {
if entry == nil || entry.Logger == nil {
// Don't run the terminalInitOnce without a logger, otherwise we'd
// cache the default (false) forever even if a logger is attached
// later.
return false
}
f.terminalInitOnce.Do(func() {
entry.Logger.mu.Lock()
out := entry.Logger.Out
entry.Logger.mu.Unlock()
f.terminal = checkIfTerminal(out)
})
return f.terminal
}
func (f *TextFormatter) isColored() bool {
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
if f.EnvironmentOverrideColors {
switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
case ok && force != "0":
isColored = true
case ok && force == "0", os.Getenv("CLICOLOR") == "0":
isColored = false
}
func (f *TextFormatter) isColored(isTerminal bool) bool {
if f.DisableColors {
return false
}
return isColored && !f.DisableColors
colored := f.ForceColors || isTerminal
if !f.EnvironmentOverrideColors {
return colored
}
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok {
return force != "0"
}
if os.Getenv("CLICOLOR") == "0" {
return false
}
return colored
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields)
for k, v := range entry.Data {
data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
data := make(Fields, len(entry.Data))
maps.Copy(data, entry.Data)
isColored := f.isColored(f.isTerminal(entry))
caller := entry.Caller
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
var funcVal, fileVal string
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
fixedKeys := make([]string, 0, 4+len(data))
if isColored {
f.printColored(b, entry, keys, data)
} else {
f.printPlain(b, entry, keys, data)
}
return b.Bytes(), nil
}
func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
caller := entry.Caller
hasCaller := caller != nil
fixedKeys := make([]string, 0, len(keys)+defaultFields)
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
@@ -154,12 +186,14 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
var funcVal, fileVal string
if caller != nil {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if funcVal != "" {
@@ -172,152 +206,108 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if !f.DisableSorting {
if f.SortingFunc == nil {
sort.Strings(keys)
// Default sorting does not sort the "fixed keys";
// see https://github.com/sirupsen/logrus/commit/73bc94e60c753099e8bae902f81fbd6e7dd95f26
slices.Sort(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
f.terminalInitOnce.Do(func() { f.init(entry) })
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if f.isColored() {
f.printColored(b, entry, keys, data, timestampFormat)
} else {
for _, key := range fixedKeys {
var value interface{}
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
value = entry.Time.Format(timestampFormat)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
value = fileVal
default:
value = data[key]
for _, key := range fixedKeys {
var value any
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
if f.TimestampFormat == "" {
value = entry.Time.Format(defaultTimestampFormat)
} else {
value = entry.Time.Format(f.TimestampFormat)
}
f.appendKeyValue(b, key, value)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && hasCaller:
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && hasCaller:
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel, TraceLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
case InfoLevel:
levelColor = blue
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())
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)
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
var callerText string
if caller := entry.Caller; caller != nil {
var funcVal, fileVal string
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
if caller.Function != "" {
funcVal = caller.Function + "()"
}
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if fileVal == "" {
caller = funcVal
callerText = funcVal
} else if funcVal == "" {
caller = fileVal
callerText = fileVal
} else {
caller = fileVal + " " + funcVal
callerText = fileVal + " " + funcVal
}
}
levelText := levelPrefix(entry.Level, f.DisableLevelTruncation, f.PadLevelText)
switch {
case f.DisableTimestamp:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
_, _ = fmt.Fprintf(b, "%s%s %-44s ", levelText, callerText, entry.Message)
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)
_, _ = fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), callerText, entry.Message)
default:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
_, _ = fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), callerText, entry.Message)
}
for _, k := range keys {
v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
}
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 {
//nolint:staticcheck // QF1001: could apply De Morgan's law
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
return true
if !f.DisableSorting {
if f.SortingFunc == nil {
slices.Sort(keys)
} else {
f.SortingFunc(keys)
}
}
return false
// Keys use the same color as the level-prefix.
for _, k := range keys {
b.WriteByte(' ')
b.WriteString(colorize(entry.Level, k))
b.WriteByte('=')
f.appendValue(b, data[k])
}
b.WriteByte('\n')
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
// appendKeyValue writes key=value. Keys are written verbatim (unquoted/unescaped);
// values are subject to quoting/escaping.
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) {
if b.Len() > 0 {
b.WriteByte(' ')
}
@@ -326,15 +316,168 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interf
f.appendValue(b, value)
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
stringVal, ok := value.(string)
if !ok {
stringVal = fmt.Sprint(value)
func (f *TextFormatter) appendValue(b *bytes.Buffer, value any) {
// Fast paths.
switch v := value.(type) {
case string:
f.appendString(b, v)
return
case []byte:
f.appendBytes(b, v)
return
case bool:
var raw [8]byte
f.appendBytes(b, strconv.AppendBool(raw[:0], v))
return
case error:
f.appendError(b, v)
return
case fmt.Stringer:
f.appendStringer(b, v)
return
}
if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
fmt.Fprintf(b, "%q", stringVal)
// Handle common primitives.
var raw [64]byte
var num []byte
switch v := value.(type) {
case int:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int8:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int16:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int32:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int64:
num = strconv.AppendInt(raw[:0], v, 10)
case uint:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint8:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint16:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint32:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint64:
num = strconv.AppendUint(raw[:0], v, 10)
case uintptr:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case float32:
num = strconv.AppendFloat(raw[:0], float64(v), 'g', -1, 32)
case float64:
num = strconv.AppendFloat(raw[:0], v, 'g', -1, 64)
default:
f.appendString(b, fmt.Sprint(value))
return
}
f.appendNumeric(b, num)
}
func (f *TextFormatter) appendString(b *bytes.Buffer, s string) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(s) == 0) || (!f.DisableQuote && needsQuoting(s))
if !quote {
b.WriteString(s)
return
}
if len(s) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], s))
}
func (f *TextFormatter) appendBytes(b *bytes.Buffer, bs []byte) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(bs) == 0) || (!f.DisableQuote && needsQuotingBytes(bs))
if !quote {
b.Write(bs)
return
}
if len(bs) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(bs)))
}
func (f *TextFormatter) appendNumeric(b *bytes.Buffer, out []byte) {
if f.ForceQuote {
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(out)))
return
}
b.Write(out)
}
func (f *TextFormatter) appendError(b *bytes.Buffer, v error) {
defer f.recoverValue(b, v, "Error")
f.appendString(b, v.Error())
}
func (f *TextFormatter) appendStringer(b *bytes.Buffer, v fmt.Stringer) {
defer f.recoverValue(b, v, "String")
f.appendString(b, v.String())
}
func (f *TextFormatter) recoverValue(b *bytes.Buffer, v any, method string) {
if r := recover(); r != nil {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer && rv.IsNil() {
f.appendString(b, "<nil>")
} else {
f.appendString(b, fmt.Sprintf("%%!v(PANIC=%s method: %v)", method, r))
}
}
}
// needsQuoting returns true if the string contains any byte that
// requires quoting. It returns false when every byte is "safe" according
// to isSafeByte.
func needsQuoting(s string) bool {
// use an index loop (avoid rune decoding).
for i := range len(s) {
c := s[i]
if !isSafeByte(c) {
return true
}
}
return false
}
// needsQuotingBytes returns true if the byte slice contains any byte that
// requires quoting. It returns false when every byte is "safe" according
// to isSafeByte.
func needsQuotingBytes(bs []byte) bool {
for _, c := range bs {
if !isSafeByte(c) {
return true
}
}
return false
}
// isSafeByte returns true if the byte is allowed unquoted (ASCII and in the allowlist).
// It purposely uses byte arithmetic (no runes) for performance.
func isSafeByte(ch byte) bool {
ok := ch < 0x80 && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))
if ok {
return true
}
switch ch {
case '-', '.', '_', '/', '@', '^', '+':
return true
default:
return false
}
}
+2 -4
View File
@@ -30,7 +30,7 @@ func (entry *Entry) Writer() *io.PipeWriter {
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
var printFunc func(args ...interface{})
printFunc := entry.Print
// Determine which log function to use based on the specified log level
switch level {
@@ -48,8 +48,6 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
printFunc = entry.Fatal
case PanicLevel:
printFunc = entry.Panic
default:
printFunc = entry.Print
}
// Start a new goroutine to scan the input and write it to the logger using the specified print function.
@@ -63,7 +61,7 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
}
// writerScanner scans the input from the reader and writes it to the logger
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...any)) {
scanner := bufio.NewScanner(reader)
// Set the buffer size to the maximum token size to avoid buffer overflows
+2 -2
View File
@@ -277,8 +277,8 @@ github.com/prometheus/procfs/internal/util
# github.com/russross/blackfriday/v2 v2.1.0
## explicit
github.com/russross/blackfriday/v2
# github.com/sirupsen/logrus v1.9.4
## explicit; go 1.17
# github.com/sirupsen/logrus v1.10.1
## explicit; go 1.23
github.com/sirupsen/logrus
# github.com/spf13/cobra v1.10.2
## explicit; go 1.15