Compare commits

..
27 Commits
Author SHA1 Message Date
Paweł Gronowski f4efc1b596 Merge tag 'v29.7.2' into 29.x
v29.7.2
2026-08-11 13:21:01 +02:00
Paweł Gronowski 2747e2e3bf Merge tag 'v29.7.1' into 29.x
v29.7.1
2026-08-11 13:21:01 +02:00
Paweł Gronowski 329cdf1a9d Merge tag 'v29.7.0' into 29.x
v29.7.0
2026-08-11 13:21:01 +02:00
Paweł Gronowski 0c7e2e7203 Merge tag 'v29.7.0-rc.1' into 29.x
v29.7.0-rc.1
2026-08-11 13:21:00 +02:00
Paweł GronowskiandGitHub dfc4efb1e2 Merge pull request #7109 from vvoland/update-docker
[29.x] Bump to 29.6.2
2026-07-16 18:09:24 +02:00
Paweł Gronowski dba867594c Bump to 29.6.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-16 12:51:30 +02:00
Paweł Gronowski d9aefe565a Merge tag 'v29.6.1' into 29.x
v29.6.1
2026-07-15 17:35:42 +02:00
Paweł Gronowski 9182b351c1 Merge tag 'v29.6.0' into 29.x
v29.6.0
2026-07-15 17:35:42 +02:00
Paweł Gronowski 0ae55a3e6c Merge tag 'v29.5.3' into 29.x
v29.5.3
2026-07-15 17:35:42 +02:00
Paweł Gronowski 029a4b21b3 Merge tag 'v29.5.2' into 29.x
v29.5.2
2026-07-15 17:35:41 +02:00
Paweł Gronowski e596fea454 Merge tag 'v29.5.1' into 29.x
v29.5.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski 0c72f456aa Merge tag 'v29.5.0' into 29.x
v29.5.0
2026-07-15 17:35:41 +02:00
Paweł Gronowski 26f2736a2b Merge tag 'v29.4.1' into 29.x
v29.4.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski ac4c4dfabd Merge tag 'v29.4.0' into 29.x
v29.4.0
2026-07-15 17:35:40 +02:00
Sebastiaan van StijnandGitHub 1245054535 Merge pull request #7099 from vvoland/7098-29.x
[29.x backport] scripts/sync-branch: Fix non-interactive merge
2026-07-10 08:34:26 +02:00
Paweł Gronowski f72fb0caf0 scripts/sync-branch: Fix non-interactive merge
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit f57e528457)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 16:52:02 +02:00
Sebastiaan van StijnandGitHub e4af2bf0d3 Merge pull request #7097 from vvoland/7091-29.x
[29.x backport] gha: Add release branch sync workflow
2026-07-09 16:35:45 +02:00
Paweł Gronowski 49a1af6aaa gha: Add release branch sync workflow
Add a manually dispatched workflow for maintainers to sync a Docker
release branch to a selected release tag.

The sync-release-branch job checks out the release branch, computes
the list of unmerged tags up to the requested tag via
scripts/unmerged-tags, merges them in order via scripts/sync-branch
using git merge --no-ff (resolving conflicts by taking the tag's
content), then pushes the result to a temporary branch.

The push-release-branch job runs after manual approval via the
docker-releases environment. It verifies that neither the release
branch nor the temporary branch moved since the sync job ran before
force-advancing the release branch and deleting the temporary branch.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 182f56fe8c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 14:45:32 +02:00
Paweł GronowskiandGitHub c2be9ccfc3 Merge pull request #6887 from thaJeztah/29.x_backport_pin_actions
[29.x backport] ci: pin actions to digests
2026-03-25 15:22:32 +01:00
Sebastiaan van Stijn 0da6a5121f ci: pin actions to digests
As a follow-up, we should use the full version (major.minor.patch).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 97b9e04a94)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 15:06:23 +01:00
Sebastiaan van StijnandGitHub 6b3ca8fc04 Merge pull request #6884 from thaJeztah/29.x_backport_missing_buildtags
[29.x backport] cli/command: add missing "go:build" comments
2026-03-25 15:04:52 +01:00
Paweł GronowskiandGitHub f47603c290 Merge pull request #6885 from thaJeztah/29.x_backport_update_actions
[29.x backport] update actions
2026-03-25 14:23:37 +01:00
dependabot[bot]andSebastiaan van Stijn 9709c8fe08 build(deps): bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 39d676c72d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:16:00 +01:00
dependabot[bot]andSebastiaan van Stijn af45a23fa9 build(deps): bump docker/bake-action from 6 to 7
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 6 to 7.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 6453c4c3a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:15:48 +01:00
Paweł GronowskiandGitHub bc97f5ac19 Merge pull request #6883 from thaJeztah/29.x_backport_update_go1.25.8
[29.x backport] update to go1.25.8
2026-03-25 14:14:46 +01:00
Sebastiaan van Stijn 6d71967120 cli/command: add missing "go:build" comments
- commit e8dc2fce32 modernized loops to
  range over int, which requires go1.22 or later.
- commit 85ebca52fd modernized code to
  use stdlib min/max, which requires go1.21 or later.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9a471180cb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:11:15 +01:00
Sebastiaan van Stijn 9b51892b80 update to go1.25.8
go1.25.8 (released 2026-03-05) includes security fixes to the html/template,
net/url, and os packages, as well as bug fixes to the go command, the compiler,
and the os package. See the Go 1.25.8 milestone on our issue tracker for details.

- 1.25.8 https://github.com/golang/go/issues?q=milestone%3AGo1.25.8+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.25.7...go1.25.8
- 1.26.1 https://github.com/golang/go/issues?q=milestone%3AGo1.26.1+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.26.0...go1.26.1

---

