diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ebbc08a..6b4085e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,3 +21,7 @@ updates: timezone: "America/Chicago" commit-message: prefix: "chore" + groups: + otel: + patterns: + - "go.opentelemetry.io/*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d75a3..0b453e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,40 +12,27 @@ on: workflow_dispatch: jobs: - fmt: - runs-on: ubuntu-20.04 + go: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: make fmt - uses: ./ci/image + - uses: actions/checkout@v3 + - name: Cache npm + uses: actions/cache@v3 with: - args: make fmt - - lint: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: make lint - uses: ./ci/image + path: ~/.npm + key: "npm-cache" + - uses: actions/setup-go@v4 with: - args: make lint - - test: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: make test - uses: ./ci/image - with: - args: make test + go-version: "1.20" + cache-dependency-path: go.sum + - name: "make" + run: | + git config --global --add safe.directory /github/workspace + make -O -j fmt lint test env: COVERALLS_TOKEN: ${{ secrets.github_token }} - - name: Upload coverage.html - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: coverage path: ci/out/coverage.html diff --git a/ci/fmt.mk b/ci/fmt.mk index 026cc36..5519ed6 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -1,4 +1,4 @@ -fmt: modtidy gofmt goimports prettier +fmt: modtidy gofmt prettier ifdef CI if [[ $$(git ls-files --other --modified --exclude-standard) != "" ]]; then echo "Files need generation or are formatted incorrectly:" @@ -13,13 +13,11 @@ modtidy: gen go mod tidy gofmt: gen - gofmt -w -s . - -goimports: gen - goimports -w "-local=$$(go list -m)" . + # gofumpt v0.7.0 requires Go 1.22 or later. + go run mvdan.cc/gofumpt@v0.6.0 -w . prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml") + npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml") gen: go generate ./... diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index 2c6988e..0000000 --- a/ci/image/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang:1 - -RUN apt-get update && \ - apt-get install -y npm - -ENV GOFLAGS="-mod=readonly" -ENV PAGER=cat -ENV CI=true -ENV MAKEFLAGS="--jobs=8 --output-sync=target" - -RUN npm install -g prettier -RUN go install golang.org/x/tools/cmd/goimports@latest -RUN go install golang.org/x/lint/golint@latest -RUN go install github.com/mattn/goveralls@latest diff --git a/ci/lint.mk b/ci/lint.mk index fbf42d2..e190817 100644 --- a/ci/lint.mk +++ b/ci/lint.mk @@ -4,4 +4,5 @@ govet: go vet ./... golint: - golint -set_exit_status ./... + # golangci-lint newer than v1.55.2 is not compatible with Go 1.20 when using go run. + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 run . diff --git a/ci/test.mk b/ci/test.mk index 2615c51..35bfd05 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -15,7 +15,7 @@ coveralls: gotest export CI_PULL_REQUEST="$$(jq .number "$$GITHUB_EVENT_PATH")" BUILD_NUMBER="$$BUILD_NUMBER-PR-$$CI_PULL_REQUEST" fi - goveralls -coverprofile=ci/out/coverage.prof -service=github + go run github.com/mattn/goveralls@latest -coverprofile=ci/out/coverage.prof -service=github gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... diff --git a/example_test.go b/example_test.go index 2853ff7..95131ee 100644 --- a/example_test.go +++ b/example_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "go.opencensus.io/trace" "golang.org/x/xerrors" "cdr.dev/slog" @@ -72,23 +71,13 @@ func Example_testing() { slog.F("field_name", "something or the other"), ) - // t.go:55: 2019-12-05 21:20:31.218 [INFO] my message here {"field_name": "something or the other"} -} - -func Example_tracing() { - log := slog.Make(sloghuman.Sink(os.Stdout)) - - ctx, _ := trace.StartSpan(context.Background(), "spanName") - - log.Info(ctx, "my msg", slog.F("hello", "hi")) - - // 2019-12-09 21:59:48.110 [INFO] my msg {"trace": "f143d018d00de835688453d8dc55c9fd", "span": "f214167bf550afc3", "hello": "hi"} + // t.go:55: 2019-12-05 21:20:31.218 [INFO] my message here field_name="something or the other" } func Example_multiple() { l := slog.Make(sloghuman.Sink(os.Stdout)) - f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644) if err != nil { l.Fatal(context.Background(), "failed to open stackdriver log file", slog.Error(err)) } @@ -97,7 +86,7 @@ func Example_multiple() { l.Info(context.Background(), "log to stdout and stackdriver") - // 2019-12-07 20:59:55.790 [INFO] log to stdout and stackdriver + // 2019-12-07 20:59:55.790 [INFO] log to stdout and stackdriver } func ExampleWith() { @@ -106,7 +95,7 @@ func ExampleWith() { l := slog.Make(sloghuman.Sink(os.Stdout)) l.Info(ctx, "msg") - // 2019-12-07 20:54:23.986 [INFO] msg {"field": 1} + // 2019-12-07 20:54:23.986 [INFO] msg field=1} } func ExampleStdlib() { @@ -115,7 +104,7 @@ func ExampleStdlib() { l.Print("msg") - // 2019-12-07 20:54:23.986 [INFO] (stdlib) msg {"field": 1} + // 2019-12-07 20:54:23.986 [INFO] (stdlib) msg field=1 } func ExampleLogger_Named() { @@ -125,7 +114,7 @@ func ExampleLogger_Named() { l = l.Named("http") l.Info(ctx, "received request", slog.F("remote address", net.IPv4(127, 0, 0, 1))) - // 2019-12-07 21:20:56.974 [INFO] (http) received request {"remote address": "127.0.0.1"} + // 2019-12-07 21:20:56.974 [INFO] (http) received request remote_address=127.0.0.1} } func ExampleLogger_Leveled() { @@ -139,6 +128,6 @@ func ExampleLogger_Leveled() { l.Debug(ctx, "testing2") - // 2019-12-07 21:26:20.945 [INFO] received request - // 2019-12-07 21:26:20.945 [DEBUG] testing2 + // 2019-12-07 21:26:20.945 [INFO] received request + // 2019-12-07 21:26:20.945 [DEBU] testing2 } diff --git a/go.mod b/go.mod index ac067d3..52efddb 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,40 @@ module cdr.dev/slog -go 1.13 +go 1.20 require ( - cloud.google.com/go v0.26.0 - github.com/alecthomas/chroma v0.10.0 - github.com/fatih/color v1.13.0 - github.com/google/go-cmp v0.5.3 - go.opencensus.io v0.24.0 - go.uber.org/goleak v1.2.1 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + cloud.google.com/go/compute/metadata v0.2.3 + cloud.google.com/go/logging v1.8.1 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/google/go-cmp v0.5.9 + github.com/muesli/termenv v0.15.2 + go.opentelemetry.io/otel/sdk v1.16.0 + go.opentelemetry.io/otel/trace v1.16.0 + go.uber.org/goleak v1.2.1 + golang.org/x/term v0.11.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 + google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e +) + +require ( + cloud.google.com/go/compute v1.23.0 // indirect + cloud.google.com/go/longrunning v0.5.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.11.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/grpc v1.57.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 7de8934..9eb2fc3 100644 --- a/go.sum +++ b/go.sum @@ -1,131 +1,76 @@ -cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 4e741c9..8af4d82 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -4,19 +4,21 @@ package entryhuman import ( "bytes" + "database/sql/driver" "encoding/json" "fmt" "io" "os" - "path/filepath" - "runtime/debug" + "reflect" "strconv" "strings" + "syscall" "time" + "unicode" - "github.com/fatih/color" - "go.opencensus.io/trace" - "golang.org/x/crypto/ssh/terminal" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" + "golang.org/x/term" "golang.org/x/xerrors" "cdr.dev/slog" @@ -34,15 +36,58 @@ func StripTimestamp(ent string) (time.Time, string, error) { // TimeFormat is a simplified RFC3339 format. const TimeFormat = "2006-01-02 15:04:05.000" -func c(w io.Writer, attrs ...color.Attribute) *color.Color { - c := color.New(attrs...) - c.DisableColor() +var ( + renderer = lipgloss.NewRenderer(os.Stdout, termenv.WithUnsafe()) + + timeStyle = renderer.NewStyle().Foreground(lipgloss.Color("#606366")) +) + +func render(w io.Writer, st lipgloss.Style, s string) string { if shouldColor(w) { - c.EnableColor() + ss := st.Render(s) + return ss + } + return s +} + +func reset(w io.Writer, termW io.Writer) { + if shouldColor(termW) { + fmt.Fprintf(w, termenv.CSI+termenv.ResetSeq+"m") } - return c } +func formatValue(v interface{}) string { + if vr, ok := v.(driver.Valuer); ok { + var err error + v, err = vr.Value() + if err != nil { + return fmt.Sprintf("error calling Value: %v", err) + } + } + if v == nil { + return "" + } + typ := reflect.TypeOf(v) + switch typ.Kind() { + case reflect.Struct, reflect.Map: + byt, err := json.Marshal(v) + if err != nil { + panic(err) + } + return string(byt) + case reflect.Slice: + // Byte slices are optimistically readable. + if typ.Elem().Kind() == reflect.Uint8 { + return fmt.Sprintf("%q", v) + } + fallthrough + default: + return quote(fmt.Sprintf("%+v", v)) + } +} + +const tab = " " + // Fmt returns a human readable format for ent. // // We never return with a trailing newline because Go's testing framework adds one @@ -50,26 +95,30 @@ func c(w io.Writer, attrs ...color.Attribute) *color.Color { // We also do not indent the fields as go's test does that automatically // for extra lines in a log so if we did it here, the fields would be indented // twice in test logs. So the Stderr logger indents all the fields itself. -func Fmt(w io.Writer, ent slog.SinkEntry) string { - ents := c(w, color.Reset).Sprint("") +func Fmt( + buf interface { + io.StringWriter + io.Writer + }, termW io.Writer, ent slog.SinkEntry, +) { + reset(buf, termW) ts := ent.Time.Format(TimeFormat) - ents += ts + " " + buf.WriteString(render(termW, timeStyle, ts+" ")) - level := "[" + ent.Level.String() + "]" - level = c(w, levelColor(ent.Level)).Sprint(level) - ents += fmt.Sprintf("%v\t", level) + level := ent.Level.String() + level = strings.ToLower(level) + if len(level) > 4 { + level = level[:4] + } + level = "[" + level + "]" + buf.WriteString(render(termW, levelStyle(ent.Level), level)) + buf.WriteString(" ") if len(ent.LoggerNames) > 0 { - loggerName := "(" + quoteKey(strings.Join(ent.LoggerNames, ".")) + ")" - loggerName = c(w, color.FgMagenta).Sprint(loggerName) - ents += fmt.Sprintf("%v\t", loggerName) + loggerName := quoteKey(strings.Join(ent.LoggerNames, ".")) + ": " + buf.WriteString(loggerName) } - hpath, hfn := humanPathAndFunc(ent.File, ent.Func) - loc := fmt.Sprintf("<%v:%v>\t%v", hpath, ent.Line, hfn) - loc = c(w, color.FgCyan).Sprint(loc) - ents += fmt.Sprintf("%v\t", loc) - var multilineKey string var multilineVal string msg := strings.TrimSpace(ent.Message) @@ -77,11 +126,11 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string { multilineKey = "msg" multilineVal = msg msg = "..." + msg = quote(msg) } - msg = quote(msg) - ents += msg + buf.WriteString(msg) - if ent.SpanContext != (trace.SpanContext{}) { + if ent.SpanContext.IsValid() { ent.Fields = append(slog.M( slog.F("trace", ent.SpanContext.TraceID), slog.F("span", ent.SpanContext.SpanID), @@ -111,48 +160,61 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string { multilineVal = s } - if len(ent.Fields) > 0 { - // No error is guaranteed due to slog.Map handling errors itself. - fields, _ := json.MarshalIndent(ent.Fields, "", "") - fields = bytes.ReplaceAll(fields, []byte(",\n"), []byte(", ")) - fields = bytes.ReplaceAll(fields, []byte("\n"), []byte("")) - fields = formatJSON(w, fields) - ents += "\t" + string(fields) + keyStyle := timeStyle + // Help users distinguish logs by keeping some color in the equal signs. + equalsStyle := timeStyle + + for i, f := range ent.Fields { + if i < len(ent.Fields) { + buf.WriteString(tab) + } + buf.WriteString(render(termW, keyStyle, quoteKey(f.Name))) + buf.WriteString(render(termW, equalsStyle, "=")) + valueStr := formatValue(f.Value) + buf.WriteString(valueStr) } if multilineVal != "" { if msg != "..." { - ents += " ..." + buf.WriteString(" ...") } // Proper indentation. lines := strings.Split(multilineVal, "\n") for i, line := range lines[1:] { if line != "" { - lines[i+1] = c(w, color.Reset).Sprint("") + strings.Repeat(" ", len(multilineKey)+4) + line + lines[i+1] = strings.Repeat(" ", len(multilineKey)+2) + line } } multilineVal = strings.Join(lines, "\n") - multilineKey = c(w, color.FgBlue).Sprintf(`"%v"`, multilineKey) - ents += fmt.Sprintf("\n%v: %v", multilineKey, multilineVal) + multilineKey = render(termW, keyStyle, multilineKey) + buf.WriteString("\n") + buf.WriteString(multilineKey) + buf.WriteString("= ") + buf.WriteString(multilineVal) } - - return ents } -func levelColor(level slog.Level) color.Attribute { +var ( + levelDebugStyle = timeStyle.Copy() + levelInfoStyle = renderer.NewStyle().Foreground(lipgloss.Color("#0091FF")) + levelWarnStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FFCF0D")) + levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")) +) + +func levelStyle(level slog.Level) lipgloss.Style { switch level { case slog.LevelDebug: - return color.Reset + return levelDebugStyle case slog.LevelInfo: - return color.FgBlue + return levelInfoStyle case slog.LevelWarn: - return color.FgYellow - case slog.LevelError: - return color.FgRed + return levelWarnStyle + case slog.LevelError, slog.LevelFatal, slog.LevelCritical: + return levelErrorStyle default: - return color.FgHiRed + panic("unknown level") } } @@ -163,10 +225,26 @@ func isTTY(w io.Writer) bool { if w == forceColorWriter { return true } - f, ok := w.(interface { - Fd() uintptr - }) - return ok && terminal.IsTerminal(int(f.Fd())) + // SyscallConn is safe during file close. + if sc, ok := w.(interface { + SyscallConn() (syscall.RawConn, error) + }); ok { + conn, err := sc.SyscallConn() + if err != nil { + return false + } + var isTerm bool + err = conn.Control(func(fd uintptr) { + isTerm = term.IsTerminal(int(fd)) + }) + if err != nil { + return false + } + return isTerm + } + // Fallback to unsafe Fd. + f, ok := w.(interface{ Fd() uintptr }) + return ok && term.IsTerminal(int(f.Fd())) } func shouldColor(w io.Writer) bool { @@ -182,11 +260,18 @@ func quote(key string) string { return `""` } + var hasSpace bool + for _, r := range key { + if unicode.IsSpace(r) { + hasSpace = true + break + } + } quoted := strconv.Quote(key) // If the key doesn't need to be quoted, don't quote it. // We do not use strconv.CanBackquote because it doesn't // account tabs. - if quoted[1:len(quoted)-1] == key { + if !hasSpace && quoted[1:len(quoted)-1] == key { return key } return quoted @@ -194,66 +279,5 @@ func quote(key string) string { func quoteKey(key string) string { // Replace spaces in the map keys with underscores. - return strings.ReplaceAll(key, " ", "_") -} - -var mainPackagePath string -var mainModulePath string - -func init() { - // Unfortunately does not work for tests yet :( - // See https://github.com/golang/go/issues/33976 - bi, ok := debug.ReadBuildInfo() - if !ok { - return - } - mainPackagePath = bi.Path - mainModulePath = bi.Main.Path -} - -// humanPathAndFunc takes the absolute path to a file and an absolute module path to a -// function in that file and returns the module path to the file. It also returns -// the path to the function stripped of its module prefix. -// -// If the file is in the main Go module then its path is returned -// relative to the main Go module's root. -// -// fn is from https://pkg.go.dev/runtime#Func.Name -func humanPathAndFunc(filename, fn string) (hpath, hfn string) { - // pkgDir is the dir of the pkg. - // e.g. cdr.dev/slog/internal - // base is the package name and the function name separated by a period. - // e.g. entryhuman.humanPathAndFunc - // There can be multiple periods when methods of types are involved. - pkgDir, base := filepath.Split(fn) - s := strings.Split(base, ".") - pkg := s[0] - hfn = strings.Join(s[1:], ".") - - if pkg == "main" { - // This happens with go build main.go - if mainPackagePath == "command-line-arguments" { - // Without a real mainPath, we can't find the path to the file - // relative to the module. So we just return the base. - return filepath.Base(filename), hfn - } - // Go doesn't return the full main path in runtime.Func.Name() - // It just returns the path "main" - // Only runtime.ReadBuildInfo returns it so we have to check and replace. - pkgDir = mainPackagePath - // pkg main isn't reflected on the disk so we should not add it - // into the pkgpath. - pkg = "" - } - - hpath = filepath.Join(pkgDir, pkg, filepath.Base(filename)) - - if mainModulePath != "" { - relhpath, err := filepath.Rel(mainModulePath, hpath) - if err == nil { - hpath = "./" + relhpath - } - } - - return hpath, hfn + return quote(strings.ReplaceAll(key, " ", "_")) } diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index f7fc596..45a885a 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -1,12 +1,15 @@ package entryhuman_test import ( - "io/ioutil" + "bytes" + "database/sql" + "flag" + "fmt" + "io" + "os" "testing" "time" - "go.opencensus.io/trace" - "cdr.dev/slog" "cdr.dev/slog/internal/assert" "cdr.dev/slog/internal/entryhuman" @@ -14,81 +17,228 @@ import ( var kt = time.Date(2000, time.February, 5, 4, 4, 4, 4, time.UTC) +var updateGoldenFiles = flag.Bool("update-golden-files", false, "update golden files in testdata") + +type testObj struct { + foo int + bar int + dra []byte +} + func TestEntry(t *testing.T) { t.Parallel() - test := func(t *testing.T, in slog.SinkEntry, exp string) { - act := entryhuman.Fmt(ioutil.Discard, in) - assert.Equal(t, "entry", exp, act) + type tcase struct { + name string + ent slog.SinkEntry } - t.Run("basic", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "wowowow\tizi", - Time: kt, - Level: slog.LevelDebug, - - File: "myfile", - Line: 100, - Func: "mypkg.ignored", - }, `2000-02-05 04:04:04.000 [DEBUG] ignored "wowowow\tizi"`) - }) - - t.Run("multilineMessage", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "line1\nline2", - Level: slog.LevelInfo, - }, `0001-01-01 00:00:00.000 [INFO] <.:0> ... -"msg": line1 - line2`) - }) - - t.Run("multilineField", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "msg", - Level: slog.LevelInfo, - Fields: slog.M(slog.F("field", "line1\nline2")), - }, `0001-01-01 00:00:00.000 [INFO] <.:0> msg ... -"field": line1 - line2`) - }) - - t.Run("named", func(t *testing.T) { - t.Parallel() + ents := []tcase{ + { + "simpleNoFields", + slog.SinkEntry{ + Message: "wowowow\tizi", + Time: kt, + Level: slog.LevelDebug, + + File: "myfile", + Line: 100, + Func: "mypkg.ignored", + }, + }, + { + "multilineMessage", + slog.SinkEntry{ + Message: "line1\nline2", + Level: slog.LevelInfo, + }, + }, + { + "multilineField", + slog.SinkEntry{ + Message: "msg", + Level: slog.LevelInfo, + Fields: slog.M(slog.F("field", "line1\nline2")), + }, + }, + { + "named", + slog.SinkEntry{ + Level: slog.LevelWarn, + LoggerNames: []string{"some", "cat"}, + Message: "meow", + Fields: slog.M( + slog.F("breath", "stinky"), + ), + }, + }, + { + "funky", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("funky^%&^&^key", "value"), + slog.F("funky^%&^&^key2", "@#\t \t \n"), + ), + }, + }, + { + "spacey", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("space in my key", "value in my value"), + ), + }, + }, + { + "nil", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("nan", nil), + ), + }, + }, + { + "bytes", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("somefile", []byte("blah bla\x01h blah")), + ), + }, + }, + { + "driverValue", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("val", sql.NullString{String: "dog", Valid: true}), + slog.F("inval", sql.NullString{String: "cat", Valid: false}), + ), + }, + }, + { + "object", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("obj", slog.M( + slog.F("obj1", testObj{ + foo: 1, + bar: 2, + dra: []byte("blah"), + }), + slog.F("obj2", testObj{ + foo: 3, + bar: 4, + dra: []byte("blah"), + }), + )), + slog.F("map", map[string]string{ + "key1": "value1", + }), + ), + }, + }, + } + if *updateGoldenFiles { + ents, err := os.ReadDir("testdata") + if err != nil { + t.Fatal(err) + } + for _, ent := range ents { + os.Remove("testdata/" + ent.Name()) + } + } - test(t, slog.SinkEntry{ - Level: slog.LevelWarn, - LoggerNames: []string{"named", "meow"}, - }, `0001-01-01 00:00:00.000 [WARN] (named.meow) <.:0> ""`) - }) + for _, tc := range ents { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + goldenPath := fmt.Sprintf("testdata/%s.golden", tc.name) + + var gotBuf bytes.Buffer + entryhuman.Fmt(&gotBuf, io.Discard, tc.ent) + + if *updateGoldenFiles { + err := os.WriteFile(goldenPath, gotBuf.Bytes(), 0o644) + if err != nil { + t.Fatal(err) + } + return + } + + wantByt, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "entry matches", string(wantByt), gotBuf.String()) + }) + } - t.Run("trace", func(t *testing.T) { + t.Run("isTTY during file close", func(t *testing.T) { t.Parallel() - test(t, slog.SinkEntry{ - Level: slog.LevelError, - SpanContext: trace.SpanContext{ - SpanID: trace.SpanID{0, 1, 2, 3, 4, 5, 6, 7}, - TraceID: trace.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - }, - }, `0001-01-01 00:00:00.000 [ERROR] <.:0> "" {"trace": "000102030405060708090a0b0c0d0e0f", "span": "0001020304050607"}`) + tmpdir := t.TempDir() + f, err := os.CreateTemp(tmpdir, "slog") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + done := make(chan struct{}, 2) + go func() { + entryhuman.Fmt(new(bytes.Buffer), f, slog.SinkEntry{ + Level: slog.LevelCritical, + Fields: slog.M( + slog.F("hey", "hi"), + ), + }) + done <- struct{}{} + }() + go func() { + _ = f.Close() + done <- struct{}{} + }() + <-done + <-done }) +} - t.Run("color", func(t *testing.T) { - t.Parallel() - - act := entryhuman.Fmt(entryhuman.ForceColorWriter, slog.SinkEntry{ - Level: slog.LevelCritical, - Fields: slog.M( - slog.F("hey", "hi"), - ), - }) - assert.Equal(t, "entry", "\x1b[0m\x1b[0m0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0> \x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", act) - }) +func BenchmarkFmt(b *testing.B) { + bench := func(b *testing.B, color bool) { + nfs := []int{1, 4, 16} + for _, nf := range nfs { + name := fmt.Sprintf("nf=%v", nf) + if color { + name = "Colored-" + name + } + b.Run(name, func(b *testing.B) { + fs := make([]slog.Field, nf) + for i := 0; i < nf; i++ { + fs[i] = slog.F("key", "value") + } + se := slog.SinkEntry{ + Level: slog.LevelCritical, + Fields: slog.M( + fs..., + ), + } + w := io.Discard + if color { + w = entryhuman.ForceColorWriter + } + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + entryhuman.Fmt(bytes.NewBuffer(nil), w, se) + } + }) + } + } + bench(b, true) + bench(b, false) } diff --git a/internal/entryhuman/json.go b/internal/entryhuman/json.go deleted file mode 100644 index 25f65f6..0000000 --- a/internal/entryhuman/json.go +++ /dev/null @@ -1,42 +0,0 @@ -package entryhuman - -import ( - "bytes" - "io" - - "github.com/alecthomas/chroma" - "github.com/alecthomas/chroma/formatters" - jlexers "github.com/alecthomas/chroma/lexers/j" -) - -// Adapted from https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/styles/bw.go#L7 -// https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/formatters/tty_indexed.go -// https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/lexers/j/json.go -var style = chroma.MustNewStyle("slog", chroma.StyleEntries{ - // Magenta. - chroma.Keyword: "#7f007f", - // Magenta. - chroma.Number: "#7f007f", - // Magenta. - chroma.Name: "#00007f", - // Green. - chroma.String: "#007f00", -}) - -var jsonLexer = chroma.Coalesce(jlexers.JSON) - -func formatJSON(w io.Writer, buf []byte) []byte { - if !shouldColor(w) { - return buf - } - - highlighted, _ := colorizeJSON(buf) - return highlighted -} - -func colorizeJSON(buf []byte) ([]byte, error) { - it, _ := jsonLexer.Tokenise(nil, string(buf)) - b := &bytes.Buffer{} - formatters.TTY8.Format(b, style, it) - return b.Bytes(), nil -} diff --git a/internal/entryhuman/testdata/bytes.golden b/internal/entryhuman/testdata/bytes.golden new file mode 100644 index 0000000..e4c5490 --- /dev/null +++ b/internal/entryhuman/testdata/bytes.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] somefile="blah bla\x01h blah" \ No newline at end of file diff --git a/internal/entryhuman/testdata/driverValue.golden b/internal/entryhuman/testdata/driverValue.golden new file mode 100644 index 0000000..e03ace2 --- /dev/null +++ b/internal/entryhuman/testdata/driverValue.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] val=dog inval= \ No newline at end of file diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden new file mode 100644 index 0000000..fc6a460 --- /dev/null +++ b/internal/entryhuman/testdata/funky.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden new file mode 100644 index 0000000..a2777d8 --- /dev/null +++ b/internal/entryhuman/testdata/multilineField.golden @@ -0,0 +1,3 @@ +0001-01-01 00:00:00.000 [info] msg ... +field= line1 + line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden new file mode 100644 index 0000000..233fda6 --- /dev/null +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -0,0 +1,3 @@ +0001-01-01 00:00:00.000 [info] ... +msg= line1 + line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden new file mode 100644 index 0000000..83867bf --- /dev/null +++ b/internal/entryhuman/testdata/named.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] some.cat: meow breath=stinky \ No newline at end of file diff --git a/internal/entryhuman/testdata/nil.golden b/internal/entryhuman/testdata/nil.golden new file mode 100644 index 0000000..86b6330 --- /dev/null +++ b/internal/entryhuman/testdata/nil.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] nan= \ No newline at end of file diff --git a/internal/entryhuman/testdata/object.golden b/internal/entryhuman/testdata/object.golden new file mode 100644 index 0000000..855cb06 --- /dev/null +++ b/internal/entryhuman/testdata/object.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] obj="[{Name:obj1 Value:{foo:1 bar:2 dra:[98 108 97 104]}} {Name:obj2 Value:{foo:3 bar:4 dra:[98 108 97 104]}}]" map={"key1":"value1"} \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden new file mode 100644 index 0000000..db46f6a --- /dev/null +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -0,0 +1 @@ +2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden new file mode 100644 index 0000000..7135d8c --- /dev/null +++ b/internal/entryhuman/testdata/spacey.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file diff --git a/map.go b/map.go index 636f4ee..cbf2991 100644 --- a/map.go +++ b/map.go @@ -2,6 +2,7 @@ package slog import ( "bytes" + "database/sql/driver" "encoding/json" "fmt" "reflect" @@ -70,6 +71,14 @@ func marshalList(rv reflect.Value) []byte { } func encode(v interface{}) []byte { + if vr, ok := v.(driver.Valuer); ok { + var err error + v, err = vr.Value() + if err != nil { + return encodeJSON(fmt.Sprintf("error calling Value: %v", err)) + } + } + switch v := v.(type) { case json.Marshaler: return encodeJSON(v) diff --git a/map_test.go b/map_test.go index e15a6ee..c89adf0 100644 --- a/map_test.go +++ b/map_test.go @@ -62,12 +62,12 @@ func TestMap(t *testing.T) { { "msg": "wrap1", "fun": "cdr.dev/slog_test.TestMap.func2", - "loc": "`+mapTestFile+`:41" + "loc": "`+mapTestFile+`:41" }, { "msg": "wrap2", "fun": "cdr.dev/slog_test.TestMap.func2", - "loc": "`+mapTestFile+`:42" + "loc": "`+mapTestFile+`:42" }, "EOF" ], @@ -93,7 +93,7 @@ func TestMap(t *testing.T) { { "msg": "failed to marshal to JSON", "fun": "cdr.dev/slog.encodeJSON", - "loc": "`+mapTestFile+`:131" + "loc": "`+mapTestFile+`:140" }, "json: error calling MarshalJSON for type slog_test.complexJSON: json: unsupported type: complex128" ], @@ -187,7 +187,7 @@ func TestMap(t *testing.T) { t.Parallel() test(t, slog.M( - slog.F("val", time.Date(2000, 02, 05, 4, 4, 4, 0, time.UTC)), + slog.F("val", time.Date(2000, 0o2, 0o5, 4, 4, 4, 0, time.UTC)), ), `{ "val": "2000-02-05T04:04:04Z" }`) @@ -222,10 +222,6 @@ func TestMap(t *testing.T) { }) } -type meow struct { - a int -} - func indentJSON(t *testing.T, j string) string { b := &bytes.Buffer{} err := json.Indent(b, []byte(j), "", strings.Repeat(" ", 4)) diff --git a/s.go b/s.go index 250a003..c6d5df9 100644 --- a/s.go +++ b/s.go @@ -41,7 +41,7 @@ func (w stdlogWriter) Write(p []byte) (n int, err error) { // we do not want. msg = strings.TrimSuffix(msg, "\n") - w.l.log(w.ctx, w.level, msg, Map{}) + w.l.log(w.ctx, w.level, msg, nil) return len(p), nil } diff --git a/s_test.go b/s_test.go index 4e3b9e1..358282e 100644 --- a/s_test.go +++ b/s_test.go @@ -23,5 +23,5 @@ func TestStdlib(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t(stdlib)\t\tTestStdlib\tstdlib\t{\"hi\": \"we\"}\n", rest) + assert.Equal(t, "entry", " [info] stdlib: stdlib hi=we\n", rest) } diff --git a/slog.go b/slog.go index adea6e2..a3b705c 100644 --- a/slog.go +++ b/slog.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "go.opencensus.io/trace" + "go.opentelemetry.io/otel/trace" ) var defaultExitFn = os.Exit @@ -80,40 +80,67 @@ func Make(sinks ...Sink) Logger { } // Debug logs the msg and fields at LevelDebug. -func (l Logger) Debug(ctx context.Context, msg string, fields ...Field) { +// See Info for information on the fields argument. +func (l Logger) Debug(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelDebug, msg, fields) } // Info logs the msg and fields at LevelInfo. -func (l Logger) Info(ctx context.Context, msg string, fields ...Field) { +// Fields may contain any combination of key value pairs, Field, and Map. +// For example: +// +// log.Info(ctx, "something happened", "user", "alex", slog.F("age", 20)) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", slog.F("user", "alex"), slog.F("age", 20)) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", slog.M( +// slog.F("user", "alex"), +// slog.F("age", 20), +// )) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", "user", "alex", "age", 20) +// +// In general, prefer using key value pairs over Field and Map, as that is how +// the standard library's slog package works. +func (l Logger) Info(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelInfo, msg, fields) } // Warn logs the msg and fields at LevelWarn. -func (l Logger) Warn(ctx context.Context, msg string, fields ...Field) { +// See Info() for information on the fields argument. +func (l Logger) Warn(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelWarn, msg, fields) } // Error logs the msg and fields at LevelError. +// See Info() for information on the fields argument. // // It will then Sync(). -func (l Logger) Error(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Error(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelError, msg, fields) l.Sync() } // Critical logs the msg and fields at LevelCritical. +// See Info() for information on the fields argument. // // It will then Sync(). -func (l Logger) Critical(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Critical(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelCritical, msg, fields) l.Sync() } // Fatal logs the msg and fields at LevelFatal. +// See Info() for information on the fields argument. // // It will then Sync() and os.Exit(1). -func (l Logger) Fatal(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Fatal(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelFatal, msg, fields) l.Sync() @@ -155,7 +182,32 @@ func (l Logger) AppendSinks(s ...Sink) Logger { return l } -func (l Logger) log(ctx context.Context, level Level, msg string, fields Map) { +func (l Logger) log(ctx context.Context, level Level, msg string, rawFields []any) { + fields := make(Map, 0, len(rawFields)) + var wipField Field + for i, f := range rawFields { + if wipField.Name != "" { + wipField.Value = f + fields = append(fields, wipField) + wipField = Field{} + continue + } + switch f := f.(type) { + case Field: + fields = append(fields, f) + case Map: + fields = append(fields, f...) + case string: + wipField.Name = f + default: + panic(fmt.Sprintf("unexpected field type %T at index %v (does it have a key?)", f, i)) + } + } + + if wipField.Name != "" { + panic(fmt.Sprintf("field %q has no value", wipField.Name)) + } + ent := l.entry(ctx, level, msg, fields) l.Log(ctx, ent) } @@ -166,7 +218,7 @@ func (l Logger) entry(ctx context.Context, level Level, msg string, fields Map) Level: level, Message: msg, Fields: fieldsFromContext(ctx).append(fields), - SpanContext: trace.FromContext(ctx).SpanContext(), + SpanContext: trace.SpanContextFromContext(ctx), } ent = ent.fillLoc(l.skip + 3) return ent diff --git a/slog_test.go b/slog_test.go index b708b51..277a9c3 100644 --- a/slog_test.go +++ b/slog_test.go @@ -2,11 +2,12 @@ package slog_test import ( "context" + "fmt" "io" "runtime" "testing" - "go.opencensus.io/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "cdr.dev/slog" "cdr.dev/slog/internal/assert" @@ -75,7 +76,7 @@ func TestLogger(t *testing.T) { File: slogTestFile, Func: "cdr.dev/slog_test.TestLogger.func2", - Line: 67, + Line: 68, Fields: slog.M( slog.F("ctx", 1024), @@ -91,7 +92,11 @@ func TestLogger(t *testing.T) { l = l.Named("hello") l = l.Named("hello2") - ctx, span := trace.StartSpan(bg, "trace") + tp := sdktrace.NewTracerProvider() + tracer := tp.Tracer("tracer") + ctx, span := tracer.Start(bg, "trace") + span.End() + _ = tp.Shutdown(bg) ctx = slog.With(ctx, slog.F("ctx", io.EOF)) l = l.With(slog.F("with", 2)) @@ -108,7 +113,7 @@ func TestLogger(t *testing.T) { File: slogTestFile, Func: "cdr.dev/slog_test.TestLogger.func3", - Line: 98, + Line: 103, SpanContext: span.SpanContext(), @@ -149,6 +154,36 @@ func TestLogger(t *testing.T) { assert.Equal(t, "level", slog.LevelFatal, s.entries[5].Level) assert.Equal(t, "exits", 1, exits) }) + + t.Run("kv", func(t *testing.T) { + s := &fakeSink{} + l := slog.Make(s) + + // All of these formats should be equivalent. + formats := [][]any{ + {"animal", "cat", "weight", 15}, + {slog.F("animal", "cat"), "weight", 15}, + {slog.M( + slog.F("animal", "cat"), + slog.F("weight", 15), + )}, + {slog.F("animal", "cat"), slog.F("weight", 15)}, + } + + for _, format := range formats { + l.Info(bg, "msg", format...) + } + + assert.Len(t, "entries", 4, s.entries) + + for i := range s.entries { + assert.Equal( + t, fmt.Sprintf("%v", i), + s.entries[0].Fields, + s.entries[i].Fields, + ) + } + }) } func TestLevel_String(t *testing.T) { diff --git a/sloggers/sloghuman/sloghuman.go b/sloggers/sloghuman/sloghuman.go index b872c74..5247d17 100644 --- a/sloggers/sloghuman/sloghuman.go +++ b/sloggers/sloghuman/sloghuman.go @@ -3,9 +3,11 @@ package sloghuman // import "cdr.dev/slog/sloggers/sloghuman" import ( + "bufio" + "bytes" "context" "io" - "strings" + "sync" "cdr.dev/slog" "cdr.dev/slog/internal/entryhuman" @@ -29,24 +31,40 @@ type humanSink struct { w2 io.Writer } +var bufPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 256)) + }, +} + func (s humanSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { - str := entryhuman.Fmt(s.w2, ent) - lines := strings.Split(str, "\n") + buf1 := bufPool.Get().(*bytes.Buffer) + buf1.Reset() + defer bufPool.Put(buf1) + + buf2 := bufPool.Get().(*bytes.Buffer) + buf2.Reset() + defer bufPool.Put(buf2) + + entryhuman.Fmt(buf1, s.w2, ent) + + var ( + i int + sc = bufio.NewScanner(buf1) + ) // We need to add 4 spaces before every field line for readability. // humanfmt doesn't do it for us because the testSink doesn't want // it as *testing.T automatically does it. - fieldsLines := lines[1:] - for i, line := range fieldsLines { - if line == "" { - continue + for ; sc.Scan(); i++ { + if i > 0 && len(sc.Bytes()) > 0 { + buf2.Write([]byte(" ")) } - fieldsLines[i] = strings.Repeat(" ", 2) + line + buf2.Write(sc.Bytes()) + buf2.WriteByte('\n') } - str = strings.Join(lines, "\n") - - s.w.Write("sloghuman", []byte(str+"\n")) + s.w.Write("sloghuman", buf2.Bytes()) } func (s humanSink) Sync() { diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 1c26631..9047161 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -3,6 +3,8 @@ package sloghuman_test import ( "bytes" "context" + "fmt" + "os" "testing" "cdr.dev/slog" @@ -24,5 +26,20 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t\tTestMake\t...\t{\"wowow\": \"me\\nyou\"}\n \"msg\": line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [info] ... wowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) +} + +func TestVisual(t *testing.T) { + t.Setenv("FORCE_COLOR", "true") + if os.Getenv("TEST_VISUAL") == "" { + t.Skip("TEST_VISUAL not set") + } + + l := slog.Make(sloghuman.Sink(os.Stdout)).Leveled(slog.LevelDebug) + l.Debug(bg, "small potatos", slog.F("aaa", "mmm"), slog.F("bbb", "nnn"), slog.F("age", 24)) + l.Info(bg, "line1\n\nline2", slog.F("wowow", "me\nyou")) + l.Warn(bg, "oops", slog.F("aaa", "mmm")) + l = l.Named("sublogger") + l.Error(bg, "big oops", slog.F("aaa", "mmm"), slog.Error(fmt.Errorf("this happened\nand this"))) + l.Sync() } diff --git a/sloggers/slogjson/slogjson.go b/sloggers/slogjson/slogjson.go index 5a5d4ae..d38ee89 100644 --- a/sloggers/slogjson/slogjson.go +++ b/sloggers/slogjson/slogjson.go @@ -23,8 +23,6 @@ import ( "fmt" "io" - "go.opencensus.io/trace" - "cdr.dev/slog" "cdr.dev/slog/internal/syncwriter" ) @@ -57,10 +55,10 @@ func (s jsonSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { m = append(m, slog.F("logger_names", ent.LoggerNames)) } - if ent.SpanContext != (trace.SpanContext{}) { + if ent.SpanContext.IsValid() { m = append(m, - slog.F("trace", ent.SpanContext.TraceID), - slog.F("span", ent.SpanContext.SpanID), + slog.F("trace", ent.SpanContext.TraceID()), + slog.F("span", ent.SpanContext.SpanID()), ) } diff --git a/sloggers/slogjson/slogjson_test.go b/sloggers/slogjson/slogjson_test.go index 46f8590..cdbffee 100644 --- a/sloggers/slogjson/slogjson_test.go +++ b/sloggers/slogjson/slogjson_test.go @@ -3,11 +3,12 @@ package slogjson_test import ( "bytes" "context" + "database/sql" "fmt" "runtime" "testing" - "go.opencensus.io/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "cdr.dev/slog" "cdr.dev/slog/internal/assert" @@ -22,14 +23,44 @@ var bg = context.Background() func TestMake(t *testing.T) { t.Parallel() - ctx, s := trace.StartSpan(bg, "meow") + tp := sdktrace.NewTracerProvider() + tracer := tp.Tracer("tracer") + ctx, span := tracer.Start(bg, "trace") + span.End() + _ = tp.Shutdown(bg) b := &bytes.Buffer{} l := slog.Make(slogjson.Sink(b)) l = l.Named("named") l.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) j := entryjson.Filter(b.String(), "ts") - exp := fmt.Sprintf(`{"level":"ERROR","msg":"line1\n\nline2","caller":"%v:29","func":"cdr.dev/slog/sloggers/slogjson_test.TestMake","logger_names":["named"],"trace":"%v","span":"%v","fields":{"wowow":"me\nyou"}} -`, slogjsonTestFile, s.SpanContext().TraceID, s.SpanContext().SpanID) + exp := fmt.Sprintf(`{"level":"ERROR","msg":"line1\n\nline2","caller":"%v:34","func":"cdr.dev/slog/sloggers/slogjson_test.TestMake","logger_names":["named"],"trace":"%v","span":"%v","fields":{"wowow":"me\nyou"}} +`, slogjsonTestFile, span.SpanContext().TraceID().String(), span.SpanContext().SpanID().String()) + assert.Equal(t, "entry", exp, j) +} + +func TestNoDriverValue(t *testing.T) { + t.Parallel() + + b := &bytes.Buffer{} + l := slog.Make(slogjson.Sink(b)) + l = l.Named("named") + validField := sql.NullString{ + String: "cat", + Valid: true, + } + invalidField := sql.NullString{ + String: "dog", + Valid: false, + } + validInt := sql.NullInt64{ + Int64: 42, + Valid: true, + } + l.Error(bg, "error!", slog.F("inval", invalidField), slog.F("val", validField), slog.F("int", validInt)) + + j := entryjson.Filter(b.String(), "ts") + exp := fmt.Sprintf(`{"level":"ERROR","msg":"error!","caller":"%v:60","func":"cdr.dev/slog/sloggers/slogjson_test.TestNoDriverValue","logger_names":["named"],"fields":{"inval":null,"val":"cat","int":42}} +`, slogjsonTestFile) assert.Equal(t, "entry", exp, j) } diff --git a/sloggers/slogstackdriver/slogstackdriver.go b/sloggers/slogstackdriver/slogstackdriver.go index 9223c26..9772ed4 100644 --- a/sloggers/slogstackdriver/slogstackdriver.go +++ b/sloggers/slogstackdriver/slogstackdriver.go @@ -11,9 +11,9 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "go.opencensus.io/trace" + "cloud.google.com/go/logging/apiv2/loggingpb" + "go.opentelemetry.io/otel/trace" logpbtype "google.golang.org/genproto/googleapis/logging/type" - logpb "google.golang.org/genproto/googleapis/logging/v2" "cdr.dev/slog" "cdr.dev/slog/internal/syncwriter" @@ -29,10 +29,14 @@ func Sink(w io.Writer) slog.Sink { // // We use a very short timeout because the metadata server should be // within the same datacenter as the cloud instance. - client := metadata.NewClient(&http.Client{ - Timeout: time.Second * 3, - }) + tp := http.DefaultTransport.(*http.Transport).Clone() + httpClient := &http.Client{ + Timeout: time.Second * 3, + Transport: tp, + } + client := metadata.NewClient(httpClient) projectID, _ := client.ProjectID() + httpClient.CloseIdleConnections() return stackdriverSink{ projectID: projectID, @@ -52,11 +56,12 @@ func (s stackdriverSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { // https://cloud.google.com/stackdriver/docs/solutions/agents/ops-agent/configuration#special-fields e := slog.M( slog.F("logging.googleapis.com/severity", sev(ent.Level)), + slog.F("severity", sev(ent.Level)), slog.F("message", ent.Message), // Unfortunately, both of these fields are required. slog.F("timestampSeconds", ent.Time.Unix()), slog.F("timestampNanos", ent.Time.UnixNano()%1e9), - slog.F("logging.googleapis.com/sourceLocation", &logpb.LogEntrySourceLocation{ + slog.F("logging.googleapis.com/sourceLocation", &loggingpb.LogEntrySourceLocation{ File: ent.File, Line: int64(ent.Line), Function: ent.Func, @@ -64,15 +69,15 @@ func (s stackdriverSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { ) if len(ent.LoggerNames) > 0 { - e = append(e, slog.F("logging.googleapis.com/operation", &logpb.LogEntryOperation{ + e = append(e, slog.F("logging.googleapis.com/operation", &loggingpb.LogEntryOperation{ Producer: strings.Join(ent.LoggerNames, "."), })) } - if ent.SpanContext != (trace.SpanContext{}) { + if ent.SpanContext.IsValid() { e = append(e, - slog.F("logging.googleapis.com/trace", s.traceField(ent.SpanContext.TraceID)), - slog.F("logging.googleapis.com/spanId", ent.SpanContext.SpanID.String()), + slog.F("logging.googleapis.com/trace", s.traceField(ent.SpanContext.TraceID())), + slog.F("logging.googleapis.com/spanId", ent.SpanContext.SpanID().String()), slog.F("logging.googleapis.com/trace_sampled", ent.SpanContext.IsSampled()), ) } diff --git a/sloggers/slogstackdriver/slogstackdriver_test.go b/sloggers/slogstackdriver/slogstackdriver_test.go index 11a0934..7a79985 100644 --- a/sloggers/slogstackdriver/slogstackdriver_test.go +++ b/sloggers/slogstackdriver/slogstackdriver_test.go @@ -4,13 +4,15 @@ import ( "bytes" "context" "fmt" + "net/http" "runtime" "testing" + "time" "go.uber.org/goleak" "cloud.google.com/go/compute/metadata" - "go.opencensus.io/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" logpbtype "google.golang.org/genproto/googleapis/logging/type" "cdr.dev/slog" @@ -27,18 +29,22 @@ var ( func TestStackdriver(t *testing.T) { t.Parallel() - ctx, s := trace.StartSpan(bg, "meow") + tp := sdktrace.NewTracerProvider() + tracer := tp.Tracer("tracer") + ctx, span := tracer.Start(bg, "trace") + span.End() + _ = tp.Shutdown(bg) b := &bytes.Buffer{} l := slog.Make(slogstackdriver.Sink(b)) l = l.Named("meow") l.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) - projectID, _ := metadata.ProjectID() + projectID, _ := metadataClient(t).ProjectID() j := entryjson.Filter(b.String(), "timestampSeconds") j = entryjson.Filter(j, "timestampNanos") - exp := fmt.Sprintf(`{"logging.googleapis.com/severity":"ERROR","message":"line1\n\nline2","logging.googleapis.com/sourceLocation":{"file":"%v","line":34,"function":"cdr.dev/slog/sloggers/slogstackdriver_test.TestStackdriver"},"logging.googleapis.com/operation":{"producer":"meow"},"logging.googleapis.com/trace":"projects/%v/traces/%v","logging.googleapis.com/spanId":"%v","logging.googleapis.com/trace_sampled":false,"wowow":"me\nyou"} -`, slogstackdriverTestFile, projectID, s.SpanContext().TraceID, s.SpanContext().SpanID) + exp := fmt.Sprintf(`{"logging.googleapis.com/severity":"ERROR","severity":"ERROR","message":"line1\n\nline2","logging.googleapis.com/sourceLocation":{"file":"%v","line":40,"function":"cdr.dev/slog/sloggers/slogstackdriver_test.TestStackdriver"},"logging.googleapis.com/operation":{"producer":"meow"},"logging.googleapis.com/trace":"projects/%v/traces/%v","logging.googleapis.com/spanId":"%v","logging.googleapis.com/trace_sampled":%v,"wowow":"me\nyou"} +`, slogstackdriverTestFile, projectID, span.SpanContext().TraceID(), span.SpanContext().SpanID(), span.SpanContext().IsSampled()) assert.Equal(t, "entry", exp, j) } @@ -55,3 +61,19 @@ func TestSevMapping(t *testing.T) { func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } + +func metadataClient(t testing.TB) *metadata.Client { + // When not running in Google Cloud, the default metadata client will + // leak a goroutine. + // + // We use a very short timeout because the metadata server should be + // within the same datacenter as the cloud instance. + tp := http.DefaultTransport.(*http.Transport).Clone() + httpClient := &http.Client{ + Timeout: time.Second * 3, + Transport: tp, + } + client := metadata.NewClient(httpClient) + t.Cleanup(httpClient.CloseIdleConnections) + return client +} diff --git a/sloggers/slogtest/assert/assert.go b/sloggers/slogtest/assert/assert.go index e11476f..1e3c456 100644 --- a/sloggers/slogtest/assert/assert.go +++ b/sloggers/slogtest/assert/assert.go @@ -89,5 +89,4 @@ func stringContainsFold(errs, sub string) bool { sub = strings.ToLower(sub) return strings.Contains(errs, sub) - } diff --git a/sloggers/slogtest/assert/assert_test.go b/sloggers/slogtest/assert/assert_test.go index 49f6c02..b483cf3 100644 --- a/sloggers/slogtest/assert/assert_test.go +++ b/sloggers/slogtest/assert/assert_test.go @@ -44,11 +44,10 @@ func TestErrorContains(t *testing.T) { defer func() { recover() simpleassert.Equal(t, "fatals", 1, tb.fatals) - }() assert.ErrorContains(tb, "meow", io.ErrClosedPipe, "eof") - } + func TestSuccess(t *testing.T) { t.Parallel() diff --git a/sloggers/slogtest/t.go b/sloggers/slogtest/t.go index 646a03d..b1fbc86 100644 --- a/sloggers/slogtest/t.go +++ b/sloggers/slogtest/t.go @@ -7,11 +7,15 @@ package slogtest // import "cdr.dev/slog/sloggers/slogtest" import ( "context" + "fmt" "log" "os" + "strings" "sync" "testing" + "golang.org/x/xerrors" + "cdr.dev/slog" "cdr.dev/slog/internal/entryhuman" "cdr.dev/slog/sloggers/sloghuman" @@ -34,13 +38,33 @@ type Options struct { // conditions exist when t.Log is called concurrently of a test exiting. Set // to true if you don't need this behavior. SkipCleanup bool + // IgnoredErrorIs causes the test logger not to error the test on Error + // if the SinkEntry contains one of the listed errors in its "error" Field. + // Errors are matched using xerrors.Is(). + // + // By default, context.Canceled and context.DeadlineExceeded are included, + // as these are nearly always benign in testing. Override to []error{} (zero + // length error slice) to disable the whitelist entirely. + IgnoredErrorIs []error + // IgnoreErrorFn, if non-nil, defines a function that should return true if + // the given SinkEntry should not error the test on Error or Critical. The + // result of this function is logically ORed with ignore directives defined + // by IgnoreErrors and IgnoredErrorIs. To depend exclusively on + // IgnoreErrorFn, set IgnoreErrors=false and IgnoredErrorIs=[]error{} (zero + // length error slice). + IgnoreErrorFn func(slog.SinkEntry) bool } -// Make creates a Logger that writes logs to tb in a human readable format. +var DefaultIgnoredErrorIs = []error{context.Canceled, context.DeadlineExceeded} + +// Make creates a Logger that writes logs to tb in a human-readable format. func Make(tb testing.TB, opts *Options) slog.Logger { if opts == nil { opts = &Options{} } + if opts.IgnoredErrorIs == nil { + opts.IgnoredErrorIs = DefaultIgnoredErrorIs + } sink := &testSink{ tb: tb, @@ -64,7 +88,7 @@ type testSink struct { testDone bool } -func (ts *testSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { +func (ts *testSink) LogEntry(_ context.Context, ent slog.SinkEntry) { ts.mu.RLock() defer ts.mu.RUnlock() @@ -73,23 +97,46 @@ func (ts *testSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { return } + var sb strings.Builder // The testing package logs to stdout and not stderr. - s := entryhuman.Fmt(os.Stdout, ent) + entryhuman.Fmt(&sb, os.Stdout, ent) switch ent.Level { case slog.LevelDebug, slog.LevelInfo, slog.LevelWarn: - ts.tb.Log(s) + ts.tb.Log(sb.String()) case slog.LevelError, slog.LevelCritical: - if ts.opts.IgnoreErrors { - ts.tb.Log(s) + if ts.shouldIgnoreError(ent) { + ts.tb.Log(sb.String()) } else { - ts.tb.Error(s) + sb.WriteString(fmt.Sprintf( + "\n *** slogtest: log detected at level %s; TEST FAILURE ***", + ent.Level, + )) + ts.tb.Error(sb.String()) } case slog.LevelFatal: - ts.tb.Fatal(s) + sb.WriteString("\n *** slogtest: FATAL log detected; TEST FAILURE ***") + ts.tb.Fatal(sb.String()) } } +func (ts *testSink) shouldIgnoreError(ent slog.SinkEntry) bool { + if ts.opts.IgnoreErrors { + return true + } + if err, ok := FindFirstError(ent); ok { + for _, ig := range ts.opts.IgnoredErrorIs { + if xerrors.Is(err, ig) { + return true + } + } + } + if ts.opts.IgnoreErrorFn != nil { + return ts.opts.IgnoreErrorFn(ent) + } + return false +} + func (ts *testSink) Sync() {} var ctx = context.Background() @@ -99,25 +146,38 @@ func l(t testing.TB) slog.Logger { } // Debug logs the given msg and fields to t via t.Log at the debug level. -func Debug(t testing.TB, msg string, fields ...slog.Field) { +func Debug(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Debug(ctx, msg, fields...) } // Info logs the given msg and fields to t via t.Log at the info level. -func Info(t testing.TB, msg string, fields ...slog.Field) { +func Info(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Info(ctx, msg, fields...) } // Error logs the given msg and fields to t via t.Error at the error level. -func Error(t testing.TB, msg string, fields ...slog.Field) { +func Error(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Error(ctx, msg, fields...) } // Fatal logs the given msg and fields to t via t.Fatal at the fatal level. -func Fatal(t testing.TB, msg string, fields ...slog.Field) { +func Fatal(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Fatal(ctx, msg, fields...) } + +// FindFirstError finds the first slog.Field named "error" that contains an +// error value. +func FindFirstError(ent slog.SinkEntry) (err error, ok bool) { + for _, f := range ent.Fields { + if f.Name == "error" { + if err, ok = f.Value.(error); ok { + return err, true + } + } + } + return nil, false +} diff --git a/sloggers/slogtest/t_test.go b/sloggers/slogtest/t_test.go index d55fa88..2aa09d5 100644 --- a/sloggers/slogtest/t_test.go +++ b/sloggers/slogtest/t_test.go @@ -2,8 +2,12 @@ package slogtest_test import ( "context" + "fmt" "testing" + "golang.org/x/xerrors" + + "cdr.dev/slog" "cdr.dev/slog/internal/assert" "cdr.dev/slog/sloggers/slogtest" ) @@ -15,6 +19,12 @@ func TestStateless(t *testing.T) { slogtest.Debug(tb, "hello") slogtest.Info(tb, "hello") + slogtest.Error(tb, "canceled", slog.Error(xerrors.Errorf("test %w:", context.Canceled))) + assert.Equal(t, "errors", 0, tb.errors) + + slogtest.Error(tb, "deadline", slog.Error(xerrors.Errorf("test %w:", context.DeadlineExceeded))) + assert.Equal(t, "errors", 0, tb.errors) + slogtest.Error(tb, "hello") assert.Equal(t, "errors", 1, tb.errors) @@ -45,6 +55,100 @@ func TestIgnoreErrors(t *testing.T) { l.Fatal(bg, "hello") } +func TestIgnoreErrorIs_Default(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + l := slogtest.Make(tb, nil) + + l.Error(bg, "canceled", slog.Error(xerrors.Errorf("test %w:", context.Canceled))) + assert.Equal(t, "errors", 0, tb.errors) + + l.Error(bg, "deadline", slog.Error(xerrors.Errorf("test %w:", context.DeadlineExceeded))) + assert.Equal(t, "errors", 0, tb.errors) + + l.Error(bg, "new", slog.Error(xerrors.New("test"))) + assert.Equal(t, "errors", 1, tb.errors) + + defer func() { + recover() + assert.Equal(t, "fatals", 1, tb.fatals) + }() + + l.Fatal(bg, "hello", slog.Error(xerrors.Errorf("fatal %w:", context.Canceled))) +} + +func TestIgnoreErrorIs_Explicit(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + ignored := xerrors.New("ignored") + notIgnored := xerrors.New("not ignored") + l := slogtest.Make(tb, &slogtest.Options{IgnoredErrorIs: []error{ignored}}) + + l.Error(bg, "ignored", slog.Error(xerrors.Errorf("test %w:", ignored))) + assert.Equal(t, "errors", 0, tb.errors) + + l.Error(bg, "not ignored", slog.Error(xerrors.Errorf("test %w:", notIgnored))) + assert.Equal(t, "errors", 1, tb.errors) + + l.Error(bg, "canceled", slog.Error(xerrors.Errorf("test %w:", context.Canceled))) + assert.Equal(t, "errors", 2, tb.errors) + + l.Error(bg, "deadline", slog.Error(xerrors.Errorf("test %w:", context.DeadlineExceeded))) + assert.Equal(t, "errors", 3, tb.errors) + + l.Error(bg, "new", slog.Error(xerrors.New("test"))) + assert.Equal(t, "errors", 4, tb.errors) + + defer func() { + recover() + assert.Equal(t, "fatals", 1, tb.fatals) + }() + + l.Fatal(bg, "hello", slog.Error(xerrors.Errorf("test %w:", ignored))) +} + +func TestIgnoreErrorFn(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + ignored := testCodedError{code: 777} + notIgnored := testCodedError{code: 911} + l := slogtest.Make(tb, &slogtest.Options{IgnoreErrorFn: func(ent slog.SinkEntry) bool { + err, ok := slogtest.FindFirstError(ent) + if !ok { + t.Error("did not contain an error") + return false + } + ce := testCodedError{} + if !xerrors.As(err, &ce) { + return false + } + return ce.code != 911 + }}) + + l.Error(bg, "ignored", slog.Error(xerrors.Errorf("test %w:", ignored))) + assert.Equal(t, "errors", 0, tb.errors) + + l.Error(bg, "not ignored", slog.Error(xerrors.Errorf("test %w:", notIgnored))) + assert.Equal(t, "errors", 1, tb.errors) + + // still ignored by default for IgnoredErrorIs + l.Error(bg, "canceled", slog.Error(xerrors.Errorf("test %w:", context.Canceled))) + assert.Equal(t, "errors", 1, tb.errors) + + l.Error(bg, "new", slog.Error(xerrors.New("test"))) + assert.Equal(t, "errors", 2, tb.errors) + + defer func() { + recover() + assert.Equal(t, "fatals", 1, tb.fatals) + }() + + l.Fatal(bg, "hello", slog.Error(xerrors.Errorf("test %w:", ignored))) +} + func TestCleanup(t *testing.T) { t.Parallel() @@ -55,7 +159,7 @@ func TestCleanup(t *testing.T) { fn() } - // This shoud not log since the logger was cleaned up. + // This should not log since the logger was cleaned up. l.Info(bg, "hello") assert.Equal(t, "no logs", 0, tb.logs) } @@ -100,3 +204,11 @@ func (tb *fakeTB) Fatal(v ...interface{}) { func (tb *fakeTB) Cleanup(fn func()) { tb.cleanups = append(tb.cleanups, fn) } + +type testCodedError struct { + code int +} + +func (e testCodedError) Error() string { + return fmt.Sprintf("code: %d", e.code) +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy