mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -05:00
Compare commits
50
Commits
v29.7.0-rc.1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d072fe407f | ||
|
|
3fe73079aa | ||
|
|
28f756087e | ||
|
|
387b8e2af8 | ||
|
|
55e4a7eef5 | ||
|
|
58d9cdd2d9 | ||
|
|
8659aa5d4c | ||
|
|
8d08c75e81 | ||
|
|
24af004cbf | ||
|
|
eb68ed19d2 | ||
|
|
8a375034e2 | ||
|
|
a7c0741fa7 | ||
|
|
12892075d2 | ||
|
|
e1f9b284df | ||
|
|
4f84911bfe | ||
|
|
228f29e810 | ||
|
|
d8c3543407 | ||
|
|
2dd7b7e15b | ||
|
|
a7dcaa6fdb | ||
|
|
400b45f682 | ||
|
|
38887ec4ed | ||
|
|
904aef7f69 | ||
|
|
f08e60eb09 | ||
|
|
abfd89157d | ||
|
|
519eb45d03 | ||
|
|
8717af7168 | ||
|
|
e9452d6e78 | ||
|
|
a6014a702b | ||
|
|
2465fca604 | ||
|
|
52b15cc571 | ||
|
|
13590921a4 | ||
|
|
0b50545471 | ||
|
|
c1eba931e3 | ||
|
|
1a305be376 | ||
|
|
4c1c648df1 | ||
|
|
ee046d35c0 | ||
|
|
d70c3fd5e7 | ||
|
|
f6d6bede46 | ||
|
|
b47659808a | ||
|
|
af626e1ed7 | ||
|
|
6d35de4601 | ||
|
|
ad8dce1012 | ||
|
|
844210bd0f | ||
|
|
bd719d6703 | ||
|
|
52b1160c85 | ||
|
|
5ff2ff6311 | ||
|
|
2d978b80ae | ||
|
|
2abf92e95c | ||
|
|
3cc61496db | ||
|
|
55b88882d7 |
+25
-39
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
+33
-15
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, " ")))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ~30–44% faster.
|
||||
* TextFormatter paths are up to ~40% faster, with allocation counts reduced
|
||||
by 25–74% 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 Entry’s 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,7 +0,0 @@
|
||||
// +build js
|
||||
|
||||
package logrus
|
||||
|
||||
func isTerminal(fd int) bool {
|
||||
return false
|
||||
}
|
||||
+2
-6
@@ -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
@@ -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
@@ -1,3 +1,5 @@
|
||||
//go:build solaris && !tinygo
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
|
||||
+1
-4
@@ -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
@@ -1,8 +0,0 @@
|
||||
//go:build wasi
|
||||
// +build wasi
|
||||
|
||||
package logrus
|
||||
|
||||
func isTerminal(fd int) bool {
|
||||
return false
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
//go:build wasip1
|
||||
// +build wasip1
|
||||
|
||||
package logrus
|
||||
|
||||
func isTerminal(fd int) bool {
|
||||
return false
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// +build !appengine,!js,windows
|
||||
//go:build windows && !appengine
|
||||
|
||||
package logrus
|
||||
|
||||
|
||||
+317
-174
@@ -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
@@ -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
|
||||
|
||||
Vendored
+4
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user