Compare commits

...
50 Commits
Author SHA1 Message Date
Sebastiaan van StijnandGitHub d072fe407f Merge pull request #7234 from vvoland/update-dco
docs: Update DCO identity guidance
2026-08-24 12:16:16 +02:00
Paweł Gronowski 3fe73079aa docs: Clarify DCO identity guidance
The DCO policy used here appears to have been derived from the Linux
kernel's guidance. In 2023, that guidance changed to relax the "real
name" requirement to a "known identity".

The DCO sign-off identifies the person certifying that they have the
right to submit a contribution. The previous wording required a "real
name" and rejected pseudonyms, which could discourage contributors
whose preferred or community name is the identity by which they can be
known.

The requirement is not anonymity: the identity in the sign-off must be
known enough for the project to follow up about the contribution if a
question arises. It does not require a legal name or a government-issued
identity. Use the clarified wording from the Linux kernel so known
pseudonyms and preferred names remain acceptable while anonymous
contributions remain excluded.

This also reflects concerns raised in the CNCF community about whether
"real name" means a legal name, a government-issued identity, or a
public identity that is sufficient to identify and contact a
contributor. The discussion emphasized that attribution and future
accountability do not require publishing a legal identity, and that
strict public-name rules can exclude people who cannot safely contribute
under that name.

Source: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d4563201f33a022fc035

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-24 11:41:24 +02:00
Paweł GronowskiandGitHub 28f756087e Merge pull request #7224 from vvoland/update-go
update to go1.26.7
2026-08-20 12:38:49 +02:00
Paweł Gronowski 387b8e2af8 update to go1.26.7
This release includes a fix to address a breakage affecting unencrypted
HTTP/2 (h2c) connections caused by a security patch included in last
patch release.

See go.dev/issue/80876 for details.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-19 20:23:24 +02:00
Paweł GronowskiandGitHub 55e4a7eef5 Merge pull request #7222 from thaJeztah/bump_logrus
vendor: github.com/sirupsen/logrus v1.10.1
2026-08-19 12:34:56 +02:00
Sebastiaan van Stijn 58d9cdd2d9 vendor: github.com/sirupsen/logrus v1.10.1
Notable changes:

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

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

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-19 11:34:53 +02:00
Sebastiaan van StijnandGitHub 8659aa5d4c Merge pull request #7185 from thaJeztah/duplicate_normalize
cli/command/manifest: remove redundant normalization
2026-08-14 18:42:04 +02:00
Paweł GronowskiandGitHub 8d08c75e81 Merge pull request #7191 from thaJeztah/bump_go
update to go1.26.6
2026-08-14 14:52:56 +02:00
Sebastiaan van Stijn 24af004cbf update to go1.26.6
This release includes 10 security fixes following the security policy:

- x/mod/sumdb/tlog: fix transparency log tile verification bypass

    A malicious GOPROXY was previously capable of forging
    up to two sumdb tiles that allow for a requested module
    to bypass the GOSUMDB check and persist attacker-controlled
    module content to a local Go module cache.

    This attack allows for a malicious GOPROXY to serve
    malicious module content that cannot be detected
    by evaluating the transparency log.

    All tiles are now correctly verified against their parents.

    In order to determine if you have been affected:

    rm -r go.sum go.work.sum vendor/ && go mod tidy

    Thanks to Filippo Valsorda (Geomys) for reporting this issue.

    This is CVE-2026-56865 and Go issue https://go.dev/issue/80744.

- x/mod/sumdb: ignore unrelated, unauthenticated hashes in Lookup

    A malicious GOSUMDB was capable of serving arbitrary
    module content not contained within the transparency
    log.

    This attack allows for a coordinating GOPROXY and
    GOSUMDB to serve a client malicious module content
    that cannot be detected by evaluating the transparency
    log.

    In order to determine if you have been affected:

    rm -r go.sum go.work.sum vendor/ && go mod tidy

    Thanks to mundur for reporting this issue.

    This is CVE-2026-56864 and Go issue https://go.dev/issue/80745.

- encoding/xml: add recursion depth guard during decode

    Previously, DecodeElement would reset the depth counter
    causing it to never fire; this could lead to stack
    exhaustion.

    This is CVE-2026-56859 and Go issue https://go.dev/issue/80481.

 net/http: apply ReadHeaderTimeout when doing unencrypted HTTP/2 check

    When a server is configured to support unencrypted HTTP/2, it reads a
    few bytes from each new connection to see if they contain the HTTP/2
    client preface. Previously, this was being done with no timeout applied.
    ReadHeaderTimeout is now applied for this.

    This is CVE-2026-56853 and Go issue https://go.dev/issue/80205.

- net/url: avoid quadratic complexity in resolvePath

    Previously, resolving relative paths containing parent directory (..) segments performed string conversions and buffer rewrites on each step, resulting in quadratic time complexity and high memory allocation overhead.

    Now, path resolution operates on a byte buffer using index-based backtracking for .. segments, eliminating the quadratic time complexity and significantly reducing memory allocations.

    This is CVE-2026-56860 and Go issue https://go.dev/issue/80494.

- golang.org/x/net/dns/dnsmessage: panic when parsing invalid SVCB record

    Parsing an invalid SVCB or HTTPS RR can panic when
    the size of a parameter value overflows the message buffer.

    Thanks to Mundur (https://github.com/M0nd0R) for reporting this issue.

    This is CVE-2026-46600 and Go issue https://go.dev/issue/79795.

- crypto/tls: limit handshake messages we are willing to accept post-handshake

    Previously, we always counted handshake messages, such as KeyUpdate, as
    state-advancing, regardless of whether a handshake has been completed or
    not. As a result, a malicious client can keep sending KeyUpdate messages
    to force the server to keep performing key derivation operations
    indefinitely.

    Thanks to Qi Deng of Aurascape.ai for reporting this issue.

    This is CVE-2026-56862 and Go issue https://go.dev/issue/80528.

- html/template: fix Javascript regexp context tracking

    Previously, pathological inputs could close an
    unescaped / early, allowing for attack-controlled
    data to inject arbitrary content, potentially
    leading to XSS.

    Thanks to Ali Sherif for reporting this issue.

    This is CVE-2026-56858 and Go issue https://go.dev/issue/80435.

- x/net/idna: failure to reject ASCII-only Punycode-encoded labels

    The ToASCII and ToUnicode functions incorrectly accepted Punycode-encoded labels
    that decode to an ASCII-only label. For example, ToUnicode("xn--example-.com")
    incorrectly returned the name "example.com" rather than an error.

    The idna package implements the processing algorithm from UTS 46.
    Older versions of UTS 46 included a specification bug which permitted
    multiple ASCII labels to decode to the same Unicode label.
    UTS 46 revision 33 fixed the specification bug.
    The idna package now implements the updated specification.

    This behavior can lead to privilege escalation in programs using the idna package.
    For example, a program which performs privilege checks on the ASCII hostname
    may reject "example.com" but permit "xn--example-.com". If that program subsequently
    converts the ASCII hostname to Unicode, it will inadvertently permits access
    to the Unicode name "example.com".

    Thanks to KC1zs4 (https://github.com/KC1zs4) for reporting this issue.

    This is CVE-2026-39821 and Go issue https://go.dev/issue/78760.

- encoding/asn1: enforce maximum recursion depth

    Enforce a recursion limit in Unmarshal to prevent stack exhaustion
    when parsing deeply-nested, recursive structures.

    Thanks to Marwan Atia (marwans...@gmail.com) for reporting this issue.

    This is CVE-2026-33818 and Go issue https://go.dev/issue/80405.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-14 14:02:40 +02:00
Sebastiaan van StijnandGitHub eb68ed19d2 Merge pull request #7157 from docker/dependabot/github_actions/codeql-actions-8417568871
build(deps): bump the codeql-actions group across 1 directory with 3 updates
2026-08-13 22:42:40 +02:00
dependabot[bot]andGitHub 8a375034e2 build(deps): bump the codeql-actions group across 1 directory with 3 updates
Bumps the codeql-actions group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

Updates `github/codeql-action/autobuild` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

Updates `github/codeql-action/analyze` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 08:46:20 +00:00
Sebastiaan van Stijn a7c0741fa7 cli/command/manifest: remove redundant normalization
commit 55fcffe743 added normalization
in config/configfile, so it's no longer needed to normalize here as
well.

updates 55fcffe743

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-12 16:38:26 +02:00
Sebastiaan van StijnandGitHub 12892075d2 Merge pull request #7180 from docker/dependabot/github_actions/docker-actions-ebe691cc85
build(deps): bump docker/github-builder/.github/workflows/bake.yml from 1.15.0 to 1.16.0 in the docker-actions group
2026-08-12 15:37:56 +02:00
dependabot[bot]andGitHub e1f9b284df build(deps): bump docker/github-builder/.github/workflows/bake.yml
Bumps the docker-actions group with 1 update: [docker/github-builder/.github/workflows/bake.yml](https://github.com/docker/github-builder).


Updates `docker/github-builder/.github/workflows/bake.yml` from 1.15.0 to 1.16.0
- [Release notes](https://github.com/docker/github-builder/releases)
- [Commits](https://github.com/docker/github-builder/compare/27ade872c1e2296e62ef15ab3b10d37665e57cf7...a492c6d04fd3315f67230809b44d60cc0acd50b3)

---
updated-dependencies:
- dependency-name: docker/github-builder/.github/workflows/bake.yml
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 08:43:00 +00:00
Sebastiaan van StijnandGitHub 4f84911bfe Merge pull request #7144 from hirehamir/fix/ps-port-sort
cli/command/formatter: sort published ports numerically by IP
2026-08-06 15:50:28 +02:00
Paweł GronowskiandGitHub 228f29e810 Merge pull request #7159 from thaJeztah/version_29.8
VERSION: 29.8.0
2026-08-06 15:47:41 +02:00
Sebastiaan van Stijn d8c3543407 VERSION: 29.8.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-06 15:34:41 +02:00
Hamir 2dd7b7e15b cli/command/formatter: sort published ports numerically by IP
`comparePorts` compared host IPs with `i.IP.String() < j.IP.String()`, which
sorts them lexicographically, so "10.0.0.2" ordered before "9.0.0.1" in
`docker ps` output.

This also avoids formatting both addresses as strings on every comparison.

ports    before               after
64       1540 allocs/op       394 allocs/op
256      5364 allocs/op       1548 allocs/op

That's a ~74% (3.9x) reduction in allocations.

Signed-off-by: Hamir <hirehamir@gmail.com>
2026-08-05 11:38:20 -07:00
Paweł GronowskiandGitHub a7dcaa6fdb Merge pull request #7151 from vvoland/update-archive
vendor: github.com/moby/go-archive v0.3.3
2026-08-05 19:34:15 +02:00
Paweł Gronowski 400b45f682 vendor: github.com/moby/go-archive v0.3.3
full diff: https://github.com/moby/go-archive/compare/v0.3.2...v0.3.3

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-05 19:11:05 +02:00
Sebastiaan van StijnandGitHub 38887ec4ed Merge pull request #7145 from winklemad/fix-service-duplicate-removal-panic
cli/command/service: fix panic when removing duplicate values
2026-08-05 19:10:29 +02:00
Sebastiaan van StijnandGitHub 904aef7f69 Merge pull request #7147 from docker/dependabot/github_actions/docker-actions-269857e486
build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml from 2.0.2 to 2.0.3 in the docker-actions group across 1 directory
2026-08-04 13:11:45 +02:00
dependabot[bot]andGitHub f08e60eb09 build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml
Bumps the docker-actions group with 1 update in the / directory: [docker/docker-agent-action/.github/workflows/review-pr.yml](https://github.com/docker/docker-agent-action).


Updates `docker/docker-agent-action/.github/workflows/review-pr.yml` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/docker/docker-agent-action/releases)
- [Commits](https://github.com/docker/docker-agent-action/compare/774b6e0e60d6c648b0f2dc43bd5221377a0a7420...baf90543d81f5de59751dfd10e6cf45e21a5a982)

---
updated-dependencies:
- dependency-name: docker/docker-agent-action/.github/workflows/review-pr.yml
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 08:42:59 +00:00
Paweł GronowskiandGitHub abfd89157d Merge pull request #7149 from vvoland/update-docker
VERSION: 29.7.2
2026-08-03 18:24:10 +02:00
Paweł Gronowski 519eb45d03 VERSION: 29.7.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-03 18:18:56 +02:00
Madan Kumar 8717af7168 cli/command/service: fix panic when removing duplicate values
Both makeEnv and updateHosts removed elements from a slice while ranging
over that same slice. The range expression is evaluated once, so after a
removal the loop keeps using the original length: it reads stale elements
that shifted down, skips a live element, and can slice past the end of the
shrunken slice, which panics.

In makeEnv, the "no update required" continue also applied to the inner
loop instead of skipping the re-append, so an env-var passed twice with
the same value was stored twice. A third occurrence with a different value
then tried to remove both entries and panicked:

    docker service create --env A=1 --env A=1 --env A=2 --name repro nginx
    panic: runtime error: slice bounds out of range [2:1]

The same happens with an env-file that lists a variable twice, and the
panic occurs before any API call, so no daemon is needed to hit it.

updateHosts has the same problem when a hostname is listed more than once
in a single entry: --host-rm either leaves a copy behind or panics with
"slice bounds out of range". That needs a spec written through the API or
swarmkit directly, as the CLI does not produce such entries itself, so it
is less likely to be hit in practice.

Use slices.DeleteFunc for both, which removes every match in one pass, and
add tests for makeEnv, which had no coverage.

Signed-off-by: Madan Kumar <winklemad@outlook.com>
2026-08-02 07:13:16 +05:30
Paweł GronowskiandGitHub e9452d6e78 Merge pull request #7142 from thaJeztah/bump_go_archive_0.3.2
vendor: github.com/moby/go-archive v0.3.2
2026-07-31 19:03:37 +02:00
Sebastiaan van Stijn a6014a702b vendor: github.com/moby/go-archive v0.3.2
full diff: https://github.com/moby/go-archive/compare/v0.3.1...v0.3.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:53:47 +02:00
Paweł GronowskiandGitHub 2465fca604 Merge pull request #7140 from thaJeztah/bump_go_archive_0.3.1
vendor: github.com/moby/go-archive v0.3.1
2026-07-31 18:33:04 +02:00
Sebastiaan van StijnandGitHub 52b15cc571 Merge pull request #7141 from thaJeztah/bump_version2
update version to 29.7.1
2026-07-31 18:20:09 +02:00
Sebastiaan van Stijn 13590921a4 update version to 29.7.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:11:45 +02:00
Sebastiaan van Stijn 0b50545471 vendor: github.com/moby/go-archive v0.3.1
full diff: https://github.com/moby/go-archive/compare/v0.3.0...v0.3.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:10:04 +02:00
Paweł GronowskiandGitHub c1eba931e3 Merge pull request #7086 from mickael-docker/docs-authz-decoding
docs(authz): clarify daemon parsing semantics
2026-07-30 22:16:32 +02:00
Paweł GronowskiandGitHub 1a305be376 Merge pull request #7084 from thaJeztah/prompt_cleans
cli/command: PromptUserForCredentials: don't mutate cli
2026-07-30 22:15:34 +02:00
Sebastiaan van StijnandGitHub 4c1c648df1 Merge pull request #7129 from docker/dependabot/github_actions/codeql-actions-f42a4cd18a
build(deps): bump the codeql-actions group across 1 directory with 3 updates
2026-07-30 21:56:12 +02:00
Sebastiaan van StijnandGitHub ee046d35c0 Merge pull request #7134 from hirehamir/fix/flaky-client-hangs
cli/command: fix flaky TestInitializeFromClientHangs
2026-07-30 21:54:54 +02:00
Sebastiaan van StijnandGitHub d70c3fd5e7 Merge pull request #7139 from thaJeztah/vendor_go_archive
vendor: github.com/moby/go-archive v0.3.0
2026-07-30 21:49:42 +02:00
Sebastiaan van Stijn f6d6bede46 vendor: github.com/moby/go-archive v0.3.0
full diff: https://github.com/moby/go-archive/compare/v0.2.1...v0.3.0

v0.3.0

This release fixes CVE-2026-17106 / GHSA-hfg8-hc9c-6c3h, where a crafted
tar archive could use links to cause extraction operations to create or
overwrite files outside the intended destination directory.

The issue affected Unpack, UnpackLayer, Untar, UntarUncompressed, and the
ApplyLayer helpers. Users should upgrade and avoid extracting untrusted
archives with earlier versions.

What's Changed

* archive: harden tar extraction against path traversal
* archive: do not follow reparse points in chtimes
* archive: fix creation time updates on Windows
* archive: minor cleanups and godoc touch-up
* archive: RebaseArchiveEntries: fix archive path rebasing

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-30 19:17:27 +02:00
Sebastiaan van StijnandGitHub b47659808a Merge pull request #7026 from vvoland/fix-hostname
cli/file_store: Go 1.26 compatibility
2026-07-30 15:51:46 +02:00
dependabot[bot]andGitHub af626e1ed7 build(deps): bump the codeql-actions group across 1 directory with 3 updates
Bumps the codeql-actions group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.1 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

Updates `github/codeql-action/autobuild` from 4.37.1 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

Updates `github/codeql-action/analyze` from 4.37.1 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-30 08:46:12 +00:00
Sebastiaan van StijnandGitHub 6d35de4601 Merge pull request #7137 from crazy-max/bin-image-github-builder
ci: use docker github builder to build bin image
2026-07-29 16:58:14 +02:00
CrazyMaxandCrazyMax ad8dce1012 ci: use docker github builder to build bin image
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-07-29 16:32:49 +02:00
Hamir 844210bd0f cli/command: fix flaky TestInitializeFromClientHangs
Signed-off-by: Hamir <hirehamir@gmail.com>
2026-07-28 15:06:53 -07:00
Paweł GronowskiandGitHub bd719d6703 Merge pull request #7131 from vvoland/build-oidc
gha/build: Use OIDC for Docker Hub login
2026-07-28 17:10:00 +02:00
Paweł Gronowski 52b1160c85 gha: bump docker/login-action from 4.4.0 to 4.5.0
OIDC login requires docker/login-action v4.5.0 or newer

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-28 15:38:23 +02:00
Paweł Gronowski 5ff2ff6311 gha/build: Use OIDC for Docker Hub login
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-28 14:28:40 +02:00
mickael emirkanian 2d978b80ae docs(authz): clarify daemon parsing semantics
Signed-off-by: mickael emirkanian <mickael.emirkanian@docker.com>
2026-07-09 13:36:32 -04:00
Sebastiaan van Stijn 2abf92e95c cli/command: PromptUserForCredentials: don't mutate cli
PromptUserForCredentials accepted a Cli as argument so that it could swap
the input stream on Windows (cli.SetIn).

Given that we only require this swap for the duration of this function (if
needed at all), we can use a local variable that either uses cli.In() or
os.Stdin (on Windows).

We currently still need to wrap the os.Stdin into a streams.In, but can use
the raw os.Stdin (and/or cli.In().File()) once prompt.DisableInputEcho is
updated.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-07 14:19:20 +02:00
Paweł Gronowski 3cc61496db cli/file_store: Preserve IPv6 URL normalization after Go change
Add a fallback for unbracketed IPv6 literals to preserve behavior.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-06-03 17:40:39 +02:00
Paweł Gronowski 55b88882d7 cli/file_store: Clarify ConvertToHostname
The implementation seems to have diverged already.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-06-03 17:40:39 +02:00
66 changed files with 2112 additions and 1114 deletions
+25 -39
View File
@@ -98,46 +98,32 @@ jobs:
if-no-files-found: error
bin-image:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps:
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: dockereng/cli-bin
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
-
name: Build and push image
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
with:
files: |
./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
uses: docker/github-builder/.github/workflows/bake.yml@a492c6d04fd3315f67230809b44d60cc0acd50b3 # v1.16.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
with:
setup-qemu: true
target: bin-image-cross
cache: true
cache-scope: bin-image
output: image
push: true
vars: |
VERSION=${{ github.ref }}
meta-images: |
dockereng/cli-bin
meta-tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
registry-identities: |
- type: dockerhub
username: dockereng
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
prepare-plugins:
runs-on: ubuntu-24.04
+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.5"
go-version: "1.26.7"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
category: "/language:go"
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@774b6e0e60d6c648b0f2dc43bd5221377a0a7420 # v2.0.2
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@baf90543d81f5de59751dfd10e6cf45e21a5a982 # v2.0.3
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
+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.5"
go-version: "1.26.7"
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.5"
go-version: "1.26.7"
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.5"
go: "1.26.7"
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 your real name (sorry, no pseudonyms or anonymous contributions.)
Use a known identity (sorry, no 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.5
ARG GO_VERSION=1.26.7
# 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.7.0
29.8.0
+33 -15
View File
@@ -208,52 +208,70 @@ func TestInitializeFromClient(t *testing.T) {
// Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) {
const (
// Sized against measured scheduler stalls:
// under CPU pressure this test sees 40-90ms stalls;
// this should give about 5x headroom.
// See https://github.com/docker/cli/issues/6003.
clientInitTimeout = 500 * time.Millisecond
// This is only a backstop against a genuine hang.
// It should never be reached on a healthy run.
// So, it should be fine to have a lenient timeout here.
waitTimeout = 10 * time.Second
)
tmpDir := t.TempDir()
socket := filepath.Join(tmpDir, "my.sock")
l, err := net.Listen("unix", socket)
assert.NilError(t, err)
receiveReqCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
// Buffered, so the handler can record
// that it was reached without a reader
// having to be ready at that instant.
receivedReqCh := make(chan struct{}, 1)
releaseHandlerCh := make(chan struct{})
// Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-timeoutCtx.Done():
case receiveReqCh <- true: // Blocks until someone receives on the channel.
case receivedReqCh <- struct{}{}:
default:
}
_, _ = w.Write([]byte("OK"))
<-releaseHandlerCh
}))
ts.Listener = l
ts.Start()
defer ts.Close()
t.Cleanup(func() {
close(releaseHandlerCh)
ts.Close()
})
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
initializedCh := make(chan bool)
initErrCh := make(chan error, 1)
go func() {
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err)
cli.CurrentVersion()
close(initializedCh)
initErrCh <- err
}()
select {
case <-timeoutCtx.Done():
case err := <-initErrCh:
assert.Check(t, err)
case <-time.After(waitTimeout):
t.Fatal("timeout waiting for initialization to complete")
case <-initializedCh:
}
select {
case <-timeoutCtx.Done():
case <-receivedReqCh:
case <-time.After(waitTimeout):
t.Fatal("server never received an init request")
case <-receiveReqCh:
}
}
+1 -1
View File
@@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool {
}
if i.IP != j.IP {
return i.IP.String() < j.IP.String()
return i.IP.Less(j.IP)
}
if i.PublicPort != j.PublicPort {
+18
View File
@@ -946,6 +946,24 @@ 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 {
+1 -23
View File
@@ -48,27 +48,6 @@ 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 {
@@ -78,8 +57,7 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
}
cfg := dockerCLI.ConfigFile()
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
configKey := getAuthConfigKey(domainName)
a, _ := cfg.GetAuthConfig(configKey)
a, _ := cfg.GetAuthConfig(domainName)
return registry.AuthConfig{
Username: a.Username,
Password: a.Password,
+12 -7
View File
@@ -97,17 +97,22 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
// On Windows, force the use of the regular OS stdin stream.
//
// StdStreams() may wrap stdin with windowsconsole.NewAnsiReader to
// emulate VT input on consoles that do not support it natively, but
// that wrapper has historically caused interactive prompts to hang
// or behave incorrectly.
//
// See:
// - https://github.com/moby/moby/issues/14336
// - https://github.com/moby/moby/issues/14210
// - https://github.com/moby/moby/pull/17738
//
// TODO(thaJeztah): we need to confirm if this special handling is still needed, as we may not be doing this in other places.
stdIn := cli.In()
if runtime.GOOS == "windows" {
cli.SetIn(streams.NewIn(os.Stdin))
// TODO(thaJeztah); change to io.Reader and skip wrapping once prompt.DisableInputEcho no longer requires a streams.In
stdIn = streams.NewIn(os.Stdin)
}
argUser = strings.TrimSpace(argUser)
@@ -132,7 +137,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
}
var err error
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
argUser, err = prompt.ReadInput(ctx, stdIn, cli.Out(), msg)
if err != nil {
return registrytypes.AuthConfig{}, err
}
@@ -146,7 +151,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
isEmpty := strings.TrimSpace(argPassword) == ""
if isEmpty {
restoreInput, err := prompt.DisableInputEcho(cli.In())
restoreInput, err := prompt.DisableInputEcho(stdIn)
if err != nil {
return registrytypes.AuthConfig{}, err
}
@@ -166,7 +171,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
}
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
argPassword, err = prompt.ReadInput(ctx, stdIn, cli.Out(), "Password: ")
if err != nil {
return registrytypes.AuthConfig{}, err
}
+7 -8
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@@ -687,15 +688,13 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
}
currentEnv := make([]string, 0, len(envVariables))
for _, env := range envVariables { // need to process each var, in order
k, _, _ := strings.Cut(env, "=")
for i, current := range currentEnv { // remove duplicates
if current == env {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
if strings.HasPrefix(current, k+"=") {
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
}
if slices.Contains(currentEnv, env) {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
k, _, _ := strings.Cut(env, "=")
currentEnv = slices.DeleteFunc(currentEnv, func(current string) bool { // remove duplicates
return strings.HasPrefix(current, k+"=")
})
currentEnv = append(currentEnv, env)
}
+46
View File
@@ -373,3 +373,49 @@ func TestToServiceSysCtls(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
func TestMakeEnv(t *testing.T) {
tests := []struct {
doc string
env []string
expected []string
}{
{
doc: "no duplicates",
env: []string{"one=1", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "same variable repeated",
env: []string{"one=1", "one=1"},
expected: []string{"one=1"},
},
{
doc: "same variable repeated, then overridden",
env: []string{"one=1", "one=1", "one=2"},
expected: []string{"one=2"},
},
{
doc: "repeated variable last",
env: []string{"one=1", "two=2", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "last value wins",
env: []string{"one=1", "two=2", "one=3"},
expected: []string{"two=2", "one=3"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
o := newServiceOptions()
for _, env := range tc.env {
assert.NilError(t, o.env.Set(env))
}
actual, err := o.makeEnv()
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(tc.expected, actual))
})
}
}
+3 -5
View File
@@ -1213,11 +1213,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
if rm.IPAddr != "" && rm.IPAddr != ip {
continue
}
for i, h := range hostNames {
if h == rm.Host {
hostNames = append(hostNames[:i], hostNames[i+1:]...)
}
}
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
return h == rm.Host
})
}
if len(hostNames) > 0 {
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
+15
View File
@@ -1727,3 +1727,18 @@ func TestUpdateUlimits(t *testing.T) {
})
}
}
func TestUpdateHostsRemoveRepeatedHost(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("host-rm", "host1")
//nolint:dupword // ignore "Duplicate words (host1) found"
hosts := []string{"127.0.0.1 host1 host1 host2", "127.0.0.2 host2 host1 host1"}
err := updateHosts(flags, &hosts)
assert.NilError(t, err)
// All occurrences of `host1` should be removed, also if the same host
// is listed multiple times in the same entry.
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 host2", "127.0.0.2 host2"}, hosts))
}
+42 -1
View File
@@ -104,7 +104,7 @@ func (c *fileStore) Store(authConfig types.AuthConfig) error {
// stored as hostname or as hostname including scheme (in legacy configuration
// files).
//
// It's the equivalent to [registry.ConvertToHostname] in the daemon.
// It's based on [registry.ConvertToHostname] from Moby daemon.
//
// [registry.ConvertToHostname]: https://pkg.go.dev/github.com/moby/moby/v2@v2.0.0-beta.7/daemon/pkg/registry#ConvertToHostname
func ConvertToHostname(maybeURL string) string {
@@ -117,7 +117,48 @@ func ConvertToHostname(maybeURL string) string {
}
return net.JoinHostPort(u.Hostname(), u.Port())
}
if hostName := hostFromURLFallback(stripped); hostName != "" {
return hostName
}
}
hostName, _, _ := strings.Cut(stripped, "/")
return hostName
}
// hostFromURLFallback extracts a host from scheme URLs that net/url rejects.
// Go rejects unbracketed IPv6 literals in URL hosts since
// https://github.com/golang/go/commit/0c28789bd7dfc55099cac86a3212dda0d6c091f6
func hostFromURLFallback(maybeURL string) string {
_, rest, ok := strings.Cut(maybeURL, "://")
if !ok {
return ""
}
hostName, _, _ := strings.Cut(rest, "/")
if hostName == "" {
return ""
}
if strings.Count(hostName, ":") > 1 && !strings.HasPrefix(hostName, "[") {
portStart := strings.LastIndex(hostName, ":")
addr, port := hostName[:portStart], hostName[portStart+1:]
if addr != "" && isPort(port) {
return net.JoinHostPort(addr, port)
}
}
return hostName
}
func isPort(port string) bool {
if port == "" {
return false
}
for _, r := range port {
if r < '0' || r > '9' {
return false
}
}
return true
}
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.5
ARG GO_VERSION=1.26.7
# 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.5
ARG GO_VERSION=1.26.7
# 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.5
ARG GO_VERSION=1.26.7
# 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
+12 -9
View File
@@ -95,6 +95,18 @@ The Engine's authorization middleware fails closed: when a plugin returns an err
the request is denied and the error is surfaced to the client. Plugins should also fail closed: if the plugin
cannot confidently evaluate a request, it should return an error or `Allow: false`.
> [!WARNING]
> Because the plugin receives the [**raw** request body](#authzpluginauthzreq) from the daemon, it must
> apply the same decoding semantics as the daemon to be sure it evaluates the request the daemon will
> act on. The daemon decodes JSON with Go's [`encoding/json.Unmarshal`](https://pkg.go.dev/encoding/json#Unmarshal).
>
> The same requirement applies to the response body. Plugins that depend on `ResponseBody`
> inspection for redaction or content-filtering should restrict their policies to endpoints
> whose response is produced as a single write (typical of REST-style API responses). For
> commands whose responses are streamed or are likely to exceed the [buffer](#response-body-size-and-partial-buffering) through multiple
> writes, do not rely on `ResponseBody` for security-relevant decisions; perform the filtering
> in a separate layer in front of the daemon.
### Response body size and partial buffering
The internal buffer that holds the response body between the daemon's HTTP
@@ -111,15 +123,6 @@ is the practical effect of this 64 KiB threshold combined with the
is immediately drained to the client and is therefore no longer available
for plugin inspection by the time the handler returns.
> [!NOTE]
> Plugins that depend on `ResponseBody` inspection for redaction or
> content-filtering should restrict their policies to endpoints whose
> response is produced as a single write (typical of REST-style API
> responses). For commands whose responses are streamed or are likely to
> exceed the buffer through multiple writes, do not rely on `ResponseBody`
> for security-relevant decisions; perform the filtering in a separate
> layer in front of the daemon.
During request/response processing, some authorization flows might
need to do additional queries to the Docker daemon. To complete such flows,
plugins can call the daemon API similar to a regular user. To enable these
+2 -2
View File
@@ -31,7 +31,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.24
github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee // main / v0.3.0-dev
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.1
github.com/moby/patternmatcher v0.6.1
@@ -46,7 +46,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/sirupsen/logrus v1.9.4
github.com/sirupsen/logrus v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
+10 -8
View File
@@ -32,7 +32,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
@@ -107,8 +106,8 @@ github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee h1:VUrUP/hu1E43KunXVZlHsNstFGeZOpm/CQoLx5OSuMw=
github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
@@ -121,6 +120,10 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
@@ -149,7 +152,6 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -172,8 +174,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -183,8 +185,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 h1:TvtdmeYsYEij78hS4oxnwikoiLdIrgav3BA+CbhaDAI=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+188 -81
View File
@@ -17,6 +17,7 @@ import (
"time"
"github.com/containerd/log"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/patternmatcher"
"github.com/moby/sys/sequential"
"github.com/moby/sys/user"
@@ -81,9 +82,22 @@ type (
// were probably in the archive for a reason, so set this option at
// your own peril.
BestEffortXattrs bool
// internalOptions contains options for use by packages within this module.
internalOptions *archiveoptions.Options
}
)
// WithProcSelfFD returns a copy of opts prepared for extraction in a
// filesystem context where /proc/self/fd may not be accessible by path.
//
// The caller must invoke the returned cleanup function after extraction
// completes. On platforms that do not use /proc/self/fd for extraction,
// the returned cleanup function is a no-op.
func WithProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
return withProcSelfFD(opts)
}
// Archiver implements the Archiver interface and allows the reuse of most utility functions of
// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
@@ -123,6 +137,8 @@ func breakoutError(err error) error {
return &breakoutErr{error: err}
}
func (e *breakoutErr) Unwrap() error { return e.error }
const (
AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts
OverlayWhiteoutFormat WhiteoutFormat = 1 // OverlayWhiteoutFormat formats whiteout according to the overlay standard.
@@ -437,6 +453,90 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
return nil
}
// resolveArchivePath resolves intermediate symlinks in name using chroot-like
// semantics when os.Root cannot traverse them. The final path component is
// intentionally preserved because archive extraction may create or replace it.
//
// This is a compatibility workaround rather than the preferred long-term
// implementation. It resolves the path separately before the actual operation,
// so a concurrent filesystem change may cause the operation to affect a
// different path within root. The subsequent os.Root operation still confines
// the operation to root and prevents such a change from escaping it.
//
// Paths with missing components are supported. Existing symlinks are resolved,
// and any remaining nonexistent components are retained for later creation.
//
// This helper should eventually be replaced by handle-relative resolution and
// operations with resolve-in-root semantics, avoiding the resolution/use race
// and repeated path traversal.
func resolveArchivePath(root *os.Root, name string) (string, error) {
parent, base := filepath.Split(name)
if parent == "" {
return name, nil
}
parent = filepath.Clean(parent)
// Follow the final parent component: it is an intermediate component of name,
// and an absolute symlink there must trigger the resolve-in-root fallback.
_, statErr := root.Stat(parent)
switch {
case statErr == nil:
return name, nil
case !os.IsNotExist(statErr) && !isPathEscapes(statErr):
return "", statErr
}
// Resolve the parent both to handle ENOENT from missing components or dangling
// symlinks, and to determine whether an os.Root breakout was caused by an
// absolute symlink. Relative symlink escapes preserve the original Stat error.
resolved, err := resolveFSRootPath(root.Name(), parent)
if err != nil {
return "", err
}
if isPathEscapes(statErr) && (!resolved.followedAbsoluteLink || resolved.relativeEscapeBeforeAbsolute) {
return "", statErr
}
relParent, err := filepath.Rel(root.Name(), resolved.path)
if err != nil {
return "", breakoutError(fmt.Errorf(
"could not make resolved parent %q relative to root %q: %w",
resolved.path,
root.Name(),
err,
))
}
if relParent != "." && !filepath.IsLocal(relParent) {
return "", breakoutError(fmt.Errorf(
"resolved parent %q escapes root %q",
resolved.path,
root.Name(),
))
}
return filepath.Join(relParent, base), nil
}
// resolveHardlinkTarget validates a POSIX hardlink target and resolves it to
// the native, root-relative filesystem path used for extraction.
func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) {
cleaned := path.Clean(linkname)
if strings.HasPrefix(cleaned, "/") {
// Some image builders (e.g. kaniko) write hardlink targets as absolute
// paths. Resolve those relative to the extraction root, with chroot-like
// semantics matching absolute symlink targets. Strip the root from the
// original linkname rather than the cleaned one so that ".." components
// are not collapsed against "/" but instead rejected below.
cleaned = path.Clean(strings.TrimLeft(linkname, "/"))
}
if cleaned == "." || !filepath.IsLocal(cleaned) {
return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname))
}
return resolveArchivePath(root, filepath.FromSlash(cleaned))
}
// createTarFile extracts a single tar entry into the given root. dstPath is the
// root-relative path of the entry being extracted, in native (host-separator)
// form so it can be passed directly to os.Root methods and fsRootPath.
@@ -445,6 +545,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
Lchown = true
inUserns, bestEffortXattrs bool
chownOpts *ChownOpts
internalOpts *archiveoptions.Options
)
// TODO(thaJeztah): make opts a required argument.
@@ -453,6 +554,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
chownOpts = opts.ChownOpts
bestEffortXattrs = opts.BestEffortXattrs
internalOpts = opts.internalOptions
}
// hdr.Mode is in linux format, which we can use for sycalls,
@@ -460,6 +562,15 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
hdrInfo := hdr.FileInfo()
var hardlinkTarget string
if hdr.Typeflag == tar.TypeLink {
var err error
hardlinkTarget, err = resolveHardlinkTarget(root, hdr.Linkname)
if err != nil {
return err
}
}
switch hdr.Typeflag {
case tar.TypeDir:
// Create directory unless it already exists as one; merge in that case.
@@ -509,13 +620,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// Defence in depth: root.Link's containment is limited when
// dest is a volume root.
linkname := path.Clean(hdr.Linkname)
if linkname == "." || !filepath.IsLocal(linkname) {
return breakoutError(fmt.Errorf("invalid hardlink target %q", hdr.Linkname))
}
if err := root.Link(filepath.FromSlash(linkname), dstPath); err != nil {
if err := root.Link(hardlinkTarget, dstPath); err != nil {
return err
}
@@ -591,7 +696,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// There is no LChmod, so ignore mode for symlink. Also, this
// must happen after chown, as that can modify the file mode
if err := handleLChmod(root, dstPath, hdr, hdrInfo); err != nil {
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
return err
}
@@ -606,15 +711,15 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// Follow the hardlink only when its target is not itself a symlink.
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
fi, err := root.Lstat(hardlinkTarget)
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
return err
}
}
default:
// All other file types follow symlinks.
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
return err
}
}
@@ -696,13 +801,13 @@ func (t *Tarballer) Do() {
defer func() {
// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil {
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
}
if err := t.compressWriter.Close(); err != nil {
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
}
if err := t.pipeWriter.Close(); err != nil {
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
}
}()
@@ -929,7 +1034,10 @@ loop:
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// hdr.Name stays POSIX (forward-slash) for logical string checks.
dstPath := filepath.FromSlash(hdr.Name)
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return err
}
// If dstPath exists we almost always just want to remove and replace it.
// The only exception is when it is a directory *and* the file from
@@ -967,7 +1075,7 @@ loop:
//
// This must be done before whiteoutConverter.ConvertRead, which
// may set xattrs on the directory or create whiteout files.
if err := createImpliedDirectories(root, hdr, options); err != nil {
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return err
}
@@ -994,7 +1102,7 @@ loop:
for _, d := range dirs {
aTime := boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime))
if err := root.Chtimes(d.name, aTime, boundTime(d.hdr.ModTime)); err != nil {
if err := chtimes(root, d.name, aTime, boundTime(d.hdr.ModTime)); err != nil {
return err
}
}
@@ -1022,81 +1130,80 @@ func unrepresentableOnWindows(hdr *tar.Header) error {
return nil
}
// createImpliedDirectories will create all parent directories of the current path with default permissions, if they do
// not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is
// defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus
// we most both create them and choose metadata like permissions.
// createImpliedDirectories creates all parent directories of dstPath with
// default permissions if they do not already exist. This is necessary because
// the tar format permits implicit directories whose existence is defined only
// by file paths, without corresponding directory headers from which metadata
// could be restored.
//
// The caller must have normalized hdr.Name (no leading ".." components).
// All directory creation is performed via root so it is bounded within the
// destination at the OS level (openat(2) semantics), preventing escape via
// symlinks in the destination tree.
func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error {
// For non-directory entries, ensure that the parent directory exists.
if hdr.Typeflag != tar.TypeDir {
parent := filepath.FromSlash(path.Dir(strings.TrimSuffix(hdr.Name, "/")))
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
// The caller must pass a normalized, root-relative local path. Any archive-path
// conversion and resolve-in-root handling must already have been applied.
// Directory creation is performed through root, so it remains confined to the
// extraction destination even if the destination tree changes concurrently.
func createImpliedDirectories(root *os.Root, dstPath string, options *TarOptions) error {
parent := filepath.Dir(dstPath)
// Similar to [user.MkdirAllAndChown]
//
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
var cur string
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
if c == "" {
continue
}
cur = filepath.Join(cur, c)
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
fi, err := root.Stat(cur)
if err != nil {
return err
}
if fi.IsDir() {
continue
}
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
// Similar to [user.MkdirAllAndChown]
//
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
var cur string
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
if c == "" {
continue
}
cur = filepath.Join(cur, c)
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
fi, err := root.Stat(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
if fi.IsDir() {
continue
}
// root.Mkdir applies the mode subject to the process umask, so
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
// independent of umask, matching the previous MkdirAllAndChown
// behavior.
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
if err := dir.Close(); err != nil {
return err
}
}
// root.Mkdir applies the mode subject to the process umask, so
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
// independent of umask, matching the previous MkdirAllAndChown
// behavior.
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
_ = dir.Close()
return err
}
if err := dir.Close(); err != nil {
return err
}
}
+18
View File
@@ -8,10 +8,28 @@ import (
"path/filepath"
"strings"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/sys/userns"
"golang.org/x/sys/unix"
)
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
procSelfFD, err := os.Open("/proc/self/fd")
if err != nil {
return nil, nil, err
}
var prepared TarOptions
if opts != nil {
prepared = *opts
}
prepared.internalOptions = &archiveoptions.Options{
ProcSelfFD: procSelfFD,
}
return &prepared, func() { _ = procSelfFD.Close() }, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
return newOverlayWhiteoutConverter()
+8
View File
@@ -2,6 +2,14 @@
package archive
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
var prepared TarOptions
if opts != nil {
prepared = *opts
}
return &prepared, func() {}, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
return nil
}
+7 -19
View File
@@ -8,11 +8,11 @@ import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
@@ -88,7 +88,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string)
// handleLChmod applies the mode from hdrInfo to dstPath within root, skipping
// symlinks (there is no lchmod). For hardlinks, the mode is applied only when
// the link target is itself not a symlink.
func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.FileInfo) error {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts *archiveoptions.Options) error {
switch hdr.Typeflag {
case tar.TypeSymlink:
return nil
@@ -96,21 +96,21 @@ func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.Fil
case tar.TypeLink:
// If the target is a symlink, there is no way to chmod the hardlink
// without following it.
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
fi, err := root.Lstat(hardlinkTarget)
if err != nil || fi.Mode()&os.ModeSymlink != 0 {
return nil
}
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
default:
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
}
}
// chmodNoSymlink applies mode to a non-symlink entry.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode, opts *archiveoptions.Options) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
if err != nil {
return err
@@ -127,19 +127,7 @@ func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
}
// Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW.
// Open the entry without following symlinks and apply the mode through the
// resulting file descriptor.
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
fd, err := unix.Openat(int(parent.Fd()), base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, perm); err != nil {
return &os.PathError{Op: "fchmod", Path: name, Err: err}
}
return nil
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm, opts) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
}
// fileModeToPerm returns the subset of an os.FileMode that can be applied
+1 -1
View File
@@ -53,7 +53,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) err
}
// handleLChmod is a no-op on Windows because chmod is not supported.
func handleLChmod(root *os.Root, path string, hdr *tar.Header, hdrInfo os.FileInfo) error {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts any) error {
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package archive
import (
"fmt"
"os"
"runtime"
"strconv"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, opts *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if opts != nil && opts.ProcSelfFD != nil {
err := unix.Fchmodat(int(opts.ProcSelfFD.Fd()), strconv.Itoa(fd), perm, 0)
// Keep the os.File alive until fchmodat has finished using its descriptor.
runtime.KeepAlive(opts.ProcSelfFD)
if err != nil {
return &os.PathError{
Op: "fchmodat",
Path: name,
Err: fmt.Errorf("via pre-opened /proc/self/fd/%d: %w", fd, err),
}
}
} else {
procPath := "/proc/self/fd/" + strconv.Itoa(fd)
if err := unix.Chmod(procPath, perm); err != nil {
return &os.PathError{
Op: "chmod",
Path: name,
Err: fmt.Errorf("via %s: %w", procPath, err),
}
}
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build !linux && !windows
package archive
import (
"os"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, _ *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, perm); err != nil {
return &os.PathError{Op: "fchmod", Path: name, Err: err}
}
return nil
}
+33 -12
View File
@@ -316,19 +316,40 @@ func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir
}
}
// newNameRebaser returns a function that replaces oldBase with newBase at the
// beginning of POSIX-style archive entry names. It converts oldBase and newBase
// to forward-slash form and trims trailing slashes.
//
// When rebasing from the archive root, the returned function removes all
// leading slashes from names. It otherwise preserves the remainder verbatim
// and does not clean or canonicalize paths.
func newNameRebaser(oldBase, newBase string) func(string) string {
oldBase = strings.TrimRight(filepath.ToSlash(oldBase), "/")
newBase = strings.TrimRight(filepath.ToSlash(newBase), "/")
if oldBase == "" {
return func(name string) string {
name = strings.TrimLeft(name, "/")
if newBase == "" {
return name
}
return newBase + "/" + name
}
}
return func(name string) string {
suffix, ok := strings.CutPrefix(name, oldBase)
if !ok || suffix != "" && !strings.HasPrefix(suffix, "/") {
return name
}
return newBase + suffix
}
}
// RebaseArchiveEntries rewrites the given srcContent archive replacing
// an occurrence of oldBase with newBase at the beginning of entry names.
func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.ReadCloser {
oldBase = filepath.ToSlash(oldBase)
newBase = filepath.ToSlash(newBase)
if oldBase == "/" {
// If oldBase specifies the root directory, use an empty string as
// oldBase instead so that newBase doesn't replace the path separator
// that all paths will start with.
oldBase = ""
}
rebase := newNameRebaser(oldBase, newBase)
rebased, w := io.Pipe()
go func() {
@@ -356,9 +377,9 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read
//
// To fix, set the format to PAX here. See docker/for-linux issue #484.
hdr.Format = tar.FormatPAX
hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
hdr.Name = rebase(hdr.Name)
if hdr.Typeflag == tar.TypeLink {
hdr.Linkname = strings.Replace(hdr.Linkname, oldBase, newBase, 1)
hdr.Linkname = rebase(hdr.Linkname)
}
if err = rebasedTar.WriteHeader(hdr); err != nil {
+19 -19
View File
@@ -29,8 +29,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
tr := tar.NewReader(layer)
var dirs []unpackedDir
// unpackedPaths tracks root-relative paths already written in this layer
// so that the AUFS opaque-whiteout walk knows which paths to preserve.
// unpackedPaths tracks resolved, native-separator, root-relative paths
// already written in this layer so that the AUFS opaque-whiteout walk
// knows which paths to preserve.
unpackedPaths := make(map[string]struct{})
if options == nil {
@@ -71,12 +72,6 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
continue
}
// Ensure that the parent directory exists.
err = createImpliedDirectories(root, hdr, options)
if err != nil {
return 0, err
}
// Skip AUFS metadata dirs
if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) {
// Regular files inside /.wh..wh.plnk can be used as hardlink targets
@@ -109,10 +104,15 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// The tar-header name (hdr.Name) is POSIX, so convert it here.
dstPath := filepath.FromSlash(hdr.Name)
base := filepath.Base(dstPath)
if strings.HasPrefix(base, WhiteoutPrefix) {
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return 0, err
}
// Ensure that the parent directory exists.
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return 0, err
}
if base := filepath.Base(dstPath); strings.HasPrefix(base, WhiteoutPrefix) {
dir := filepath.Dir(dstPath)
if base == WhiteoutOpaqueDir {
_, err := root.Lstat(dir)
@@ -144,9 +144,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
return err
}
// unpackedPaths is keyed by root-relative slash paths; convert
// filepath.WalkDir's native path before looking it up.
if _, exists := unpackedPaths[filepath.ToSlash(rel)]; !exists {
// unpackedPaths is keyed by resolved, native-separator,
// root-relative paths, matching filepath.WalkDir's paths.
if _, exists := unpackedPaths[rel]; !exists {
return root.RemoveAll(rel)
}
return nil
@@ -206,14 +206,14 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
}
// unpackedPaths is keyed by the POSIX (forward-slash) name so it
// matches the ToSlash'd lookup in the opaque-whiteout walk above.
unpackedPaths[hdr.Name] = struct{}{}
// Record the resolved, native-separator, root-relative path so it
// matches the paths produced by the opaque-whiteout walk.
unpackedPaths[dstPath] = struct{}{}
}
}
for _, d := range dirs {
if err := root.Chtimes(d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil {
if err := chtimes(root, d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil {
return 0, err
}
}
+12
View File
@@ -0,0 +1,12 @@
// Package archiveoptions defines internal options shared between archive and
// chrootarchive.
package archiveoptions
import "os"
// Options contains extraction resources supplied by internal callers.
type Options struct {
// ProcSelfFD references /proc/self/fd as opened before entering a chroot.
// The caller retains ownership of the file.
ProcSelfFD *os.File
}
+45 -15
View File
@@ -24,31 +24,47 @@ import (
var errTooManyLinks = errors.New("too many links")
type fsRootPathResult struct {
path string
followedAbsoluteLink bool
relativeEscapeBeforeAbsolute bool
}
// fsRootPath joins a path with a root, evaluating and bounding any
// symlink to the root directory.
func fsRootPath(root, path string) (string, error) {
result, err := resolveFSRootPath(root, path)
if err != nil {
return "", err
}
return result.path, nil
}
func resolveFSRootPath(root, path string) (fsRootPathResult, error) {
result := fsRootPathResult{path: root}
if path == "" {
return root, nil
return result, nil
}
var linksWalked int // to protect against cycles
for {
i := linksWalked
newpath, err := walkLinks(root, path, &linksWalked)
newpath, err := walkLinks(root, path, &linksWalked, &result)
if err != nil {
return "", err
return fsRootPathResult{}, err
}
path = newpath
if i == linksWalked {
newpath = filepath.Join(string(os.PathSeparator), newpath)
if path == newpath {
return filepath.Join(root, newpath), nil
result.path = filepath.Join(root, newpath)
return result, nil
}
path = newpath
}
}
}
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (newpath string, islink bool, err error) {
if *linksWalked > 255 {
return "", false, errTooManyLinks
}
@@ -74,37 +90,51 @@ func walkLink(root, path string, linksWalked *int) (newpath string, islink bool,
if err != nil {
return "", false, err
}
if filepath.IsAbs(newpath) {
result.followedAbsoluteLink = true
} else if !result.followedAbsoluteLink {
// Record an escape before a later absolute link can make the original
// os.Root error appear eligible for resolve-in-root fallback.
relativeDir, err := filepath.Rel(string(os.PathSeparator), filepath.Dir(path))
if err != nil {
return "", false, err
}
resolved := filepath.Join(relativeDir, newpath)
if resolved != "." && !filepath.IsLocal(resolved) {
result.relativeEscapeBeforeAbsolute = true
}
}
*linksWalked++
return newpath, true, nil
}
func walkLinks(root, path string, linksWalked *int) (string, error) {
func walkLinks(root, path string, linksWalked *int, result *fsRootPathResult) (string, error) {
switch dir, file := filepath.Split(path); {
case dir == "":
newpath, _, err := walkLink(root, file, linksWalked)
newpath, _, err := walkLink(root, file, linksWalked, result)
return newpath, err
case file == "":
if os.IsPathSeparator(dir[len(dir)-1]) {
if dir == string(os.PathSeparator) {
return dir, nil
}
return walkLinks(root, dir[:len(dir)-1], linksWalked)
return walkLinks(root, dir[:len(dir)-1], linksWalked, result)
}
newpath, _, err := walkLink(root, dir, linksWalked)
newpath, _, err := walkLink(root, dir, linksWalked, result)
return newpath, err
default:
newdir, err := walkLinks(root, dir, linksWalked)
newdir, err := walkLinks(root, dir, linksWalked, result)
if err != nil {
return "", err
}
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result)
if err != nil {
return "", err
}
if !islink {
return newpath, nil
}
if filepath.IsAbs(newpath) {
if !islink || filepath.IsAbs(newpath) {
return newpath, nil
}
return filepath.Join(newdir, newpath), nil
+4
View File
@@ -22,6 +22,10 @@ func init() {
}
}
// boundTime returns t if it falls within the range supported by os.Chtimes.
// Times before the Unix epoch (minTime) or after the end of Unix time
// (maxTime) are replaced with minTime, as os.Chtimes has undefined behavior
// outside that range.
func boundTime(t time.Time) time.Time {
if t.Before(minTime) || t.After(maxTime) {
return minTime
+18 -17
View File
@@ -14,23 +14,13 @@ import (
"golang.org/x/sys/unix"
)
// chtimes changes the access time and modified time of a file at the given path.
// If the modified time is prior to the Unix Epoch (unixMinTime), or after the
// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior. In this
// case, Chtimes defaults to Unix Epoch, just in case.
func chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(name, atime, mtime)
}
func timeToTimespec(time time.Time) unix.Timespec {
if time.IsZero() {
// Return UTIME_OMIT special value
return unix.Timespec{
Sec: 0,
Nsec: (1 << 30) - 2,
}
}
return unix.NsecToTimespec(time.UnixNano())
// chtimes changes the access and modification time of a file at the given
// path relative to root.
//
// Callers must use boundTime to ensure timestamps are within the range
// supported by os.Chtimes.
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
return root.Chtimes(name, atime, mtime)
}
func lchtimes(root *os.Root, name string, atime, mtime time.Time) error {
@@ -63,3 +53,14 @@ func lchtimes(root *os.Root, name string, atime, mtime time.Time) error {
}
return nil
}
func timeToTimespec(time time.Time) unix.Timespec {
if time.IsZero() {
// Return UTIME_OMIT special value
return unix.Timespec{
Sec: 0,
Nsec: (1 << 30) - 2,
}
}
return unix.NsecToTimespec(time.UnixNano())
}
+94 -15
View File
@@ -1,32 +1,111 @@
package archive
import (
"errors"
"os"
"path/filepath"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
func chtimes(name string, atime time.Time, mtime time.Time) error {
if err := os.Chtimes(name, atime, mtime); err != nil {
// chtimes changes the access and modification time of a file at the given
// path relative to root.
//
// Symlink entries are handled separately through lchtimes. The final path
// component is expected not to be a reparse point; if one is encountered,
// chtimes returns an error.
//
// Callers must use boundTime to ensure timestamps are within the range
// supported by os.Chtimes.
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
if err != nil {
return err
}
defer parent.Close()
pathp, err := windows.UTF16PtrFromString(name)
if err != nil {
return err
}
h, err := windows.CreateFile(pathp,
windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil,
windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return err
}
defer windows.Close(h)
c := windows.NsecToFiletime(mtime.UnixNano())
return windows.SetFileTime(h, &c, nil, nil)
// Symlink entries are handled by lchtimes. The destination for all
// chtimes callers is therefore expected not to be a reparse point.
//
// Do not follow the final component: if it was concurrently replaced
// with a reparse point, fail instead of updating its target.
return chtimesAt(parent, filepath.Base(name), atime, mtime, true)
}
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
return nil
}
func chtimesAt(parent *os.File, name string, atime, mtime time.Time, noFollow bool) error {
h, err := openForWriteAttributesAt(windows.Handle(parent.Fd()), name, noFollow)
if err != nil {
if noFollow && errors.Is(err, windows.STATUS_REPARSE_POINT_ENCOUNTERED) {
// Encountering a reparse point when noFollow is requested is unexpected.
// Treat it as a potential breakout to fail extraction safely.
return breakoutError(err)
}
return err
}
defer func() { _ = windows.Close(h) }()
var (
creationTime = windows.NsecToFiletime(mtime.UnixNano())
accessTime = windows.NsecToFiletime(atime.UnixNano())
modificationTime = windows.NsecToFiletime(mtime.UnixNano())
)
return windows.SetFileTime(h, &creationTime, &accessTime, &modificationTime)
}
// openForWriteAttributesAt opens name relative to parent with permission to
// modify its file attributes. If noFollow is true, it does not follow reparse
// points.
//
// This implementation is based on Go's internal Windows Openat support:
//
// https://github.com/golang/go/blob/go1.26.0/src/internal/syscall/windows/at_windows.go
//
// It is used by os.Root's Windows implementation for root-relative filesystem
// operations:
//
// https://github.com/golang/go/blob/go1.26.0/src/os/root_windows.go
//
// Keep this implementation aligned with the upstream code until an equivalent
// operation is available from golang.org/x/sys/windows.
func openForWriteAttributesAt(parent windows.Handle, name string, noFollow bool) (windows.Handle, error) {
name16, err := windows.UTF16FromString(name)
if err != nil {
return windows.InvalidHandle, err
}
attrs := uint32(windows.OBJ_CASE_INSENSITIVE)
if noFollow {
attrs |= windows.OBJ_DONT_REPARSE
}
var handle windows.Handle
err = windows.NtCreateFile(
&handle,
windows.SYNCHRONIZE|windows.FILE_WRITE_ATTRIBUTES,
&windows.OBJECT_ATTRIBUTES{
Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})),
RootDirectory: parent,
ObjectName: &windows.NTUnicodeString{
Length: uint16((len(name16) - 1) * 2), // #nosec G115 -- Length is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
MaximumLength: uint16(len(name16) * 2), // #nosec G115 -- MaximumLength is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
Buffer: &name16[0],
},
Attributes: attrs,
},
&windows.IO_STATUS_BLOCK{},
nil,
windows.FILE_ATTRIBUTE_NORMAL,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
windows.FILE_OPEN,
windows.FILE_OPEN_FOR_BACKUP_INTENT|windows.FILE_SYNCHRONOUS_IO_NONALERT,
0, // EA buffer
0, // EA length
)
return handle, err
}
+10 -38
View File
@@ -1,12 +1,9 @@
version: "2"
run:
tests: false
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errchkjson
@@ -22,46 +19,21 @@ linters:
- nilerr
- nilnesserr
- noctx
- protogetter
- reassign
- recvcheck
- rowserrcheck
- spancheck
- sqlclosecheck
- testifylint
- unparam
- zerologlint
disable:
- prealloc
settings:
errcheck:
check-type-assertions: false
check-blank: false
lll:
line-length: 100
tab-width: 4
prealloc:
simple: false
range-loops: false
for-loops: false
whitespace:
multi-if: false
multi-func: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gosec
- musttag
- noctx # TODO: enable once we switch to Go 1.24+.
- linters: # TODO: remove once golangci-lint is updated with https://github.com/golangci/golangci-lint/pull/6584
- gocheckcompilerdirectives
text: 'compiler directive unrecognized: //go:fix'
-15
View File
@@ -1,15 +0,0 @@
language: go
go_import_path: github.com/sirupsen/logrus
git:
depth: 1
env:
- GO111MODULE=on
go: 1.15.x
os: linux
install:
- ./travis/install.sh
script:
- cd ci
- go run mage.go -v -w ../ crossBuild
- go run mage.go -v -w ../ lint
- go run mage.go -v -w ../ test
+201 -38
View File
@@ -1,90 +1,246 @@
# 1.8.1
# Changelog
All notable changes to this project will be documented in this file.
## 1.10.1
Fixes:
* Fix a regression introduced in v1.10.0 where `TextFormatter` could panic
when formatting nil or panicking `error` and `fmt.Stringer` values.
* Allow function-backed implementations of `error` as field values.
## 1.10.0
Fixes:
* Fix reentrant logging deadlocks in formatter paths.
* Fix race conditions in formatter and entry handling.
* Fix generic `Log`, `Logf`, `Logln`, and `LogFn` methods unexpectedly
panicking when called with `PanicLevel`. Use the corresponding `Panic`
methods when panic behavior is desired.
* Improve concurrency safety around formatter and hook access.
Features:
* Add `slog` hook for forwarding Logrus entries to `log/slog`.
* Add `slog.Handler` for forwarding `log/slog` records to a Logrus logger,
including levels, fields, groups, context, time, and optional caller
reporting. The hook and handler can also be combined to help migrate
between Logrus and `log/slog`.
* Add minimal, composable logging interfaces for each log level. This enables
consumers to depend on narrower interfaces, making it easier to substitute
or adapt logging implementations.
* Allow `Entry.Caller` to be set explicitly and preserve it across derived
entries, enabling custom caller detection without Logrus overwriting
caller information when `ReportCaller` is enabled.
Changed:
* Raise minimum supported Go version to 1.23.
* TextFormatter now renders `[]byte` values as raw/quoted strings instead of slice-of-ints.
* TextFormatter now uses distinct dimmed colors for debug and trace output.
* TextFormatter now automatically enables colors on Windows terminals with ANSI support,
matching the behavior on other platforms.
* `Entry.HasCaller` is now deprecated in favor of checking `Entry.Caller` directly.
* Deprecated `MutexWrap`, which was unintentionally exposed as public API.
It remains available as an alias for compatibility but should not be used
directly.
Performance:
* Significantly improve TextFormatter performance and reduce allocations.
* Optimize common Entry and Logger hot paths.
* Reduce allocations in caller reporting.
* ~17% lower geomean runtime and ~27% higher formatter throughput overall.
* Common enabled logging paths are ~3044% faster.
* TextFormatter paths are up to ~40% faster, with allocation counts reduced
by 2574% across the measured formatter cases.
## 1.9.4
Fixes:
* Remove uses of deprecated `ioutil` package
Features:
* Add GNU/Hurd support
* Add WASI wasip1 support
Code quality:
* Update minimum supported Go version to 1.17
* Documentation updates
## 1.9.3
Fixes:
* Re-apply fix for potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
* Fix panic in Writer
## 1.9.2
Fixes:
* Revert Writer DoS fix (#1376) due to regression
## 1.9.1
Fixes:
* Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
## 1.9.0
Fixes:
* Multiple concurrency and race condition fixes
* Improve Windows terminal and ANSI handling
Code quality:
* Internal cleanups and modernization
## 1.8.3
Fixes:
* Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376)
## 1.8.2
Features:
* Add support for the logger private buffer pool (#1253)
Fixes:
* Fix race condition for SetFormatter and SetReportCaller
* Fix data race in hooks test package
## 1.8.1
Code quality:
* move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
* improve timestamp format documentation
Fixes:
* fix race condition on logger hooks
# 1.8.0
## 1.8.0
Correct versioning number replacing v1.7.1.
# 1.7.1
## 1.7.1
Beware this release has introduced a new public API and its semver is therefore incorrect.
Code quality:
* use go 1.15 in travis
* use magefile as task runner
Fixes:
* small fixes about new go 1.13 error formatting system
* Fix for long time race condiction with mutating data hooks
Features:
* build support for zos
# 1.7.0
## 1.7.0
Fixes:
* the dependency toward a windows terminal library has been removed
Features:
* a new buffer pool management API has been added
* a set of `<LogLevel>Fn()` functions have been added
# 1.6.0
## 1.6.0
Fixes:
* end of line cleanup
* revert the entry concurrency bug fix which leads to deadlock under some circumstances
* update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
Features:
* add an option to the `TextFormatter` to completely disable fields quoting
# 1.5.0
## 1.5.0
Code quality:
* add golangci linter run on travis
Fixes:
* add mutex for hooks concurrent access on `Entry` data
* caller function field for go1.14
* fix build issue for gopherjs target
Feature:
* add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
* add a `DisableHTMLEscape` option in the `JSONFormatter`
* add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
# 1.4.2
## 1.4.2
* Fixes build break for plan9, nacl, solaris
# 1.4.1
## 1.4.1
This new release introduces:
* Enhance TextFormatter to not print caller information when they are empty (#944)
* Remove dependency on golang.org/x/crypto (#932, #943)
Fixes:
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
# 1.4.0
## 1.4.0
This new release introduces:
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
Fixes:
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
* Fix infinite recursion on unknown `Level.String()` (#907)
* Fix race condition in `getCaller` (#916).
# 1.3.0
## 1.3.0
This new release introduces:
* Log, Logf, Logln functions for Logger and Entry that take a Level
Fixes:
* Building prometheus node_exporter on AIX (#840)
* Race condition in TextFormatter (#468)
* Travis CI import path (#868)
@@ -92,20 +248,26 @@ Fixes:
* Pointer to func as field in JSONFormatter (#870)
* Properly marshal Levels (#873)
# 1.2.0
## 1.2.0
This new release introduces:
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
* A new trace level named `Trace` whose level is below `Debug`
* A configurable exit function to be called upon a Fatal trace
* The `Level` object now implements `encoding.TextUnmarshaler` interface
# 1.1.1
## 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
## 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
@@ -122,9 +284,10 @@ This new release introduces:
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
## 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
@@ -134,71 +297,71 @@ This new release introduces:
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5
## 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
## 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3
## 1.0.3
* Replace example files with testable examples
# 1.0.2
## 1.0.2
* bug: quote non-string values in text formatter (#583)
* Make (*Logger) SetLevel a public method
# 1.0.1
## 1.0.1
* bug: fix escaping in text formatter (#575)
# 1.0.0
## 1.0.0
* Officially changed name to lower-case
* bug: colors on Windows 10 (#541)
* bug: fix race in accessing level (#512)
# 0.11.5
## 0.11.5
* feature: add writer and writerlevel to entry (#372)
# 0.11.4
## 0.11.4
* bug: fix undefined variable on solaris (#493)
# 0.11.3
## 0.11.3
* formatter: configure quoting of empty values (#484)
* formatter: configure quoting character (default is `"`) (#484)
* bug: fix not importing io correctly in non-linux environments (#481)
# 0.11.2
## 0.11.2
* bug: fix windows terminal detection (#476)
# 0.11.1
## 0.11.1
* bug: fix tty detection with custom out (#471)
# 0.11.0
## 0.11.0
* performance: Use bufferpool to allocate (#370)
* terminal: terminal detection for app-engine (#343)
* feature: exit handler (#375)
# 0.10.0
## 0.10.0
* feature: Add a test hook (#180)
* feature: `ParseLevel` is now case-insensitive (#326)
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
* performance: avoid re-allocations on `WithFields` (#335)
# 0.9.0
## 0.9.0
* logrus/text_formatter: don't emit empty msg
* logrus/hooks/airbrake: move out of main repository
@@ -210,25 +373,25 @@ This new release introduces:
* logrus/core: support `WithError` on logger
* logrus/core: Solaris support
# 0.8.7
## 0.8.7
* logrus/core: fix possible race (#216)
* logrus/doc: small typo fixes and doc improvements
# 0.8.6
## 0.8.6
* hooks/raven: allow passing an initialized client
# 0.8.5
## 0.8.5
* logrus/core: revert #208
# 0.8.4
## 0.8.4
* formatter/text: fix data race (#218)
# 0.8.3
## 0.8.3
* logrus/core: fix entry log level (#208)
* logrus/core: improve performance of text formatter by 40%
@@ -236,24 +399,24 @@ This new release introduces:
* logrus/core: add support for DragonflyBSD and NetBSD
* formatter/text: print structs more verbosely
# 0.8.2
## 0.8.2
* logrus: fix more Fatal family functions
# 0.8.1
## 0.8.1
* logrus: fix not exiting on `Fatalf` and `Fatalln`
# 0.8.0
## 0.8.0
* logrus: defaults to stderr instead of stdout
* hooks/sentry: add special field for `*http.Request`
* formatter/text: ignore Windows for colors
# 0.7.3
## 0.7.3
* formatter/\*: allow configuration of timestamp layout
# 0.7.2
## 0.7.2
* formatter/text: Add configuration option for time format (#158)
+39 -53
View File
@@ -3,13 +3,10 @@
Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger.
**Logrus is in maintenance-mode.** We will not be introducing new features. It's
simply too hard to do in a way that won't break many people's projects, which is
the last thing you want from your Logging library (again...).
This does not mean Logrus is dead. Logrus will continue to be maintained for
security, (backwards compatible) bug fixes, and performance (where we are
limited by the interface).
**Logrus is in maintenance mode.** The project focuses on security, bug fixes,
and performance improvements. New features are not planned, aside from changes
required to provide interoperability with other logging ecosystems (e.g., Go's
[log/slog](https://pkg.go.dev/log/slog)).
I believe Logrus' biggest contribution is to have played a part in today's
widespread use of structured logging in Golang. There doesn't seem to be a
@@ -23,18 +20,6 @@ about structured logging in Go today. Check out, for example,
[zap]: https://github.com/uber-go/zap
[apex]: https://github.com/apex/log
**Seeing weird case-sensitive problems?** It's in the past been possible to
import Logrus as both upper- and lower-case. Due to the Go package environment,
this caused issues in the community and we needed a standard. Some environments
experienced problems with the upper-case variant, so the lower-case was decided.
Everything using `logrus` will need to use the lower-case:
`github.com/sirupsen/logrus`. Any package that isn't, should be changed.
To fix Glide, see [these
comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437).
For an in-depth explanation of the casing issue, see [this
comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276).
Nicely color-coded in development (when a TTY is attached, otherwise just
plain text):
@@ -43,35 +28,27 @@ plain text):
With `logrus.SetFormatter(&logrus.JSONFormatter{})`, for easy parsing by logstash
or Splunk:
```text
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
{"level":"warning","msg":"The group's number increased tremendously!",
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```json lines
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!","size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.","size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```
With the default `logrus.SetFormatter(&logrus.TextFormatter{})` when a TTY is not
attached, the output is compatible with the
[logfmt](https://pkg.go.dev/github.com/kr/logfmt) format:
```text
```bash
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" animal=orca err="It's over 9000!" number=100 omg=true size=9009
```
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
```go
@@ -88,29 +65,32 @@ If you wish to add the calling method as a field, instruct the logger via:
```go
logrus.SetReportCaller(true)
```
This adds the caller as 'method' like so:
```json
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by","time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
```
```text
```bash
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
```
Note that this does add measurable overhead - the cost will depend on the version of Go, but is
between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
environment via benchmarks:
```bash
go test -bench=.*CallerTracing
go test -bench=ReportCaller
```
#### Case-sensitivity
The organization's name was changed to lower-case--and this will not be changed
back. If you are getting import conflicts due to case sensitivity, please use
the lower-case import: `github.com/sirupsen/logrus`.
The organization's name was [changed to lower-case][1]. If you are getting import
conflicts due to case sensitivity, please use the lower-case import:
`github.com/sirupsen/logrus`.
[1]: https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276
#### Example
@@ -289,6 +269,7 @@ func init() {
}
}
```
Note: Syslog hooks also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
@@ -367,18 +348,21 @@ Splunk or Logstash.
The built-in logging formatters are:
* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
without colors.
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
* [`logrus.TextFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter)
logs the event in colors if the logger output is a TTY, otherwise without colors.
* To force colored output when there is no TTY, set the `ForceColors`
field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
`DisableColors` field to `true`.
* On modern Windows terminals with ANSI (Virtual Terminal) support, TextFormatter
automatically enables colored output.
* If your environment does not support ANSI escape sequences, wrap the logger output
using [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable)
and set `ForceColors` (or `CLICOLOR_FORCE=1`) to enable colors through the wrapper.
* When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`.
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
* All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter).
* [`logrus.JSONFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter)
logs fields as JSON.
Third-party logging formatters:
@@ -390,10 +374,12 @@ Third-party logging formatters:
* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Save log to files.
* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
* [`easy-logrus-formatter`](https://github.com/WeiZhixiong/easy-logrus-formatter). Provide a user-friendly formatter for logrus.
* [`redactrus`](https://github.com/ibreakthecloud/redactrus). Redacts sensitive information like password, apikeys, email, etc. from logs.
You can define your formatter by implementing the `Formatter` interface,
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
`Fields` type (`map[string]interface{}`) with all your fields as well as the
`Fields` type (`map[string]any`) with all your fields as well as the
default ones (see Entries section above):
```go
@@ -516,4 +502,4 @@ Situations when locking is not needed include:
2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing)
(Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)
(Refer to <http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/>)
+2 -2
View File
@@ -57,7 +57,7 @@ func Exit(code int) {
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing database connections, or sending an alert that the application is
// closing.
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
@@ -69,7 +69,7 @@ func RegisterExitHandler(handler func()) {
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing database connections, or sending an alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
+8 -14
View File
@@ -5,9 +5,13 @@ import (
"sync"
)
var (
bufferPool BufferPool
)
var bufferPool BufferPool = &defaultPool{
pool: &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
type BufferPool interface {
Put(*bytes.Buffer)
@@ -27,17 +31,7 @@ func (p *defaultPool) Get() *bytes.Buffer {
}
// SetBufferPool allows to replace the default logrus buffer pool
// to better meets the specific needs of an application.
// to better meet the specific needs of an application.
func SetBufferPool(bp BufferPool) {
bufferPool = bp
}
func init() {
SetBufferPool(&defaultPool{
pool: &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
},
})
}
+13 -13
View File
@@ -1,25 +1,25 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
package main
import (
log "github.com/sirupsen/logrus"
)
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/sirupsen/logrus
*/
+268 -146
View File
@@ -4,9 +4,11 @@ import (
"bytes"
"context"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
@@ -17,8 +19,10 @@ var (
// qualified package name, cached at first use
logrusPackage string
// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// Positions in the call stack when tracing to report the calling method.
//
// Start at the bottom of the stack before the package-name cache is primed.
minimumCallerDepth = 1
// Used for caller information initialisation
callerInitOnce sync.Once
@@ -29,68 +33,116 @@ const (
knownLogrusFrames int = 4
)
func init() {
// start at the bottom of the stack before the package-name cache is primed
minimumCallerDepth = 1
}
// ErrorKey defines the key when adding errors using [WithError], [Logger.WithError].
var ErrorKey = "error"
// Entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// reused and passed around as much as you wish to avoid field duplication.
// Entry represents a single log event. It may be either an intermediate
// entry (created via WithField(s), WithContext, etc.) or a final entry
// that is emitted when one of the level methods (Trace, Debug, Info,
// Warn, Error, Fatal, Panic) is called.
//
//nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver.
// An Entry always belongs to a Logger. A nil Logger is invalid and will
// cause a panic when the entry is logged. Use [NewEntry] or Logger methods
// to construct entries.
//
// Entries are safe to reuse for adding fields and may be passed around
// to avoid field duplication. Each log operation operates on a copy
// of the Entrys data to avoid mutation during formatting.
//
//nolint:recvcheck // Entry methods intentionally use both pointer and value receivers.
type Entry struct {
// Logger is the Logger that owns this entry and is responsible for
// formatting, hooks, and output. It must not be nil. An Entry without
// a Logger is invalid and will panic when logged.
Logger *Logger
// Contains all the fields set by the user.
// Data contains all user-defined fields attached to this entry.
Data Fields
// Time at which the log entry was created
// Time is the timestamp for the log event. If zero when the entry is
// logged, it defaults to the current time.
Time time.Time
// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
// Level is the severity of the log entry. It is set when the entry
// is fired and reflects the level used for that log call.
Level Level
// Calling method, with package name
// Caller contains the calling method information.
//
// When [Logger.ReportCaller] is enabled, Caller is populated automatically at
// log time if it is nil. Hooks and formatters may inspect Caller.
//
// Applications generally should not modify Caller unless they intentionally
// want to provide custom caller information.
Caller *runtime.Frame
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
// Message is the log message supplied to one of the logging methods
// (Trace, Debug, Info, Warn, Error, Fatal, or Panic). It is set when
// the entry is logged.
Message string
// When formatter is called in entry.log(), a Buffer may be set to entry
// Buffer is a reusable buffer provided to the formatter. It is set
// before formatting in the normal log path; when nil, formatters
// allocate their own.
Buffer *bytes.Buffer
// Contains the context set by the user. Useful for hook processing etc.
// Context carries user-provided context for hooks and formatters.
Context context.Context
// err may contain a field formatting error
// err contains internal field-formatting errors.
err string
}
// NewEntry creates a new [Entry] associated with the provided Logger.
// The logger must not be nil. Passing a nil logger results in a
// panic when a logging method (e.g., [Entry.Info], [Entry.Error], etc.)
// is called.
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, plus one optional. Give a little extra room.
Data: make(Fields, 6),
// Reserve default predefined fields and a little extra room.
Data: make(Fields, defaultFields+3),
}
}
// Dup creates a copy of the entry for further modification.
//
// Data is cloned to avoid mutating the original entry. Other fields
// (Logger, Time, Context, etc.) are copied by value.
func (entry *Entry) Dup() *Entry {
data := make(Fields, len(entry.Data))
for k, v := range entry.Data {
data[k] = v
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
return dup
}
// dup copies the entry fields shared by derived entries except Data, which
// callers must copy or initialize as appropriate for their use.
func (entry *Entry) dup() *Entry {
return &Entry{
Logger: entry.Logger,
Time: entry.Time,
Caller: entry.Caller,
Context: entry.Context,
err: entry.err,
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
}
// Bytes returns the bytes representation of this entry from the formatter.
func (entry *Entry) Bytes() ([]byte, error) {
return entry.Logger.Formatter.Format(entry)
// Snapshot the formatter under the lock to protect against concurrent
// SetFormatter calls, then release the lock before formatting.
// This avoids a data race and prevents a deadlock if Format() triggers
// reentrant logging (e.g., a field's MarshalJSON calls logrus).
//
// See:
//
// - https://github.com/sirupsen/logrus/issues/1440
// - https://github.com/sirupsen/logrus/issues/1448
entry.Logger.mu.Lock()
formatter := entry.Logger.Formatter
entry.Logger.mu.Unlock()
return formatter.Format(entry)
}
// String returns the string representation from the reader and ultimately the
@@ -112,58 +164,61 @@ func (entry *Entry) WithError(err error) *Entry {
// WithContext adds a context to the Entry.
func (entry *Entry) WithContext(ctx context.Context) *Entry {
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
}
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.Context = ctx
return dup
}
// WithField adds a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
func (entry *Entry) WithField(key string, value any) *Entry {
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.addField(key, value)
return dup
}
// WithFields adds a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
dup := entry.dup()
dup.Data = make(Fields, len(entry.Data)+len(fields))
maps.Copy(dup.Data, entry.Data)
for key, value := range fields {
dup.addField(key, value)
}
fieldErr := entry.err
for k, v := range fields {
isErrField := false
if t := reflect.TypeOf(v); t != nil {
switch {
case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
isErrField = true
}
}
if isErrField {
tmp := fmt.Sprintf("can not add field %q", k)
if fieldErr != "" {
fieldErr = entry.err + ", " + tmp
} else {
fieldErr = tmp
}
} else {
data[k] = v
}
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
return dup
}
// WithTime overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
dup := entry.dup()
dup.Data = maps.Clone(entry.Data)
dup.Time = t
return dup
}
func (entry *Entry) addField(key string, value any) {
if _, ok := value.(error); !ok {
t := reflect.TypeOf(value)
if t != nil && (t.Kind() == reflect.Func || t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Func) {
if entry.err != "" {
entry.err += ", skipping unsupported field " + strconv.Quote(key)
} else {
entry.err = "skipping unsupported field " + strconv.Quote(key)
}
return
}
}
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
if entry.Data == nil {
entry.Data = make(Fields, 1)
}
entry.Data[key] = value
}
// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
// There really ought to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
@@ -186,7 +241,7 @@ func getCaller() *runtime.Frame {
_ = runtime.Callers(0, pcs)
// dynamic get the package name and the minimum caller depth
for i := 0; i < maximumCallerDepth; i++ {
for i := range maximumCallerDepth {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
logrusPackage = getPackageName(funcName)
@@ -215,16 +270,47 @@ func getCaller() *runtime.Frame {
return nil
}
func (entry Entry) HasCaller() (has bool) {
return entry.Logger != nil &&
entry.Logger.ReportCaller &&
entry.Caller != nil
// HasCaller reports whether this Entry contains caller information.
//
// Caller may be set explicitly, or populated at log time when
// [Logger.ReportCaller] is enabled.
//
// Deprecated: use [Entry.Caller] != nil instead.
//
//go:fix inline
func (entry Entry) HasCaller() bool {
return entry.Caller != nil
}
func (entry *Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
func (entry *Entry) logArgs(level Level, panicAfter bool, args ...any) {
entry.log(level, panicAfter, sprint(args...))
}
newEntry := entry.Dup()
func (entry *Entry) logf(level Level, panicAfter bool, format string, args ...any) {
entry.log(level, panicAfter, fmt.Sprintf(format, args...))
}
// logln uses Sprintln for multiple arguments to preserve Println-style
// spacing between args, then trims the trailing newline.
func (entry *Entry) logln(level Level, panicAfter bool, args ...any) {
if len(args) <= 1 {
entry.log(level, panicAfter, sprint(args...))
return
}
msg := fmt.Sprintln(args...)
msg = msg[:len(msg)-1] // Trim the newline added by Sprintln; logging adds its own.
entry.log(level, panicAfter, msg)
}
// log writes msg at level. If panicAfter is true, it panics with the fully
// populated entry after hooks and output have completed.
//
// The explicit flag keeps panic behavior limited to Panic, Panicf, and
// Panicln while avoiding a return value used only as the panic value.
// See #1283 and commits f96066e and 5f8c666.
func (entry *Entry) log(level Level, panicAfter bool, msg string) {
newEntry := entry.dup()
newEntry.Data = maps.Clone(entry.Data)
if newEntry.Time.IsZero() {
newEntry.Time = time.Now()
@@ -233,17 +319,24 @@ func (entry *Entry) log(level Level, msg string) {
newEntry.Level = level
newEntry.Message = msg
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
logger := newEntry.Logger
logger.mu.Lock()
reportCaller := logger.ReportCaller
bufPool := newEntry.getBufferPool()
newEntry.Logger.mu.Unlock()
logger.mu.Unlock()
if reportCaller {
// Preserve explicitly set caller information.
if reportCaller && newEntry.Caller == nil {
newEntry.Caller = getCaller()
}
newEntry.fireHooks()
buffer = bufPool.Get()
// Select hooks based on the level for this log call. Hooks receive the
// Entry and may mutate it, but that does not affect which hooks are
// fired for this event.
hooks := logger.hooksForLevel(level)
newEntry.fireHooks(hooks)
buffer := bufPool.Get()
defer func() {
newEntry.Buffer = nil
buffer.Reset()
@@ -251,15 +344,12 @@ func (entry *Entry) log(level Level, msg string) {
}()
buffer.Reset()
newEntry.Buffer = buffer
newEntry.write()
newEntry.Buffer = nil
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
// Panic here so the panic value contains the fully populated entry without
// requiring log to return it to the caller.
if panicAfter {
panic(newEntry)
}
}
@@ -271,175 +361,207 @@ func (entry *Entry) getBufferPool() (pool BufferPool) {
return bufferPool
}
func (entry *Entry) fireHooks() {
var tmpHooks LevelHooks
entry.Logger.mu.Lock()
tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
for k, v := range entry.Logger.Hooks {
tmpHooks[k] = v
}
entry.Logger.mu.Unlock()
err := tmpHooks.Fire(entry.Level, entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
func (entry *Entry) fireHooks(hooks []Hook) {
for _, hook := range hooks {
if err := hook.Fire(entry); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to fire hook:", err)
return
}
}
}
func (entry *Entry) write() {
// Snapshot the formatter under the lock to protect against concurrent
// SetFormatter calls, then release the lock before formatting.
// This avoids a deadlock when Format() triggers reentrant logging (e.g.,
// a field's MarshalJSON calls logrus). See #1448, #1440.
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
formatter := entry.Logger.Formatter
entry.Logger.mu.Unlock()
serialized, err := formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
_, _ = fmt.Fprintln(os.Stderr, "Failed to format entry:", err)
return
}
// Re-acquire the lock to serialize writes to the underlying io.Writer.
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
_, _ = fmt.Fprintln(os.Stderr, "Failed to write to log:", err)
}
}
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
func (entry *Entry) Log(level Level, args ...interface{}) {
// Log logs a message at the specified level.
//
// Using Log with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Log treats the level as logging severity only;
// use [Entry.Panic] or [Entry.Fatal] when those side effects are desired.
func (entry *Entry) Log(level Level, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.log(level, fmt.Sprint(args...))
entry.logArgs(level, panicAfter, args...)
}
}
func (entry *Entry) Trace(args ...interface{}) {
func (entry *Entry) Trace(args ...any) {
entry.Log(TraceLevel, args...)
}
func (entry *Entry) Debug(args ...interface{}) {
func (entry *Entry) Debug(args ...any) {
entry.Log(DebugLevel, args...)
}
func (entry *Entry) Print(args ...interface{}) {
func (entry *Entry) Print(args ...any) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
func (entry *Entry) Info(args ...any) {
entry.Log(InfoLevel, args...)
}
func (entry *Entry) Warn(args ...interface{}) {
func (entry *Entry) Warn(args ...any) {
entry.Log(WarnLevel, args...)
}
func (entry *Entry) Warning(args ...interface{}) {
func (entry *Entry) Warning(args ...any) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
func (entry *Entry) Error(args ...any) {
entry.Log(ErrorLevel, args...)
}
func (entry *Entry) Fatal(args ...interface{}) {
func (entry *Entry) Fatal(args ...any) {
entry.Log(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
entry.Log(PanicLevel, args...)
func (entry *Entry) Panic(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logArgs(PanicLevel, panicAfter, args...)
}
}
// Entry Printf family functions
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
// Logf logs a formatted message at the specified level.
//
// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logf treats the level as logging severity only;
// use [Entry.Panicf] or [Entry.Fatalf] when those side effects are desired.
func (entry *Entry) Logf(level Level, format string, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, fmt.Sprintf(format, args...))
entry.logf(level, panicAfter, format, args...)
}
}
func (entry *Entry) Tracef(format string, args ...interface{}) {
func (entry *Entry) Tracef(format string, args ...any) {
entry.Logf(TraceLevel, format, args...)
}
func (entry *Entry) Debugf(format string, args ...interface{}) {
func (entry *Entry) Debugf(format string, args ...any) {
entry.Logf(DebugLevel, format, args...)
}
func (entry *Entry) Infof(format string, args ...interface{}) {
func (entry *Entry) Infof(format string, args ...any) {
entry.Logf(InfoLevel, format, args...)
}
func (entry *Entry) Printf(format string, args ...interface{}) {
func (entry *Entry) Printf(format string, args ...any) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
func (entry *Entry) Warnf(format string, args ...any) {
entry.Logf(WarnLevel, format, args...)
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
func (entry *Entry) Warningf(format string, args ...any) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
func (entry *Entry) Errorf(format string, args ...any) {
entry.Logf(ErrorLevel, format, args...)
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
func (entry *Entry) Fatalf(format string, args ...any) {
entry.Logf(FatalLevel, format, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
entry.Logf(PanicLevel, format, args...)
func (entry *Entry) Panicf(format string, args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logf(PanicLevel, panicAfter, format, args...)
}
}
// Entry Println family functions
func (entry *Entry) Logln(level Level, args ...interface{}) {
// Logln logs a message at the specified level with Println-style spacing.
//
// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logln treats the level as logging severity only;
// use [Entry.Panicln] or [Entry.Fatalln] when those side effects are desired.
func (entry *Entry) Logln(level Level, args ...any) {
const panicAfter = false
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, entry.sprintlnn(args...))
entry.logln(level, panicAfter, args...)
}
}
func (entry *Entry) Traceln(args ...interface{}) {
func (entry *Entry) Traceln(args ...any) {
entry.Logln(TraceLevel, args...)
}
func (entry *Entry) Debugln(args ...interface{}) {
func (entry *Entry) Debugln(args ...any) {
entry.Logln(DebugLevel, args...)
}
func (entry *Entry) Infoln(args ...interface{}) {
func (entry *Entry) Infoln(args ...any) {
entry.Logln(InfoLevel, args...)
}
func (entry *Entry) Println(args ...interface{}) {
func (entry *Entry) Println(args ...any) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
func (entry *Entry) Warnln(args ...any) {
entry.Logln(WarnLevel, args...)
}
func (entry *Entry) Warningln(args ...interface{}) {
func (entry *Entry) Warningln(args ...any) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
func (entry *Entry) Errorln(args ...any) {
entry.Logln(ErrorLevel, args...)
}
func (entry *Entry) Fatalln(args ...interface{}) {
func (entry *Entry) Fatalln(args ...any) {
entry.Logln(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
entry.Logln(PanicLevel, args...)
func (entry *Entry) Panicln(args ...any) {
const panicAfter = true
if entry.Logger.IsLevelEnabled(PanicLevel) {
entry.logln(PanicLevel, panicAfter, args...)
}
}
// sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
// sprint is fmt.Sprint with fast paths for zero or one string argument.
func sprint(args ...any) string {
switch len(args) {
case 0:
return ""
case 1:
if msg, ok := args[0].(string); ok {
return msg
}
}
return fmt.Sprint(args...)
}
+82 -87
View File
@@ -6,11 +6,12 @@ import (
"time"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
// std is the package-level standard logger, similar to the default logger
// in the stdlib [log] package.
var std = New()
// StandardLogger returns the package-level standard logger used by
// the top-level logging functions.
func StandardLogger() *Logger {
return std
}
@@ -41,7 +42,7 @@ func GetLevel() Level {
return std.GetLevel()
}
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
// IsLevelEnabled checks if logging for the given level is enabled for the standard logger.
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
}
@@ -51,9 +52,10 @@ func AddHook(hook Hook) {
std.AddHook(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
// WithError creates an entry from the standard logger and adds an error to it,
// using the value defined in [ErrorKey] as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
return std.WithError(err)
}
// WithContext creates an entry from the standard logger and adds a context to it.
@@ -61,210 +63,203 @@ func WithContext(ctx context.Context) *Entry {
return std.WithContext(ctx)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
// WithField creates an entry from the standard logger and adds a single field.
// For multiple fields, prefer [WithFields] over chaining WithField calls.
func WithField(key string, value any) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
// WithFields creates an entry from the standard logger and adds the fields to it.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// WithTime creates an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
// WithTime creates an entry from the standard logger and overrides the time
// used for logs generated with it.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
// Trace logs a message at level [TraceLevel] on the standard logger.
func Trace(args ...any) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
// Debug logs a message at level [DebugLevel] on the standard logger.
func Debug(args ...any) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
// Print logs a message at level [InfoLevel] on the standard logger.
func Print(args ...any) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
// Info logs a message at level [InfoLevel] on the standard logger.
func Info(args ...any) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
// Warn logs a message at level [WarnLevel] on the standard logger.
func Warn(args ...any) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
// Warning logs a message at level [WarnLevel] on the standard logger.
func Warning(args ...any) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
// Error logs a message at level [ErrorLevel] on the standard logger.
func Error(args ...any) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
// Panic logs a message at level [PanicLevel] on the standard logger.
func Panic(args ...any) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) {
// Fatal logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatal(args ...any) {
std.Fatal(args...)
}
// TraceFn logs a message from a func at level Trace on the standard logger.
// TraceFn logs a message from a func at level [TraceLevel] on the standard logger.
func TraceFn(fn LogFunction) {
std.TraceFn(fn)
}
// DebugFn logs a message from a func at level Debug on the standard logger.
// DebugFn logs a message from a func at level [DebugLevel] on the standard logger.
func DebugFn(fn LogFunction) {
std.DebugFn(fn)
}
// PrintFn logs a message from a func at level Info on the standard logger.
// PrintFn logs a message from a func at level [InfoLevel] on the standard logger.
func PrintFn(fn LogFunction) {
std.PrintFn(fn)
}
// InfoFn logs a message from a func at level Info on the standard logger.
// InfoFn logs a message from a func at level [InfoLevel] on the standard logger.
func InfoFn(fn LogFunction) {
std.InfoFn(fn)
}
// WarnFn logs a message from a func at level Warn on the standard logger.
// WarnFn logs a message from a func at level [WarnLevel] on the standard logger.
func WarnFn(fn LogFunction) {
std.WarnFn(fn)
}
// WarningFn logs a message from a func at level Warn on the standard logger.
// WarningFn logs a message from a func at level [WarnLevel] on the standard logger.
func WarningFn(fn LogFunction) {
std.WarningFn(fn)
}
// ErrorFn logs a message from a func at level Error on the standard logger.
// ErrorFn logs a message from a func at level [ErrorLevel] on the standard logger.
func ErrorFn(fn LogFunction) {
std.ErrorFn(fn)
}
// PanicFn logs a message from a func at level Panic on the standard logger.
// PanicFn logs a message from a func at level [PanicLevel] on the standard logger.
func PanicFn(fn LogFunction) {
std.PanicFn(fn)
}
// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
// FatalFn logs a message from a func at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func FatalFn(fn LogFunction) {
std.FatalFn(fn)
}
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
// Tracef logs a message at level [TraceLevel] on the standard logger.
func Tracef(format string, args ...any) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
// Debugf logs a message at level [DebugLevel] on the standard logger.
func Debugf(format string, args ...any) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
// Printf logs a message at level [InfoLevel] on the standard logger.
func Printf(format string, args ...any) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
// Infof logs a message at level [InfoLevel] on the standard logger.
func Infof(format string, args ...any) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
// Warnf logs a message at level [WarnLevel] on the standard logger.
func Warnf(format string, args ...any) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
// Warningf logs a message at level [WarnLevel] on the standard logger.
func Warningf(format string, args ...any) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
// Errorf logs a message at level [ErrorLevel] on the standard logger.
func Errorf(format string, args ...any) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
// Panicf logs a message at level [PanicLevel] on the standard logger.
func Panicf(format string, args ...any) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) {
// Fatalf logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalf(format string, args ...any) {
std.Fatalf(format, args...)
}
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
// Traceln logs a message at level [TraceLevel] on the standard logger.
func Traceln(args ...any) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
// Debugln logs a message at level [DebugLevel] on the standard logger.
func Debugln(args ...any) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
// Println logs a message at level [InfoLevel] on the standard logger.
func Println(args ...any) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
// Infoln logs a message at level [InfoLevel] on the standard logger.
func Infoln(args ...any) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
// Warnln logs a message at level [WarnLevel] on the standard logger.
func Warnln(args ...any) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
// Warningln logs a message at level [WarnLevel] on the standard logger.
func Warningln(args ...any) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
// Errorln logs a message at level [ErrorLevel] on the standard logger.
func Errorln(args ...any) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
// Panicln logs a message at level [PanicLevel] on the standard logger.
func Panicln(args ...any) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) {
// Fatalln logs a message at level [FatalLevel] on the standard logger,
// then exits the process with status 1.
func Fatalln(args ...any) {
std.Fatalln(args...)
}
+30 -17
View File
@@ -2,27 +2,40 @@ package logrus
import "time"
// Default key names for the default fields
const (
// defaultTimestampFormat is the layout used to format entry timestamps
// when a formatter has not specified a custom TimestampFormat.
// It follows time.RFC3339 and is applied unless timestamps are disabled.
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
// defaultFields is the number of commonly included predefined log entry fields
// (msg, level, time). It is used as a capacity hint when constructing
// intermediate collections during formatting (for example, the fixed key list).
//
// It does not include the optional "logrus_error", "func", or "file" fields.
defaultFields = 3
)
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
)
// Formatter is implemented by types that format log entries. It receives an
// [*Entry], which contains:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
// - entry.Message: the message passed to logging methods such as [Info], [Warn], [Error]
// - entry.Time: the timestamp
// - entry.Level: the log level
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
// Additional fields added with [WithField] or [WithFields] are available in
// [Entry.Data]. Format should return the formatted log entry as a byte slice,
// which is written to [Logger.Out].
type Formatter interface {
Format(*Entry) ([]byte, error)
}
@@ -30,12 +43,12 @@ type Formatter interface {
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
+23 -14
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"runtime"
"strconv"
)
type fieldKey string
@@ -20,7 +21,13 @@ func (f FieldMap) resolve(key fieldKey) string {
return string(key)
}
// JSONFormatter formats logs into parsable json
// JSONFormatter formats logs into parsable JSON.
//
// Fields from [Entry.Data] are included in the JSON object together with the
// standard fields derived from the entry. If a field conflicts with a standard
// field, it is prefixed with "fields.". Standard field names can be customized
// through FieldMap. When DataKey is set, fields from [Entry.Data] are nested
// under that key instead.
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
// The format to use is the same than for time.Format or time.Parse from the standard
@@ -61,7 +68,8 @@ type JSONFormatter struct {
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+4)
caller := entry.Caller
data := make(Fields, len(entry.Data)+defaultFields)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
@@ -73,13 +81,14 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
if f.DataKey != "" {
newData := make(Fields, 4)
if f.DataKey != "" && len(entry.Data) > 0 {
newData := make(Fields, defaultFields+1)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
@@ -94,11 +103,13 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
if entry.HasCaller() {
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if caller != nil {
var funcVal, fileVal string
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
@@ -108,11 +119,9 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
}
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
encoder := json.NewEncoder(b)
+101
View File
@@ -0,0 +1,101 @@
package logrus
import (
"strings"
"sync"
)
const (
ansiReset = "\x1b[0m" // reset attributes
ansiRed = "\x1b[31m" // red
ansiYellow = "\x1b[33m" // yellow
ansiCyan = "\x1b[36m" // cyan
ansiDimCyan = "\x1b[2;36m" // dim cyan
ansiDimWhite = "\x1b[2;37m" // dim white (light gray)
)
type lvlPrefix struct {
full string
truncated string
padded string
}
func colorize(level Level, s string) string {
color := ansiCyan
switch level {
case TraceLevel:
color = ansiDimWhite
case DebugLevel:
color = ansiDimCyan
case WarnLevel:
color = ansiYellow
case ErrorLevel, FatalLevel, PanicLevel:
color = ansiRed
case InfoLevel:
color = ansiCyan
}
return color + s + ansiReset
}
func formatLevel(level Level, disableTrunc, pad bool, maxLen int) string {
upper := strings.ToUpper(level.String())
if pad && maxLen > len(upper) {
upper += strings.Repeat(" ", maxLen-len(upper))
}
if !pad && !disableTrunc && len(upper) > 4 {
upper = upper[:4]
}
return colorize(level, upper)
}
var levelPrefixOnce = sync.OnceValues(func() (map[Level]lvlPrefix, lvlPrefix) {
var maxLevel Level
maxLen := 0
for _, lvl := range AllLevels {
if lvl > maxLevel {
maxLevel = lvl
}
if l := len(lvl.String()); l > maxLen {
maxLen = l
}
}
prefix := make(map[Level]lvlPrefix, len(AllLevels))
for _, lvl := range AllLevels {
prefix[lvl] = lvlPrefix{
full: formatLevel(lvl, true, false, maxLen),
truncated: formatLevel(lvl, false, false, maxLen),
padded: formatLevel(lvl, true, true, maxLen),
}
}
unknownLevel := maxLevel + 1
unknown := lvlPrefix{
full: formatLevel(unknownLevel, true, false, maxLen),
truncated: formatLevel(unknownLevel, false, false, maxLen),
padded: formatLevel(unknownLevel, true, true, maxLen),
}
return prefix, unknown
})
func levelPrefix(level Level, disableTrunc, pad bool) string {
prefix, unknown := levelPrefixOnce()
p, ok := prefix[level]
if !ok {
p = unknown
}
switch {
case pad:
return p.padded
case !disableTrunc:
return p.truncated
default:
return p.full
}
}
+107 -49
View File
@@ -12,17 +12,19 @@ import (
// LogFunction For big messages, it can be more efficient to pass a function
// and only call it if the log level is actually enables rather than
// generating the log message and then checking if the level is enabled
type LogFunction func() []interface{}
type LogFunction func() []any
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventurous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
@@ -38,37 +40,44 @@ type Logger struct {
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged.
Level Level
// Used to sync writing to the log. Locking is enabled by Default
mu MutexWrap
mu mutexWrap
// Reusable empty entry
entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
ExitFunc func(int)
// The buffer pool used to format the log. If it is nil, the default global
// buffer pool will be used.
BufferPool BufferPool
}
type exitFunc func(int)
// MutexWrap is the mutex implementation used by [Logger].
//
// Deprecated: MutexWrap is an implementation detail of Logger and should not be used directly.
type MutexWrap = mutexWrap
type MutexWrap struct {
type mutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
func (mw *mutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
func (mw *mutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
func (mw *mutexWrap) Disable() {
mw.disabled = true
}
@@ -104,7 +113,7 @@ func (logger *Logger) newEntry() *Entry {
}
func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
entry.Data = map[string]any{}
logger.entryPool.Put(entry)
}
@@ -112,7 +121,7 @@ func (logger *Logger) releaseEntry(entry *Entry) {
// Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
// this new returned entry.
// If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
func (logger *Logger) WithField(key string, value any) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
@@ -148,7 +157,12 @@ func (logger *Logger) WithTime(t time.Time) *Entry {
return entry.WithTime(t)
}
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
// Logf logs a formatted message at the specified level.
//
// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logf treats the level as logging severity only;
// use [Logger.Panicf] or [Logger.Fatalf] when those side effects are desired.
func (logger *Logger) Logf(level Level, format string, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logf(level, format, args...)
@@ -156,49 +170,55 @@ func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
}
}
func (logger *Logger) Tracef(format string, args ...interface{}) {
func (logger *Logger) Tracef(format string, args ...any) {
logger.Logf(TraceLevel, format, args...)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
func (logger *Logger) Debugf(format string, args ...any) {
logger.Logf(DebugLevel, format, args...)
}
func (logger *Logger) Infof(format string, args ...interface{}) {
func (logger *Logger) Infof(format string, args ...any) {
logger.Logf(InfoLevel, format, args...)
}
func (logger *Logger) Printf(format string, args ...interface{}) {
func (logger *Logger) Printf(format string, args ...any) {
entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
func (logger *Logger) Warnf(format string, args ...any) {
logger.Logf(WarnLevel, format, args...)
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
func (logger *Logger) Warningf(format string, args ...any) {
logger.Warnf(format, args...)
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
func (logger *Logger) Errorf(format string, args ...any) {
logger.Logf(ErrorLevel, format, args...)
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
func (logger *Logger) Fatalf(format string, args ...any) {
logger.Logf(FatalLevel, format, args...)
logger.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.Logf(PanicLevel, format, args...)
func (logger *Logger) Panicf(format string, args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicf(format, args...)
}
}
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
func (logger *Logger) Log(level Level, args ...interface{}) {
// Log logs a message at the specified level.
//
// Using Log with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Log treats the level as logging severity only;
// use [Logger.Panic] or [Logger.Fatal] when those side effects are desired.
func (logger *Logger) Log(level Level, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Log(level, args...)
@@ -206,6 +226,11 @@ func (logger *Logger) Log(level Level, args ...interface{}) {
}
}
// LogFn logs a message returned by fn at the specified level.
//
// Using LogFn with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. LogFn treats the level as logging severity only;
// use [Logger.PanicFn] or [Logger.FatalFn] when those side effects are desired.
func (logger *Logger) LogFn(level Level, fn LogFunction) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
@@ -214,43 +239,47 @@ func (logger *Logger) LogFn(level Level, fn LogFunction) {
}
}
func (logger *Logger) Trace(args ...interface{}) {
func (logger *Logger) Trace(args ...any) {
logger.Log(TraceLevel, args...)
}
func (logger *Logger) Debug(args ...interface{}) {
func (logger *Logger) Debug(args ...any) {
logger.Log(DebugLevel, args...)
}
func (logger *Logger) Info(args ...interface{}) {
func (logger *Logger) Info(args ...any) {
logger.Log(InfoLevel, args...)
}
func (logger *Logger) Print(args ...interface{}) {
func (logger *Logger) Print(args ...any) {
entry := logger.newEntry()
entry.Print(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warn(args ...interface{}) {
func (logger *Logger) Warn(args ...any) {
logger.Log(WarnLevel, args...)
}
func (logger *Logger) Warning(args ...interface{}) {
func (logger *Logger) Warning(args ...any) {
logger.Warn(args...)
}
func (logger *Logger) Error(args ...interface{}) {
func (logger *Logger) Error(args ...any) {
logger.Log(ErrorLevel, args...)
}
func (logger *Logger) Fatal(args ...interface{}) {
func (logger *Logger) Fatal(args ...any) {
logger.Log(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
logger.Log(PanicLevel, args...)
func (logger *Logger) Panic(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(args...)
}
}
func (logger *Logger) TraceFn(fn LogFunction) {
@@ -289,10 +318,19 @@ func (logger *Logger) FatalFn(fn LogFunction) {
}
func (logger *Logger) PanicFn(fn LogFunction) {
logger.LogFn(PanicLevel, fn)
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panic(fn()...)
}
}
func (logger *Logger) Logln(level Level, args ...interface{}) {
// Logln logs a message at the specified level with Println-style spacing.
//
// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not
// trigger a panic or exit. Logln treats the level as logging severity only;
// use [Logger.Panicln] or [Logger.Fatalln] when those side effects are desired.
func (logger *Logger) Logln(level Level, args ...any) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logln(level, args...)
@@ -300,43 +338,47 @@ func (logger *Logger) Logln(level Level, args ...interface{}) {
}
}
func (logger *Logger) Traceln(args ...interface{}) {
func (logger *Logger) Traceln(args ...any) {
logger.Logln(TraceLevel, args...)
}
func (logger *Logger) Debugln(args ...interface{}) {
func (logger *Logger) Debugln(args ...any) {
logger.Logln(DebugLevel, args...)
}
func (logger *Logger) Infoln(args ...interface{}) {
func (logger *Logger) Infoln(args ...any) {
logger.Logln(InfoLevel, args...)
}
func (logger *Logger) Println(args ...interface{}) {
func (logger *Logger) Println(args ...any) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnln(args ...interface{}) {
func (logger *Logger) Warnln(args ...any) {
logger.Logln(WarnLevel, args...)
}
func (logger *Logger) Warningln(args ...interface{}) {
func (logger *Logger) Warningln(args ...any) {
logger.Warnln(args...)
}
func (logger *Logger) Errorln(args ...interface{}) {
func (logger *Logger) Errorln(args ...any) {
logger.Logln(ErrorLevel, args...)
}
func (logger *Logger) Fatalln(args ...interface{}) {
func (logger *Logger) Fatalln(args ...any) {
logger.Logln(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
logger.Logln(PanicLevel, args...)
func (logger *Logger) Panicln(args ...any) {
if logger.IsLevelEnabled(PanicLevel) {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
entry.Panicln(args...)
}
}
func (logger *Logger) Exit(code int) {
@@ -375,7 +417,22 @@ func (logger *Logger) AddHook(hook Hook) {
logger.Hooks.Add(hook)
}
// IsLevelEnabled checks if the log level of the logger is greater than the level param
// hooksForLevel returns a snapshot of the hooks registered for the given level.
// The returned slice is a shallow copy and may be used without holding logger.mu.
func (logger *Logger) hooksForLevel(level Level) []Hook {
logger.mu.Lock()
hooks := logger.Hooks[level]
if len(hooks) == 0 {
logger.mu.Unlock()
return nil
}
out := make([]Hook, len(hooks))
copy(out, hooks)
logger.mu.Unlock()
return out
}
// IsLevelEnabled checks if logging for the given level is enabled.
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
@@ -394,6 +451,7 @@ func (logger *Logger) SetOutput(output io.Writer) {
logger.Out = output
}
// SetReportCaller sets whether the caller stack frame must be logged.
func (logger *Logger) SetReportCaller(reportCaller bool) {
logger.mu.Lock()
defer logger.mu.Unlock()
@@ -403,9 +461,9 @@ func (logger *Logger) SetReportCaller(reportCaller bool) {
// ReplaceHooks replaces the logger hooks and returns the old ones
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Lock()
defer logger.mu.Unlock()
oldHooks := logger.Hooks
logger.Hooks = hooks
logger.mu.Unlock()
return oldHooks
}
+129 -92
View File
@@ -1,13 +1,13 @@
package logrus
import (
"bytes"
"fmt"
"log"
"strings"
)
// Fields type, used to pass to [WithFields].
type Fields map[string]interface{}
type Fields map[string]any
// Level type
//
@@ -16,39 +16,56 @@ type Level uint32
// Convert the Level to a string. E.g. [PanicLevel] becomes "panic".
func (level Level) String() string {
if b, err := level.MarshalText(); err == nil {
return string(b)
} else {
switch level {
case TraceLevel:
return "trace"
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
default:
return "unknown"
}
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
case "trace":
return TraceLevel, nil
}
return parseLevel([]byte(lvl))
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
func parseLevel(b []byte) (Level, error) {
switch {
case bytes.EqualFold(b, []byte("panic")):
return PanicLevel, nil
case bytes.EqualFold(b, []byte("fatal")):
return FatalLevel, nil
case bytes.EqualFold(b, []byte("error")):
return ErrorLevel, nil
case bytes.EqualFold(b, []byte("warn")),
bytes.EqualFold(b, []byte("warning")):
return WarnLevel, nil
case bytes.EqualFold(b, []byte("info")):
return InfoLevel, nil
case bytes.EqualFold(b, []byte("debug")):
return DebugLevel, nil
case bytes.EqualFold(b, []byte("trace")):
return TraceLevel, nil
default:
return 0, fmt.Errorf("not a valid logrus Level: %q", b)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := ParseLevel(string(text))
l, err := parseLevel(text)
if err != nil {
return err
}
@@ -60,23 +77,11 @@ func (level *Level) UnmarshalText(text []byte) error {
func (level Level) MarshalText() ([]byte, error) {
switch level {
case TraceLevel:
return []byte("trace"), nil
case DebugLevel:
return []byte("debug"), nil
case InfoLevel:
return []byte("info"), nil
case WarnLevel:
return []byte("warning"), nil
case ErrorLevel:
return []byte("error"), nil
case FatalLevel:
return []byte("fatal"), nil
case PanicLevel:
return []byte("panic"), nil
case TraceLevel, DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel, PanicLevel:
return []byte(level.String()), nil
default:
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// AllLevels exposing all logging levels.
@@ -91,7 +96,7 @@ var AllLevels = []Level{
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
// on your instance of logger, obtained with [logrus.New].
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
@@ -113,78 +118,110 @@ const (
TraceLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
// Compile-time interface assertions.
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
_ StdLogger = (*log.Logger)(nil)
_ StdLogger = (*Entry)(nil)
_ StdLogger = (*Logger)(nil)
_ FieldLogger = (*Logger)(nil)
_ FieldLogger = (*Entry)(nil)
_ FieldLogger = Ext1FieldLogger(nil)
_ DebugLogger = (*Logger)(nil)
_ InfoLogger = (*Logger)(nil)
_ WarnLogger = (*Logger)(nil)
_ ErrorLogger = (*Logger)(nil)
_ TraceLogger = (*Logger)(nil)
_ DebugLogger = (*Entry)(nil)
_ InfoLogger = (*Entry)(nil)
_ WarnLogger = (*Entry)(nil)
_ ErrorLogger = (*Entry)(nil)
_ TraceLogger = (*Entry)(nil)
_ Ext1FieldLogger = (*Logger)(nil)
_ Ext1FieldLogger = (*Entry)(nil)
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger ([log.Logger]) and a logrus logger.
// There's no standard interface, so this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Print(args ...any)
Printf(format string, args ...any)
Println(args ...any)
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Fatal(args ...any)
Fatalf(format string, args ...any)
Fatalln(args ...any)
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Panic(args ...any)
Panicf(format string, args ...any)
Panicln(args ...any)
}
// FieldLogger extends the [StdLogger] interface, generalizing
// the [Entry] and [Logger] types.
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithField(key string, value any) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
StdLogger
DebugLogger
InfoLogger
WarnLogger
ErrorLogger
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
// Legacy warning aliases. These are kept on FieldLogger for backwards
// compatibility, but are intentionally omitted from [WarnLogger].
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
Warning(args ...any)
Warningf(format string, args ...any)
Warningln(args ...any)
}
// Ext1FieldLogger (the first extension to [FieldLogger]) is superfluous, it is
// here for consistency. Do not use. Use [FieldLogger], [Logger] or [Entry]
// instead.
// DebugLogger provides convenience functions to log messages at level [DebugLevel].
type DebugLogger interface {
Debug(args ...any)
Debugf(format string, args ...any)
Debugln(args ...any)
}
// InfoLogger provides convenience functions to log messages at level [InfoLevel].
type InfoLogger interface {
Info(args ...any)
Infof(format string, args ...any)
Infoln(args ...any)
}
// WarnLogger provides convenience functions to log messages at level [WarnLevel].
type WarnLogger interface {
Warn(args ...any)
Warnf(format string, args ...any)
Warnln(args ...any)
}
// ErrorLogger provides convenience functions to log messages at level [ErrorLevel].
type ErrorLogger interface {
Error(args ...any)
Errorf(format string, args ...any)
Errorln(args ...any)
}
// TraceLogger provides convenience functions to log messages at level [TraceLevel].
type TraceLogger interface {
Trace(args ...any)
Tracef(format string, args ...any)
Traceln(args ...any)
}
// Ext1FieldLogger is FieldLogger extended with Trace-level methods.
//
// New code should prefer the smallest applicable interface, such as
// [FieldLogger] or [TraceLogger], or use [Logger] or [Entry] directly.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
TraceLogger
}
+2 -6
View File
@@ -1,11 +1,7 @@
// +build appengine
//go:build appengine
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
func checkIfTerminal(_ any) bool {
return true
}
+1 -2
View File
@@ -1,5 +1,4 @@
// +build darwin dragonfly freebsd netbsd openbsd hurd
// +build !js
//go:build (darwin || dragonfly || freebsd || netbsd || openbsd || hurd) && !tinygo
package logrus
-7
View File
@@ -1,7 +0,0 @@
// +build js
package logrus
func isTerminal(fd int) bool {
return false
}
+2 -6
View File
@@ -1,11 +1,7 @@
// +build js nacl plan9
//go:build js || nacl || plan9 || wasi || wasip1 || tinygo
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
func checkIfTerminal(_ any) bool {
return false
}
+6 -2
View File
@@ -1,4 +1,4 @@
// +build !appengine,!js,!windows,!nacl,!plan9
//go:build !appengine && !js && !windows && !nacl && !plan9 && !wasi && !wasip1 && !tinygo
package logrus
@@ -10,7 +10,11 @@ import (
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return isTerminal(int(v.Fd()))
fd := v.Fd()
if fd > uintptr(^uint(0)>>1) {
return false
}
return isTerminal(int(fd))
default:
return false
}
+2
View File
@@ -1,3 +1,5 @@
//go:build solaris && !tinygo
package logrus
import (
+1 -4
View File
@@ -1,7 +1,4 @@
//go:build (linux || aix || zos) && !js && !wasi
// +build linux aix zos
// +build !js
// +build !wasi
//go:build (linux || aix || zos) && !tinygo
package logrus
-8
View File
@@ -1,8 +0,0 @@
//go:build wasi
// +build wasi
package logrus
func isTerminal(fd int) bool {
return false
}
-8
View File
@@ -1,8 +0,0 @@
//go:build wasip1
// +build wasip1
package logrus
func isTerminal(fd int) bool {
return false
}
+1 -1
View File
@@ -1,4 +1,4 @@
// +build !appengine,!js,windows
//go:build windows && !appengine
package logrus
+317 -174
View File
@@ -3,30 +3,32 @@ package logrus
import (
"bytes"
"fmt"
"maps"
"os"
"reflect"
"runtime"
"sort"
"slices"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
)
const (
red = 31
yellow = 33
blue = 36
gray = 37
)
var baseTimestamp = time.Now()
var baseTimestamp time.Time
func init() {
baseTimestamp = time.Now()
}
// TextFormatter formats logs into text
// TextFormatter formats logs into text.
//
// Output is logfmt-like: key=value pairs separated by spaces. Fields from
// [Entry.Data] are included together with the standard fields derived from the
// entry. If a field conflicts with a standard field, it is prefixed with
// "fields.". Standard field names can be customized through FieldMap.
//
// Field keys are written as-is (unquoted and unescaped) in the plain
// (non-colored) format; only field values may be quoted depending on
// DisableQuote, ForceQuote, QuoteEmptyFields, and the value content.
//
// When colors are enabled, ANSI escape sequences may be added for presentation.
// For fully escaped structured output (including safe keys), use JSONFormatter.
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
@@ -64,7 +66,7 @@ type TextFormatter struct {
// be desired.
DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
// The keys sorting function, when uninitialized it uses slices.Sort.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
@@ -77,16 +79,22 @@ type TextFormatter struct {
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
// Whether the logger's out is to a terminal. Don't use this field
// directly; use TextFormatter.isTerminal instead.
terminal bool
// FieldMap allows users to customize the names of keys for default fields.
// Mapped keys are written as-is, so they should be safe for plain-text output.
//
// As an example:
//
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// },
// }
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
@@ -96,54 +104,78 @@ type TextFormatter struct {
CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once
// The max length of the level text, generated dynamically on init
levelTextMaxLength int
}
func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out)
}
// Get the max length of the level text
for _, level := range AllLevels {
levelTextLength := utf8.RuneCount([]byte(level.String()))
if levelTextLength > f.levelTextMaxLength {
f.levelTextMaxLength = levelTextLength
}
func (f *TextFormatter) isTerminal(entry *Entry) bool {
if entry == nil || entry.Logger == nil {
// Don't run the terminalInitOnce without a logger, otherwise we'd
// cache the default (false) forever even if a logger is attached
// later.
return false
}
f.terminalInitOnce.Do(func() {
entry.Logger.mu.Lock()
out := entry.Logger.Out
entry.Logger.mu.Unlock()
f.terminal = checkIfTerminal(out)
})
return f.terminal
}
func (f *TextFormatter) isColored() bool {
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
if f.EnvironmentOverrideColors {
switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
case ok && force != "0":
isColored = true
case ok && force == "0", os.Getenv("CLICOLOR") == "0":
isColored = false
}
func (f *TextFormatter) isColored(isTerminal bool) bool {
if f.DisableColors {
return false
}
return isColored && !f.DisableColors
colored := f.ForceColors || isTerminal
if !f.EnvironmentOverrideColors {
return colored
}
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok {
return force != "0"
}
if os.Getenv("CLICOLOR") == "0" {
return false
}
return colored
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields)
for k, v := range entry.Data {
data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
data := make(Fields, len(entry.Data))
maps.Copy(data, entry.Data)
isColored := f.isColored(f.isTerminal(entry))
caller := entry.Caller
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
var funcVal, fileVal string
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
fixedKeys := make([]string, 0, 4+len(data))
if isColored {
f.printColored(b, entry, keys, data)
} else {
f.printPlain(b, entry, keys, data)
}
return b.Bytes(), nil
}
func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
caller := entry.Caller
hasCaller := caller != nil
fixedKeys := make([]string, 0, len(keys)+defaultFields)
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
@@ -154,12 +186,14 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
var funcVal, fileVal string
if caller != nil {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if funcVal != "" {
@@ -172,152 +206,108 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if !f.DisableSorting {
if f.SortingFunc == nil {
sort.Strings(keys)
// Default sorting does not sort the "fixed keys";
// see https://github.com/sirupsen/logrus/commit/73bc94e60c753099e8bae902f81fbd6e7dd95f26
slices.Sort(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
f.terminalInitOnce.Do(func() { f.init(entry) })
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if f.isColored() {
f.printColored(b, entry, keys, data, timestampFormat)
} else {
for _, key := range fixedKeys {
var value interface{}
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
value = entry.Time.Format(timestampFormat)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
value = fileVal
default:
value = data[key]
for _, key := range fixedKeys {
var value any
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
if f.TimestampFormat == "" {
value = entry.Time.Format(defaultTimestampFormat)
} else {
value = entry.Time.Format(f.TimestampFormat)
}
f.appendKeyValue(b, key, value)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && hasCaller:
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && hasCaller:
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel, TraceLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
case InfoLevel:
levelColor = blue
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation && !f.PadLevelText {
levelText = levelText[0:4]
}
if f.PadLevelText {
// Generates the format string used in the next line, for example "%-6s" or "%-7s".
// Based on the max level text length.
formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
// Formats the level text by appending spaces up to the max length, for example:
// - "INFO "
// - "WARNING"
levelText = fmt.Sprintf(formatString, levelText)
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
var callerText string
if caller := entry.Caller; caller != nil {
var funcVal, fileVal string
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
if caller.Function != "" {
funcVal = caller.Function + "()"
}
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if fileVal == "" {
caller = funcVal
callerText = funcVal
} else if funcVal == "" {
caller = fileVal
callerText = fileVal
} else {
caller = fileVal + " " + funcVal
callerText = fileVal + " " + funcVal
}
}
levelText := levelPrefix(entry.Level, f.DisableLevelTruncation, f.PadLevelText)
switch {
case f.DisableTimestamp:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
_, _ = fmt.Fprintf(b, "%s%s %-44s ", levelText, callerText, entry.Message)
case !f.FullTimestamp:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
_, _ = fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), callerText, entry.Message)
default:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
_, _ = fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), callerText, entry.Message)
}
for _, k := range keys {
v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
}
func (f *TextFormatter) needsQuoting(text string) bool {
if f.ForceQuote {
return true
}
if f.QuoteEmptyFields && len(text) == 0 {
return true
}
if f.DisableQuote {
return false
}
for _, ch := range text {
//nolint:staticcheck // QF1001: could apply De Morgan's law
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
return true
if !f.DisableSorting {
if f.SortingFunc == nil {
slices.Sort(keys)
} else {
f.SortingFunc(keys)
}
}
return false
// Keys use the same color as the level-prefix.
for _, k := range keys {
b.WriteByte(' ')
b.WriteString(colorize(entry.Level, k))
b.WriteByte('=')
f.appendValue(b, data[k])
}
b.WriteByte('\n')
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
// appendKeyValue writes key=value. Keys are written verbatim (unquoted/unescaped);
// values are subject to quoting/escaping.
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) {
if b.Len() > 0 {
b.WriteByte(' ')
}
@@ -326,15 +316,168 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interf
f.appendValue(b, value)
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
stringVal, ok := value.(string)
if !ok {
stringVal = fmt.Sprint(value)
func (f *TextFormatter) appendValue(b *bytes.Buffer, value any) {
// Fast paths.
switch v := value.(type) {
case string:
f.appendString(b, v)
return
case []byte:
f.appendBytes(b, v)
return
case bool:
var raw [8]byte
f.appendBytes(b, strconv.AppendBool(raw[:0], v))
return
case error:
f.appendError(b, v)
return
case fmt.Stringer:
f.appendStringer(b, v)
return
}
if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
fmt.Fprintf(b, "%q", stringVal)
// Handle common primitives.
var raw [64]byte
var num []byte
switch v := value.(type) {
case int:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int8:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int16:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int32:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int64:
num = strconv.AppendInt(raw[:0], v, 10)
case uint:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint8:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint16:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint32:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint64:
num = strconv.AppendUint(raw[:0], v, 10)
case uintptr:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case float32:
num = strconv.AppendFloat(raw[:0], float64(v), 'g', -1, 32)
case float64:
num = strconv.AppendFloat(raw[:0], v, 'g', -1, 64)
default:
f.appendString(b, fmt.Sprint(value))
return
}
f.appendNumeric(b, num)
}
func (f *TextFormatter) appendString(b *bytes.Buffer, s string) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(s) == 0) || (!f.DisableQuote && needsQuoting(s))
if !quote {
b.WriteString(s)
return
}
if len(s) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], s))
}
func (f *TextFormatter) appendBytes(b *bytes.Buffer, bs []byte) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(bs) == 0) || (!f.DisableQuote && needsQuotingBytes(bs))
if !quote {
b.Write(bs)
return
}
if len(bs) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(bs)))
}
func (f *TextFormatter) appendNumeric(b *bytes.Buffer, out []byte) {
if f.ForceQuote {
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(out)))
return
}
b.Write(out)
}
func (f *TextFormatter) appendError(b *bytes.Buffer, v error) {
defer f.recoverValue(b, v, "Error")
f.appendString(b, v.Error())
}
func (f *TextFormatter) appendStringer(b *bytes.Buffer, v fmt.Stringer) {
defer f.recoverValue(b, v, "String")
f.appendString(b, v.String())
}
func (f *TextFormatter) recoverValue(b *bytes.Buffer, v any, method string) {
if r := recover(); r != nil {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer && rv.IsNil() {
f.appendString(b, "<nil>")
} else {
f.appendString(b, fmt.Sprintf("%%!v(PANIC=%s method: %v)", method, r))
}
}
}
// needsQuoting returns true if the string contains any byte that
// requires quoting. It returns false when every byte is "safe" according
// to isSafeByte.
func needsQuoting(s string) bool {
// use an index loop (avoid rune decoding).
for i := range len(s) {
c := s[i]
if !isSafeByte(c) {
return true
}
}
return false
}
// needsQuotingBytes returns true if the byte slice contains any byte that
// requires quoting. It returns false when every byte is "safe" according
// to isSafeByte.
func needsQuotingBytes(bs []byte) bool {
for _, c := range bs {
if !isSafeByte(c) {
return true
}
}
return false
}
// isSafeByte returns true if the byte is allowed unquoted (ASCII and in the allowlist).
// It purposely uses byte arithmetic (no runes) for performance.
func isSafeByte(ch byte) bool {
ok := ch < 0x80 && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))
if ok {
return true
}
switch ch {
case '-', '.', '_', '/', '@', '^', '+':
return true
default:
return false
}
}
+2 -4
View File
@@ -30,7 +30,7 @@ func (entry *Entry) Writer() *io.PipeWriter {
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
var printFunc func(args ...interface{})
printFunc := entry.Print
// Determine which log function to use based on the specified log level
switch level {
@@ -48,8 +48,6 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
printFunc = entry.Fatal
case PanicLevel:
printFunc = entry.Panic
default:
printFunc = entry.Print
}
// Start a new goroutine to scan the input and write it to the logger using the specified print function.
@@ -63,7 +61,7 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
}
// writerScanner scans the input from the reader and writes it to the logger
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...any)) {
scanner := bufio.NewScanner(reader)
// Set the buffer size to the maximum token size to avoid buffer overflows
+4 -3
View File
@@ -162,10 +162,11 @@ github.com/mattn/go-runewidth
# github.com/moby/docker-image-spec v1.3.1
## explicit; go 1.18
github.com/moby/docker-image-spec/specs-go/v1
# github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee
# github.com/moby/go-archive v0.3.3
## explicit; go 1.25
github.com/moby/go-archive
github.com/moby/go-archive/compression
github.com/moby/go-archive/internal/archiveoptions
github.com/moby/go-archive/tarheader
# github.com/moby/moby/api v1.55.0
## explicit; go 1.24
@@ -276,8 +277,8 @@ github.com/prometheus/procfs/internal/util
# github.com/russross/blackfriday/v2 v2.1.0
## explicit
github.com/russross/blackfriday/v2
# github.com/sirupsen/logrus v1.9.4
## explicit; go 1.17
# github.com/sirupsen/logrus v1.10.1
## explicit; go 1.23
github.com/sirupsen/logrus
# github.com/spf13/cobra v1.10.2
## explicit; go 1.15