We have just released Go versions 1.26.1 and 1.25.8, minor point releases.

These releases include 5 security fixes following the security policy:

crypto/x509: incorrect enforcement of email constraints

- When verifying a certificate chain which contains a certificate containing
  multiple email address constraints (composed of the full email address) which
  share common local portions (the portion of the address before the '@'
  character) but different domain portions (the portion of the address after the
  '@' character), these constraints will not be properly applied, and only the
  last constraint will be considered.

  This can allow certificates in the chain containing email addresses which are
  either not permitted or excluded by the relevant constraints to be returned by
  calls to Certificate.Verify. Since the name constraint checks happen after chain
  building is complete, this only applies to certificate chains which chain to
  trusted roots (root certificates either in VerifyOptions.Roots or in the system
  root certificate pool), requiring a trusted CA to issue certificates containing
  either not permitted or excluded email addresses.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27137 and Go issue https://go.dev/issue/77952.

- crypto/x509: panic in name constraint checking for malformed certificates

  Certificate verification can panic when a certificate in the chain has an empty
  DNS name and another certificate in the chain has excluded name constraints.
  This can crash programs that are either directly verifying X.509 certificate
  chains, or those that use TLS.

  Since the name constraint checks happen after chain building is complete, this
  only applies to certificate chains which chain to trusted roots (root
  certificates either in VerifyOptions.Roots or in the system root certificate
  pool), requiring a trusted CA to issue certificates containing malformed DNS
  names.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27138 and Go issue https://go.dev/issue/77953.

- html/template: URLs in meta content attribute actions are not escaped

  Actions which insert URLs into the content attribute of HTML meta tags are not
  escaped. This can allow XSS if the meta tag also has an http-equiv attribute
  with the value "refresh".

  A new GODEBUG setting has been added, htmlmetacontenturlescape, which can be
  used to disable escaping URLs in actions in the meta content attribute which
  follow "url=" by setting htmlmetacontenturlescape=0.

  This is CVE-2026-27142 and Go issue https://go.dev/issue/77954.

- net/url: reject IPv6 literal not at start of host

  The Go standard library function net/url.Parse insufficiently
  validated the host/authority component and accepted some invalid URLs
  by effectively treating garbage before an IP-literal as ignorable.
  The function should have rejected this as invalid.

  To prevent this behavior, net/url.Parse now rejects IPv6 literals
  that do not appear at the start of the host subcomponent of a URL.

  Thanks to Masaki Hara (https://github.com/qnighy) of Wantedly.

  This is CVE-2026-25679 and Go issue https://go.dev/issue/77578.

- os: FileInfo can escape from a Root

  On Unix platforms, when listing the contents of a directory using
  File.ReadDir or File.Readdir the returned FileInfo could reference
  a file outside of the Root in which the File was opened.

  The contents of the FileInfo were populated using the lstat system
  call, which takes the path to the file as a parameter. If a component
  of the full path of the file described by the FileInfo is replaced with
  a symbolic link, the target of the lstat can be directed to another
  location on the filesystem.

  The impact of this escape is limited to reading metadata provided by
  lstat from arbitrary locations on the filesystem. This could be used
  to probe for the presence or absence of files as well as gleaning
  metadata like file sizes, but does not permit reading or writing files
  outside the root.

  The FileInfo is now populated using fstatat.

  Thank you to Miloslav Trmač of Red Hat for reporting this issue.

  This is CVE-2026-27139 and Go issue https://go.dev/issue/77827.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f7d83cbae8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:08:46 +01:00
43 changed files with 843 additions and 1384 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ jobs:
bin-image:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@a492c6d04fd3315f67230809b44d60cc0acd50b3 # v1.16.0
uses: docker/github-builder/.github/workflows/bake.yml@27ade872c1e2296e62ef15ab3b10d37665e57cf7 # v1.15.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
+4 -4
View File
@@ -64,18 +64,18 @@ jobs:
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.7"
go-version: "1.26.5"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
category: "/language:go"
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.7"
go-version: "1.26.5"
cache: false
-
name: Test
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.7"
go-version: "1.26.5"
cache: false
-
name: Run gocompat check
+1 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.7"
go: "1.26.5"
timeout: 5m
+1 -1
View File
@@ -267,7 +267,7 @@ Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use a known identity (sorry, no anonymous contributions.)
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
+1 -1
View File
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.5
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
+1 -1
View File
@@ -1 +1 @@
29.8.0
29.7.2
+1 -1
View File
@@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool {
}
if i.IP != j.IP {
return i.IP.Less(j.IP)
return i.IP.String() < j.IP.String()
}
if i.PublicPort != j.PublicPort {
-18
View File
@@ -946,24 +946,6 @@ func TestDisplayablePorts(t *testing.T) {
},
expected: "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", //nolint:revive // ignore line-length-limit (revive)
},
{
// host IPs are ordered numerically, not lexicographically:
// "10.0.0.2" sorts as a string before "9.0.0.1".
ports: []container.PortSummary{
{
IP: netip.MustParseAddr("10.0.0.2"),
PublicPort: 8080,
PrivatePort: 80,
Type: "tcp",
}, {
IP: netip.MustParseAddr("9.0.0.1"),
PublicPort: 8081,
PrivatePort: 80,
Type: "tcp",
},
},
expected: "9.0.0.1:8081->80/tcp, 10.0.0.2:8080->80/tcp",
},
}
for _, port := range cases {
+23 -1
View File
@@ -48,6 +48,27 @@ func newManifestStore(dockerCLI command.Cli) store.Store {
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
//
// It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate.
//
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey
}
return domainName
}
// newRegistryClient returns a client for communicating with a Docker distribution
// registry
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
@@ -57,7 +78,8 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
}
cfg := dockerCLI.ConfigFile()
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
a, _ := cfg.GetAuthConfig(domainName)
configKey := getAuthConfigKey(domainName)
a, _ := cfg.GetAuthConfig(configKey)
return registry.AuthConfig{
Username: a.Username,
Password: a.Password,
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+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.10.1
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
+6 -4
View File
@@ -32,6 +32,7 @@ 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=
@@ -152,6 +153,7 @@ 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=
@@ -174,8 +176,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.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
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=
@@ -185,8 +187,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.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
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/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=
+38 -10
View File
@@ -1,9 +1,12 @@
version: "2"
run:
tests: false
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errchkjson
@@ -19,21 +22,46 @@ 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
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'
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+15
View File
@@ -0,0 +1,15 @@
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
+38 -201
View File
@@ -1,246 +1,90 @@
# 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
# 1.8.1
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)
@@ -248,26 +92,20 @@ 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
@@ -284,10 +122,9 @@ 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
@@ -297,71 +134,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
@@ -373,25 +210,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%
@@ -399,24 +236,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)
+53 -39
View File
@@ -3,10 +3,13 @@
Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger.
**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)).
**Logrus is in maintenance-mode.** We will not be introducing new features. It's
simply too hard to do in a way that won't break many people's projects, which is
the last thing you want from your Logging library (again...).
This does not mean Logrus is dead. Logrus will continue to be maintained for
security, (backwards compatible) bug fixes, and performance (where we are
limited by the interface).
I believe Logrus' biggest contribution is to have played a part in today's
widespread use of structured logging in Golang. There doesn't seem to be a
@@ -20,6 +23,18 @@ 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):
@@ -28,27 +43,35 @@ plain text):
With `logrus.SetFormatter(&logrus.JSONFormatter{})`, for easy parsing by logstash
or Splunk:
```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"}
```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"}
```
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:
```bash
```text
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!" animal=orca err="It's over 9000!" number=100 omg=true 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
```
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
```go
@@ -65,32 +88,29 @@ 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"}
```
```bash
```text
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=ReportCaller
go test -bench=.*CallerTracing
```
#### Case-sensitivity
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
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`.
#### Example
@@ -269,7 +289,6 @@ 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)
@@ -348,21 +367,18 @@ Splunk or Logstash.
The built-in logging formatters are:
* [`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`
* `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`
field to `true`. To force no colored output even if there is a TTY set the
`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.
`DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`.
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
* [`logrus.JSONFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter)
logs fields as JSON.
* 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).
Third-party logging formatters:
@@ -374,12 +390,10 @@ 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]any`) with all your fields as well as the
`Fields` type (`map[string]interface{}`) with all your fields as well as the
default ones (see Entries section above):
```go
@@ -502,4 +516,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 an alert that the application is
// closing database connections, or sending a 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 an alert that the application is
// closing database connections, or sending a alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
+14 -8
View File
@@ -5,13 +5,9 @@ import (
"sync"
)
var bufferPool BufferPool = &defaultPool{
pool: &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
var (
bufferPool BufferPool
)
type BufferPool interface {
Put(*bytes.Buffer)
@@ -31,7 +27,17 @@ func (p *defaultPool) Get() *bytes.Buffer {
}
// SetBufferPool allows to replace the default logrus buffer pool
// to better meet the specific needs of an application.
// to better meets the specific needs of an application.
func SetBufferPool(bp BufferPool) {
bufferPool = bp
}
func init() {
SetBufferPool(&defaultPool{
pool: &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
},
})
}
+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
*/
+148 -270
View File
@@ -4,11 +4,9 @@ import (
"bytes"
"context"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
@@ -19,10 +17,8 @@ var (
// qualified package name, cached at first use
logrusPackage string
// 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
// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// Used for caller information initialisation
callerInitOnce sync.Once
@@ -33,116 +29,68 @@ 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 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.
// 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.
//
// 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.
//nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver.
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
// Data contains all user-defined fields attached to this entry.
// Contains all the fields set by the user.
Data Fields
// Time is the timestamp for the log event. If zero when the entry is
// logged, it defaults to the current time.
// Time at which the log entry was created
Time time.Time
// 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 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 Level
// 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.
// Calling method, with package name
Caller *runtime.Frame
// 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 passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string
// 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.
// When formatter is called in entry.log(), a Buffer may be set to entry
Buffer *bytes.Buffer
// Context carries user-provided context for hooks and formatters.
// Contains the context set by the user. Useful for hook processing etc.
Context context.Context
// err contains internal field-formatting errors.
// err may contain a field formatting error
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,
// Reserve default predefined fields and a little extra room.
Data: make(Fields, defaultFields+3),
// Default is three fields, plus one optional. Give a little extra room.
Data: make(Fields, 6),
}
}
// 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 {
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,
data := make(Fields, len(entry.Data))
for k, v := range entry.Data {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
}
// Bytes returns the bytes representation of this entry from the formatter.
func (entry *Entry) Bytes() ([]byte, error) {
// 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)
return entry.Logger.Formatter.Format(entry)
}
// String returns the string representation from the reader and ultimately the
@@ -164,61 +112,58 @@ func (entry *Entry) WithError(err error) *Entry {
// WithContext adds a context to the Entry.
func (entry *Entry) WithContext(ctx context.Context) *Entry {
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.Context = ctx
return dup
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}
}
// WithField adds a single field to the Entry.
func (entry *Entry) WithField(key string, value any) *Entry {
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.addField(key, value)
return dup
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// WithFields adds a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
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)
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
}
return dup
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}
}
// WithTime overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
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
}
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
}
if entry.Data == nil {
entry.Data = make(Fields, 1)
}
entry.Data[key] = value
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
}
// getPackageName reduces a fully qualified function name to the package name
// There really ought to be a better way...
// There really ought to be to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
@@ -241,7 +186,7 @@ func getCaller() *runtime.Frame {
_ = runtime.Callers(0, pcs)
// dynamic get the package name and the minimum caller depth
for i := range maximumCallerDepth {
for i := 0; i < maximumCallerDepth; i++ {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
logrusPackage = getPackageName(funcName)
@@ -270,47 +215,16 @@ func getCaller() *runtime.Frame {
return 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) HasCaller() (has bool) {
return entry.Logger != nil &&
entry.Logger.ReportCaller &&
entry.Caller != nil
}
func (entry *Entry) logArgs(level Level, panicAfter bool, args ...any) {
entry.log(level, panicAfter, sprint(args...))
}
func (entry *Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
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)
newEntry := entry.Dup()
if newEntry.Time.IsZero() {
newEntry.Time = time.Now()
@@ -319,24 +233,17 @@ func (entry *Entry) log(level Level, panicAfter bool, msg string) {
newEntry.Level = level
newEntry.Message = msg
logger := newEntry.Logger
logger.mu.Lock()
reportCaller := logger.ReportCaller
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
bufPool := newEntry.getBufferPool()
logger.mu.Unlock()
newEntry.Logger.mu.Unlock()
// Preserve explicitly set caller information.
if reportCaller && newEntry.Caller == nil {
if reportCaller {
newEntry.Caller = getCaller()
}
// 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()
newEntry.fireHooks()
buffer = bufPool.Get()
defer func() {
newEntry.Buffer = nil
buffer.Reset()
@@ -344,12 +251,15 @@ func (entry *Entry) log(level Level, panicAfter bool, msg string) {
}()
buffer.Reset()
newEntry.Buffer = buffer
newEntry.write()
newEntry.Buffer = nil
// Panic here so the panic value contains the fully populated entry without
// requiring log to return it to the caller.
if panicAfter {
// 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(newEntry)
}
}
@@ -361,207 +271,175 @@ func (entry *Entry) getBufferPool() (pool BufferPool) {
return bufferPool
}
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) 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) 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()
formatter := entry.Logger.Formatter
entry.Logger.mu.Unlock()
serialized, err := formatter.Format(entry)
if err != nil {
_, _ = 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()
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
return
}
if _, err := entry.Logger.Out.Write(serialized); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to write to log:", err)
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
// 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
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
func (entry *Entry) Log(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.logArgs(level, panicAfter, args...)
entry.log(level, fmt.Sprint(args...))
}
}
func (entry *Entry) Trace(args ...any) {
func (entry *Entry) Trace(args ...interface{}) {
entry.Log(TraceLevel, args...)
}
func (entry *Entry) Debug(args ...any) {
func (entry *Entry) Debug(args ...interface{}) {
entry.Log(DebugLevel, args...)
}
func (entry *Entry) Print(args ...any) {
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...any) {
func (entry *Entry) Info(args ...interface{}) {
entry.Log(InfoLevel, args...)
}
func (entry *Entry) Warn(args ...any) {
func (entry *Entry) Warn(args ...interface{}) {
entry.Log(WarnLevel, args...)
}
func (entry *Entry) Warning(args ...any) {
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...any) {
func (entry *Entry) Error(args ...interface{}) {
entry.Log(ErrorLevel, args...)
}
func (entry *Entry) Fatal(args ...any) {
func (entry *Entry) Fatal(args ...interface{}) {
entry.Log(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panic(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logArgs(PanicLevel, panicAfter, args...)
}
func (entry *Entry) Panic(args ...interface{}) {
entry.Log(PanicLevel, args...)
}
// Entry Printf family functions
// 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
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.logf(level, panicAfter, format, args...)
entry.Log(level, fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Tracef(format string, args ...any) {
func (entry *Entry) Tracef(format string, args ...interface{}) {
entry.Logf(TraceLevel, format, args...)
}
func (entry *Entry) Debugf(format string, args ...any) {
func (entry *Entry) Debugf(format string, args ...interface{}) {
entry.Logf(DebugLevel, format, args...)
}
func (entry *Entry) Infof(format string, args ...any) {
func (entry *Entry) Infof(format string, args ...interface{}) {
entry.Logf(InfoLevel, format, args...)
}
func (entry *Entry) Printf(format string, args ...any) {
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...any) {
func (entry *Entry) Warnf(format string, args ...interface{}) {
entry.Logf(WarnLevel, format, args...)
}
func (entry *Entry) Warningf(format string, args ...any) {
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...any) {
func (entry *Entry) Errorf(format string, args ...interface{}) {
entry.Logf(ErrorLevel, format, args...)
}
func (entry *Entry) Fatalf(format string, args ...any) {
func (entry *Entry) Fatalf(format string, args ...interface{}) {
entry.Logf(FatalLevel, format, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logf(PanicLevel, panicAfter, format, args...)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
entry.Logf(PanicLevel, format, args...)
}
// Entry Println family functions
// 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
func (entry *Entry) Logln(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.logln(level, panicAfter, args...)
entry.Log(level, entry.sprintlnn(args...))
}
}
func (entry *Entry) Traceln(args ...any) {
func (entry *Entry) Traceln(args ...interface{}) {
entry.Logln(TraceLevel, args...)
}
func (entry *Entry) Debugln(args ...any) {
func (entry *Entry) Debugln(args ...interface{}) {
entry.Logln(DebugLevel, args...)
}
func (entry *Entry) Infoln(args ...any) {
func (entry *Entry) Infoln(args ...interface{}) {
entry.Logln(InfoLevel, args...)
}
func (entry *Entry) Println(args ...any) {
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...any) {
func (entry *Entry) Warnln(args ...interface{}) {
entry.Logln(WarnLevel, args...)
}
func (entry *Entry) Warningln(args ...any) {
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...any) {
func (entry *Entry) Errorln(args ...interface{}) {
entry.Logln(ErrorLevel, args...)
}
func (entry *Entry) Fatalln(args ...any) {
func (entry *Entry) Fatalln(args ...interface{}) {
entry.Logln(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicln(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logln(PanicLevel, panicAfter, args...)
}
func (entry *Entry) Panicln(args ...interface{}) {
entry.Logln(PanicLevel, args...)
}
// 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...)
// 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]
}
+87 -82
View File
@@ -6,12 +6,11 @@ import (
"time"
)
// std is the package-level standard logger, similar to the default logger
// in the stdlib [log] package.
var std = New()
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
// StandardLogger returns the package-level standard logger used by
// the top-level logging functions.
func StandardLogger() *Logger {
return std
}
@@ -42,7 +41,7 @@ func GetLevel() Level {
return std.GetLevel()
}
// IsLevelEnabled checks if logging for the given level is enabled for the standard logger.
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
}
@@ -52,10 +51,9 @@ 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.WithError(err)
return std.WithField(ErrorKey, err)
}
// WithContext creates an entry from the standard logger and adds a context to it.
@@ -63,203 +61,210 @@ func WithContext(ctx context.Context) *Entry {
return std.WithContext(ctx)
}
// 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 {
// 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 {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds the fields to it.
// 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.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// WithTime creates an entry from the standard logger and overrides the time
// used for logs generated with it.
// 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.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level [TraceLevel] on the standard logger.
func Trace(args ...any) {
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
std.Trace(args...)
}
// Debug logs a message at level [DebugLevel] on the standard logger.
func Debug(args ...any) {
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
}
// Print logs a message at level [InfoLevel] on the standard logger.
func Print(args ...any) {
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
std.Print(args...)
}
// Info logs a message at level [InfoLevel] on the standard logger.
func Info(args ...any) {
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
std.Info(args...)
}
// Warn logs a message at level [WarnLevel] on the standard logger.
func Warn(args ...any) {
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
std.Warn(args...)
}
// Warning logs a message at level [WarnLevel] on the standard logger.
func Warning(args ...any) {
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
std.Warning(args...)
}
// Error logs a message at level [ErrorLevel] on the standard logger.
func Error(args ...any) {
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
std.Error(args...)
}
// Panic logs a message at level [PanicLevel] on the standard logger.
func Panic(args ...any) {
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatal(args ...any) {
// 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{}) {
std.Fatal(args...)
}
// TraceFn logs a message from a func at level [TraceLevel] on the standard logger.
// TraceFn logs a message from a func at level Trace on the standard logger.
func TraceFn(fn LogFunction) {
std.TraceFn(fn)
}
// DebugFn logs a message from a func at level [DebugLevel] on the standard logger.
// DebugFn logs a message from a func at level Debug on the standard logger.
func DebugFn(fn LogFunction) {
std.DebugFn(fn)
}
// PrintFn logs a message from a func at level [InfoLevel] on the standard logger.
// PrintFn logs a message from a func at level Info on the standard logger.
func PrintFn(fn LogFunction) {
std.PrintFn(fn)
}
// InfoFn logs a message from a func at level [InfoLevel] on the standard logger.
// InfoFn logs a message from a func at level Info on the standard logger.
func InfoFn(fn LogFunction) {
std.InfoFn(fn)
}
// WarnFn logs a message from a func at level [WarnLevel] on the standard logger.
// WarnFn logs a message from a func at level Warn on the standard logger.
func WarnFn(fn LogFunction) {
std.WarnFn(fn)
}
// WarningFn logs a message from a func at level [WarnLevel] on the standard logger.
// WarningFn logs a message from a func at level Warn on the standard logger.
func WarningFn(fn LogFunction) {
std.WarningFn(fn)
}
// ErrorFn logs a message from a func at level [ErrorLevel] on the standard logger.
// ErrorFn logs a message from a func at level Error on the standard logger.
func ErrorFn(fn LogFunction) {
std.ErrorFn(fn)
}
// PanicFn logs a message from a func at level [PanicLevel] on the standard logger.
// PanicFn logs a message from a func at level Panic on the standard logger.
func PanicFn(fn LogFunction) {
std.PanicFn(fn)
}
// FatalFn logs a message from a func at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
func FatalFn(fn LogFunction) {
std.FatalFn(fn)
}
// Tracef logs a message at level [TraceLevel] on the standard logger.
func Tracef(format string, args ...any) {
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
std.Tracef(format, args...)
}
// Debugf logs a message at level [DebugLevel] on the standard logger.
func Debugf(format string, args ...any) {
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
}
// Printf logs a message at level [InfoLevel] on the standard logger.
func Printf(format string, args ...any) {
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
std.Printf(format, args...)
}
// Infof logs a message at level [InfoLevel] on the standard logger.
func Infof(format string, args ...any) {
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
// Warnf logs a message at level [WarnLevel] on the standard logger.
func Warnf(format string, args ...any) {
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
std.Warnf(format, args...)
}
// Warningf logs a message at level [WarnLevel] on the standard logger.
func Warningf(format string, args ...any) {
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
std.Warningf(format, args...)
}
// Errorf logs a message at level [ErrorLevel] on the standard logger.
func Errorf(format string, args ...any) {
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
std.Errorf(format, args...)
}
// Panicf logs a message at level [PanicLevel] on the standard logger.
func Panicf(format string, args ...any) {
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalf(format string, args ...any) {
// 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{}) {
std.Fatalf(format, args...)
}
// Traceln logs a message at level [TraceLevel] on the standard logger.
func Traceln(args ...any) {
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
std.Traceln(args...)
}
// Debugln logs a message at level [DebugLevel] on the standard logger.
func Debugln(args ...any) {
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
}
// Println logs a message at level [InfoLevel] on the standard logger.
func Println(args ...any) {
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
std.Println(args...)
}
// Infoln logs a message at level [InfoLevel] on the standard logger.
func Infoln(args ...any) {
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
std.Infoln(args...)
}
// Warnln logs a message at level [WarnLevel] on the standard logger.
func Warnln(args ...any) {
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
std.Warnln(args...)
}
// Warningln logs a message at level [WarnLevel] on the standard logger.
func Warningln(args ...any) {
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
std.Warningln(args...)
}
// Errorln logs a message at level [ErrorLevel] on the standard logger.
func Errorln(args ...any) {
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
std.Errorln(args...)
}
// Panicln logs a message at level [PanicLevel] on the standard logger.
func Panicln(args ...any) {
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalln(args ...any) {
// 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{}) {
std.Fatalln(args...)
}
+17 -30
View File
@@ -2,40 +2,27 @@ package logrus
import "time"
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
// 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
)
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
defaultTimestampFormat = time.RFC3339
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:
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// - entry.Message: the message passed to logging methods such as [Info], [Warn], [Error]
// - entry.Time: the timestamp
// - entry.Level: the log level
// * `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.
//
// 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].
// 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`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
@@ -43,12 +30,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.
+14 -23
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"runtime"
"strconv"
)
type fieldKey string
@@ -21,13 +20,7 @@ func (f FieldMap) resolve(key fieldKey) string {
return string(key)
}
// 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.
// JSONFormatter formats logs into parsable json
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
// The format to use is the same than for time.Format or time.Parse from the standard
@@ -68,8 +61,7 @@ type JSONFormatter struct {
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
caller := entry.Caller
data := make(Fields, len(entry.Data)+defaultFields)
data := make(Fields, len(entry.Data)+4)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
@@ -81,14 +73,13 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
if f.DataKey != "" && len(entry.Data) > 0 {
newData := make(Fields, defaultFields+1)
if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
@@ -103,13 +94,11 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
if caller != nil {
var funcVal, fileVal string
if entry.HasCaller() {
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
@@ -119,9 +108,11 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
encoder := json.NewEncoder(b)
-101
View File
@@ -1,101 +0,0 @@
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
}
}
+49 -107
View File
@@ -12,19 +12,17 @@ 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() []any
type LogFunction func() []interface{}
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// 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
@@ -40,44 +38,37 @@ 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 func(int)
ExitFunc exitFunc
// The buffer pool used to format the log. If it is nil, the default global
// buffer pool will be used.
BufferPool BufferPool
}
// 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 exitFunc func(int)
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
}
@@ -113,7 +104,7 @@ func (logger *Logger) newEntry() *Entry {
}
func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]any{}
entry.Data = map[string]interface{}{}
logger.entryPool.Put(entry)
}
@@ -121,7 +112,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 any) *Entry {
func (logger *Logger) WithField(key string, value interface{}) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
@@ -157,12 +148,7 @@ func (logger *Logger) WithTime(t time.Time) *Entry {
return entry.WithTime(t)
}
// 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) {
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logf(level, format, args...)
@@ -170,55 +156,49 @@ func (logger *Logger) Logf(level Level, format string, args ...any) {
}
}
func (logger *Logger) Tracef(format string, args ...any) {
func (logger *Logger) Tracef(format string, args ...interface{}) {
logger.Logf(TraceLevel, format, args...)
}
func (logger *Logger) Debugf(format string, args ...any) {
func (logger *Logger) Debugf(format string, args ...interface{}) {
logger.Logf(DebugLevel, format, args...)
}
func (logger *Logger) Infof(format string, args ...any) {
func (logger *Logger) Infof(format string, args ...interface{}) {
logger.Logf(InfoLevel, format, args...)
}
func (logger *Logger) Printf(format string, args ...any) {
func (logger *Logger) Printf(format string, args ...interface{}) {
entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnf(format string, args ...any) {
func (logger *Logger) Warnf(format string, args ...interface{}) {
logger.Logf(WarnLevel, format, args...)
}
func (logger *Logger) Warningf(format string, args ...any) {
func (logger *Logger) Warningf(format string, args ...interface{}) {
logger.Warnf(format, args...)
}
func (logger *Logger) Errorf(format string, args ...any) {
func (logger *Logger) Errorf(format string, args ...interface{}) {
logger.Logf(ErrorLevel, format, args...)
}
func (logger *Logger) Fatalf(format string, args ...any) {
func (logger *Logger) Fatalf(format string, args ...interface{}) {
logger.Logf(FatalLevel, format, args...)
logger.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicf(format, args...)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.Logf(PanicLevel, format, args...)
}
// 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) {
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
func (logger *Logger) Log(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Log(level, args...)
@@ -226,11 +206,6 @@ func (logger *Logger) Log(level Level, args ...any) {
}
}
// 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()
@@ -239,47 +214,43 @@ func (logger *Logger) LogFn(level Level, fn LogFunction) {
}
}
func (logger *Logger) Trace(args ...any) {
func (logger *Logger) Trace(args ...interface{}) {
logger.Log(TraceLevel, args...)
}
func (logger *Logger) Debug(args ...any) {
func (logger *Logger) Debug(args ...interface{}) {
logger.Log(DebugLevel, args...)
}
func (logger *Logger) Info(args ...any) {
func (logger *Logger) Info(args ...interface{}) {
logger.Log(InfoLevel, args...)
}
func (logger *Logger) Print(args ...any) {
func (logger *Logger) Print(args ...interface{}) {
entry := logger.newEntry()
entry.Print(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warn(args ...any) {
func (logger *Logger) Warn(args ...interface{}) {
logger.Log(WarnLevel, args...)
}
func (logger *Logger) Warning(args ...any) {
func (logger *Logger) Warning(args ...interface{}) {
logger.Warn(args...)
}
func (logger *Logger) Error(args ...any) {
func (logger *Logger) Error(args ...interface{}) {
logger.Log(ErrorLevel, args...)
}
func (logger *Logger) Fatal(args ...any) {
func (logger *Logger) Fatal(args ...interface{}) {
logger.Log(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panic(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(args...)
}
func (logger *Logger) Panic(args ...interface{}) {
logger.Log(PanicLevel, args...)
}
func (logger *Logger) TraceFn(fn LogFunction) {
@@ -318,19 +289,10 @@ func (logger *Logger) FatalFn(fn LogFunction) {
}
func (logger *Logger) PanicFn(fn LogFunction) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(fn()...)
}
logger.LogFn(PanicLevel, fn)
}
// 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) {
func (logger *Logger) Logln(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logln(level, args...)
@@ -338,47 +300,43 @@ func (logger *Logger) Logln(level Level, args ...any) {
}
}
func (logger *Logger) Traceln(args ...any) {
func (logger *Logger) Traceln(args ...interface{}) {
logger.Logln(TraceLevel, args...)
}
func (logger *Logger) Debugln(args ...any) {
func (logger *Logger) Debugln(args ...interface{}) {
logger.Logln(DebugLevel, args...)
}
func (logger *Logger) Infoln(args ...any) {
func (logger *Logger) Infoln(args ...interface{}) {
logger.Logln(InfoLevel, args...)
}
func (logger *Logger) Println(args ...any) {
func (logger *Logger) Println(args ...interface{}) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnln(args ...any) {
func (logger *Logger) Warnln(args ...interface{}) {
logger.Logln(WarnLevel, args...)
}
func (logger *Logger) Warningln(args ...any) {
func (logger *Logger) Warningln(args ...interface{}) {
logger.Warnln(args...)
}
func (logger *Logger) Errorln(args ...any) {
func (logger *Logger) Errorln(args ...interface{}) {
logger.Logln(ErrorLevel, args...)
}
func (logger *Logger) Fatalln(args ...any) {
func (logger *Logger) Fatalln(args ...interface{}) {
logger.Logln(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panicln(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicln(args...)
}
func (logger *Logger) Panicln(args ...interface{}) {
logger.Logln(PanicLevel, args...)
}
func (logger *Logger) Exit(code int) {
@@ -417,22 +375,7 @@ func (logger *Logger) AddHook(hook Hook) {
logger.Hooks.Add(hook)
}
// 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.
// IsLevelEnabled checks if the log level of the logger is greater than the level param
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
@@ -451,7 +394,6 @@ 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()
@@ -461,9 +403,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
}
+85 -122
View File
@@ -1,13 +1,13 @@
package logrus
import (
"bytes"
"fmt"
"log"
"strings"
)
// Fields type, used to pass to [WithFields].
type Fields map[string]any
type Fields map[string]interface{}
// Level type
//
@@ -16,56 +16,39 @@ type Level uint32
// Convert the Level to a string. E.g. [PanicLevel] becomes "panic".
func (level Level) String() string {
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:
if b, err := level.MarshalText(); err == nil {
return string(b)
} else {
return "unknown"
}
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
return parseLevel([]byte(lvl))
}
func parseLevel(b []byte) (Level, error) {
switch {
case bytes.EqualFold(b, []byte("panic")):
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case bytes.EqualFold(b, []byte("fatal")):
case "fatal":
return FatalLevel, nil
case bytes.EqualFold(b, []byte("error")):
case "error":
return ErrorLevel, nil
case bytes.EqualFold(b, []byte("warn")),
bytes.EqualFold(b, []byte("warning")):
case "warn", "warning":
return WarnLevel, nil
case bytes.EqualFold(b, []byte("info")):
case "info":
return InfoLevel, nil
case bytes.EqualFold(b, []byte("debug")):
case "debug":
return DebugLevel, nil
case bytes.EqualFold(b, []byte("trace")):
case "trace":
return TraceLevel, nil
default:
return 0, fmt.Errorf("not a valid logrus Level: %q", b)
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := parseLevel(text)
l, err := ParseLevel(string(text))
if err != nil {
return err
}
@@ -77,11 +60,23 @@ func (level *Level) UnmarshalText(text []byte) error {
func (level Level) MarshalText() ([]byte, error) {
switch level {
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)
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
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// AllLevels exposing all logging levels.
@@ -96,7 +91,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, ...
@@ -118,110 +113,78 @@ const (
TraceLevel
)
// Compile-time interface assertions.
// Won't compile if StdLogger can't be realized by a log.Logger
var (
_ 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 = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
)
// 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(args ...any)
Printf(format string, args ...any)
Println(args ...any)
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(args ...any)
Fatalf(format string, args ...any)
Fatalln(args ...any)
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(args ...any)
Panicf(format string, args ...any)
Panicln(args ...any)
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}
// FieldLogger extends the [StdLogger] interface, generalizing
// the [Entry] and [Logger] types.
type FieldLogger interface {
WithField(key string, value any) *Entry
WithField(key string, value interface{}) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
StdLogger
DebugLogger
InfoLogger
WarnLogger
ErrorLogger
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{})
// Legacy warning aliases. These are kept on FieldLogger for backwards
// compatibility, but are intentionally omitted from [WarnLogger].
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
Warning(args ...any)
Warningf(format string, args ...any)
Warningln(args ...any)
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
}
// 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.
// Ext1FieldLogger (the first extension to [FieldLogger]) is superfluous, it is
// here for consistency. Do not use. Use [FieldLogger], [Logger] or [Entry]
// instead.
type Ext1FieldLogger interface {
FieldLogger
TraceLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
}
+6 -2
View File
@@ -1,7 +1,11 @@
//go:build appengine
// +build appengine
package logrus
func checkIfTerminal(_ any) bool {
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}
+2 -1
View File
@@ -1,4 +1,5 @@
//go:build (darwin || dragonfly || freebsd || netbsd || openbsd || hurd) && !tinygo
// +build darwin dragonfly freebsd netbsd openbsd hurd
// +build !js
package logrus
+7
View File
@@ -0,0 +1,7 @@
// +build js
package logrus
func isTerminal(fd int) bool {
return false
}
+6 -2
View File
@@ -1,7 +1,11 @@
//go:build js || nacl || plan9 || wasi || wasip1 || tinygo
// +build js nacl plan9
package logrus
func checkIfTerminal(_ any) bool {
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return false
}
+2 -6
View File
@@ -1,4 +1,4 @@
//go:build !appengine && !js && !windows && !nacl && !plan9 && !wasi && !wasip1 && !tinygo
// +build !appengine,!js,!windows,!nacl,!plan9
package logrus
@@ -10,11 +10,7 @@ import (
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
fd := v.Fd()
if fd > uintptr(^uint(0)>>1) {
return false
}
return isTerminal(int(fd))
return isTerminal(int(v.Fd()))
default:
return false
}
-2
View File
@@ -1,5 +1,3 @@
//go:build solaris && !tinygo
package logrus
import (
+4 -1
View File
@@ -1,4 +1,7 @@
//go:build (linux || aix || zos) && !tinygo
//go:build (linux || aix || zos) && !js && !wasi
// +build linux aix zos
// +build !js
// +build !wasi
package logrus
+8
View File
@@ -0,0 +1,8 @@
//go:build wasi
// +build wasi
package logrus
func isTerminal(fd int) bool {
return false
}
+8
View File
@@ -0,0 +1,8 @@
//go:build wasip1
// +build wasip1
package logrus
func isTerminal(fd int) bool {
return false
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build windows && !appengine
// +build !appengine,!js,windows
package logrus
+175 -318
View File
@@ -3,32 +3,30 @@ package logrus
import (
"bytes"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
)
var baseTimestamp = time.Now()
const (
red = 31
yellow = 33
blue = 36
gray = 37
)
// 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.
var baseTimestamp time.Time
func init() {
baseTimestamp = time.Now()
}
// TextFormatter formats logs into text
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
@@ -66,7 +64,7 @@ type TextFormatter struct {
// be desired.
DisableSorting bool
// The keys sorting function, when uninitialized it uses slices.Sort.
// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
@@ -79,22 +77,16 @@ type TextFormatter struct {
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal. Don't use this field
// directly; use TextFormatter.isTerminal instead.
terminal bool
// Whether the logger's out is to a terminal
isTerminal 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
@@ -104,78 +96,54 @@ 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) 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
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
}
}
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(isTerminal bool) bool {
if f.DisableColors {
return false
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
}
}
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
return isColored && !f.DisableColors
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
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)
data := make(Fields)
for k, v := range entry.Data {
data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
var funcVal, fileVal string
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)
fixedKeys := make([]string, 0, 4+len(data))
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
@@ -186,14 +154,12 @@ func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string,
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
var funcVal, fileVal string
if caller != nil {
if entry.HasCaller() {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(caller)
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
} else {
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
}
if funcVal != "" {
@@ -206,108 +172,152 @@ func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string,
if !f.DisableSorting {
if f.SortingFunc == nil {
// Default sorting does not sort the "fixed keys";
// see https://github.com/sirupsen/logrus/commit/73bc94e60c753099e8bae902f81fbd6e7dd95f26
slices.Sort(keys)
sort.Strings(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
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)
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]
}
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)
}
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) {
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)
}
// 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")
var callerText string
if caller := entry.Caller; caller != nil {
var funcVal, fileVal string
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
if caller.Function != "" {
funcVal = caller.Function + "()"
}
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if fileVal == "" {
callerText = funcVal
caller = funcVal
} else if funcVal == "" {
callerText = fileVal
caller = fileVal
} else {
callerText = fileVal + " " + funcVal
caller = fileVal + " " + funcVal
}
}
levelText := levelPrefix(entry.Level, f.DisableLevelTruncation, f.PadLevelText)
switch {
case f.DisableTimestamp:
_, _ = fmt.Fprintf(b, "%s%s %-44s ", levelText, callerText, entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
case !f.FullTimestamp:
_, _ = fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), callerText, entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
default:
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
_, _ = fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), callerText, entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
}
if !f.DisableSorting {
if f.SortingFunc == nil {
slices.Sort(keys)
} else {
f.SortingFunc(keys)
}
}
// 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])
v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
b.WriteByte('\n')
}
// 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) {
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
}
}
return false
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
if b.Len() > 0 {
b.WriteByte(' ')
}
@@ -316,168 +326,15 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) {
f.appendValue(b, 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
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
stringVal, ok := value.(string)
if !ok {
stringVal = fmt.Sprint(value)
}
// 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
if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
fmt.Fprintf(b, "%q", stringVal)
}
}
+4 -2
View File
@@ -30,7 +30,7 @@ func (entry *Entry) Writer() *io.PipeWriter {
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
printFunc := entry.Print
var printFunc func(args ...interface{})
// Determine which log function to use based on the specified log level
switch level {
@@ -48,6 +48,8 @@ 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.
@@ -61,7 +63,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 ...any)) {
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader)
// Set the buffer size to the maximum token size to avoid buffer overflows
+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.10.1
## explicit; go 1.23
# github.com/sirupsen/logrus v1.9.4
## explicit; go 1.17
github.com/sirupsen/logrus
# github.com/spf13/cobra v1.10.2
## explicit; go 1.15