diff --git a/.github/workflows/fossa.yaml b/.github/workflows/fossa.yaml new file mode 100644 index 000000000..86e6db7d0 --- /dev/null +++ b/.github/workflows/fossa.yaml @@ -0,0 +1,17 @@ +name: FOSSA Analysis +on: push + +jobs: + + build: + runs-on: ubuntu-latest + if: github.repository_owner == 'uber-go' + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: FOSSA analysis + uses: fossas/fossa-action@v1 + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..d174d4129 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,52 @@ +name: Go + +on: + push: + branches: ['*'] + tags: ['v*'] + pull_request: + branches: ['*'] + +jobs: + + build: + runs-on: ubuntu-latest + strategy: + matrix: + go: ["1.15.x", "1.16.x"] + include: + - go: 1.16.x + latest: true + + steps: + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Load cached dependencies + uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download Dependencies + run: go mod download + + - name: Lint + if: matrix.latest + run: make lint + + - name: Test + run: make cover + + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@v1 + + - name: Benchmark + run: make bench diff --git a/.gitignore b/.gitignore index 08fbde6ce..da9d9d00b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ _testmain.go *.pprof *.out *.log + +/bin +cover.out +cover.html diff --git a/.readme.tmpl b/.readme.tmpl index c6440db8e..3154a1e64 100644 --- a/.readme.tmpl +++ b/.readme.tmpl @@ -100,9 +100,10 @@ pinned in zap's [glide.lock][] file. [↩](#anchor-versions) [doc-img]: https://godoc.org/go.uber.org/zap?status.svg [doc]: https://godoc.org/go.uber.org/zap -[ci-img]: https://travis-ci.org/uber-go/zap.svg?branch=master -[ci]: https://travis-ci.org/uber-go/zap +[ci-img]: https://travis-ci.com/uber-go/zap.svg?branch=master +[ci]: https://travis-ci.com/uber-go/zap [cov-img]: https://codecov.io/gh/uber-go/zap/branch/master/graph/badge.svg [cov]: https://codecov.io/gh/uber-go/zap [benchmarking suite]: https://github.com/uber-go/zap/tree/master/benchmarks [glide.lock]: https://github.com/uber-go/zap/blob/master/glide.lock + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d586200e4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: go -sudo: false -go: - - 1.10.x - - 1.11.x -go_import_path: go.uber.org/zap -env: - global: - - TEST_TIMEOUT_SCALE=10 -cache: - directories: - - vendor -install: - - make dependencies -script: - - make lint - - make test - - make bench -after_success: - - make cover - - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d5b49f3..3b99bf0ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,121 @@ # Changelog +## 1.17.0 (25 May 2021) + +Bugfixes: +* [#867][]: Encode `` for nil `error` instead of a panic. +* [#931][], [#936][]: Update minimum version constraints to address + vulnerabilities in dependencies. + +Enhancements: +* [#865][]: Improve alignment of fields of the Logger struct, reducing its + size from 96 to 80 bytes. +* [#881][]: Support `grpclog.LoggerV2` in zapgrpc. +* [#903][]: Support URL-encoded POST requests to the AtomicLevel HTTP handler + with the `application/x-www-form-urlencoded` content type. +* [#912][]: Support multi-field encoding with `zap.Inline`. +* [#913][]: Speed up SugaredLogger for calls with a single string. +* [#928][]: Add support for filtering by field name to `zaptest/observer`. + +Thanks to @ash2k, @FMLS, @jimmystewpot, @Oncilla, @tsoslow, @tylitianrui, @withshubh, and @wziww for their contributions to this release. + +## 1.16.0 (1 Sep 2020) + +Bugfixes: +* [#828][]: Fix missing newline in IncreaseLevel error messages. +* [#835][]: Fix panic in JSON encoder when encoding times or durations + without specifying a time or duration encoder. +* [#843][]: Honor CallerSkip when taking stack traces. +* [#862][]: Fix the default file permissions to use `0666` and rely on the umask instead. +* [#854][]: Encode `` for nil `Stringer` instead of a panic error log. + +Enhancements: +* [#629][]: Added `zapcore.TimeEncoderOfLayout` to easily create time encoders + for custom layouts. +* [#697][]: Added support for a configurable delimiter in the console encoder. +* [#852][]: Optimize console encoder by pooling the underlying JSON encoder. +* [#844][]: Add ability to include the calling function as part of logs. +* [#843][]: Add `StackSkip` for including truncated stacks as a field. +* [#861][]: Add options to customize Fatal behaviour for better testability. + +Thanks to @SteelPhase, @tmshn, @lixingwang, @wyxloading, @moul, @segevfiner, @andy-retailnext and @jcorbin for their contributions to this release. + +## 1.15.0 (23 Apr 2020) + +Bugfixes: +* [#804][]: Fix handling of `Time` values out of `UnixNano` range. +* [#812][]: Fix `IncreaseLevel` being reset after a call to `With`. + +Enhancements: +* [#806][]: Add `WithCaller` option to supersede the `AddCaller` option. This + allows disabling annotation of log entries with caller information if + previously enabled with `AddCaller`. +* [#813][]: Deprecate `NewSampler` constructor in favor of + `NewSamplerWithOptions` which supports a `SamplerHook` option. This option + adds support for monitoring sampling decisions through a hook. + +Thanks to @danielbprice for their contributions to this release. + +## 1.14.1 (14 Mar 2020) + +Bugfixes: +* [#791][]: Fix panic on attempting to build a logger with an invalid Config. +* [#795][]: Vendoring Zap with `go mod vendor` no longer includes Zap's + development-time dependencies. +* [#799][]: Fix issue introduced in 1.14.0 that caused invalid JSON output to + be generated for arrays of `time.Time` objects when using string-based time + formats. + +Thanks to @YashishDua for their contributions to this release. + +## 1.14.0 (20 Feb 2020) + +Enhancements: +* [#771][]: Optimize calls for disabled log levels. +* [#773][]: Add millisecond duration encoder. +* [#775][]: Add option to increase the level of a logger. +* [#786][]: Optimize time formatters using `Time.AppendFormat` where possible. + +Thanks to @caibirdme for their contributions to this release. + +## 1.13.0 (13 Nov 2019) + +Enhancements: +* [#758][]: Add `Intp`, `Stringp`, and other similar `*p` field constructors + to log pointers to primitives with support for `nil` values. + +Thanks to @jbizzle for their contributions to this release. + +## 1.12.0 (29 Oct 2019) + +Enhancements: +* [#751][]: Migrate to Go modules. + +## 1.11.0 (21 Oct 2019) + +Enhancements: +* [#725][]: Add `zapcore.OmitKey` to omit keys in an `EncoderConfig`. +* [#736][]: Add `RFC3339` and `RFC3339Nano` time encoders. + +Thanks to @juicemia, @uhthomas for their contributions to this release. + +## 1.10.0 (29 Apr 2019) + +Bugfixes: +* [#657][]: Fix `MapObjectEncoder.AppendByteString` not adding value as a + string. +* [#706][]: Fix incorrect call depth to determine caller in Go 1.12. + +Enhancements: +* [#610][]: Add `zaptest.WrapOptions` to wrap `zap.Option` for creating test + loggers. +* [#675][]: Don't panic when encoding a String field. +* [#704][]: Disable HTML escaping for JSON objects encoded using the + reflect-based encoder. + +Thanks to @iaroslav-ciupin, @lelenanam, @joa, @NWilson for their contributions +to this release. + ## v1.9.1 (06 Aug 2018) Bugfixes: @@ -303,3 +419,42 @@ upgrade to the upcoming stable release. [#572]: https://github.com/uber-go/zap/pull/572 [#606]: https://github.com/uber-go/zap/pull/606 [#614]: https://github.com/uber-go/zap/pull/614 +[#657]: https://github.com/uber-go/zap/pull/657 +[#706]: https://github.com/uber-go/zap/pull/706 +[#610]: https://github.com/uber-go/zap/pull/610 +[#675]: https://github.com/uber-go/zap/pull/675 +[#704]: https://github.com/uber-go/zap/pull/704 +[#725]: https://github.com/uber-go/zap/pull/725 +[#736]: https://github.com/uber-go/zap/pull/736 +[#751]: https://github.com/uber-go/zap/pull/751 +[#758]: https://github.com/uber-go/zap/pull/758 +[#771]: https://github.com/uber-go/zap/pull/771 +[#773]: https://github.com/uber-go/zap/pull/773 +[#775]: https://github.com/uber-go/zap/pull/775 +[#786]: https://github.com/uber-go/zap/pull/786 +[#791]: https://github.com/uber-go/zap/pull/791 +[#795]: https://github.com/uber-go/zap/pull/795 +[#799]: https://github.com/uber-go/zap/pull/799 +[#804]: https://github.com/uber-go/zap/pull/804 +[#812]: https://github.com/uber-go/zap/pull/812 +[#806]: https://github.com/uber-go/zap/pull/806 +[#813]: https://github.com/uber-go/zap/pull/813 +[#629]: https://github.com/uber-go/zap/pull/629 +[#697]: https://github.com/uber-go/zap/pull/697 +[#828]: https://github.com/uber-go/zap/pull/828 +[#835]: https://github.com/uber-go/zap/pull/835 +[#843]: https://github.com/uber-go/zap/pull/843 +[#844]: https://github.com/uber-go/zap/pull/844 +[#852]: https://github.com/uber-go/zap/pull/852 +[#854]: https://github.com/uber-go/zap/pull/854 +[#861]: https://github.com/uber-go/zap/pull/861 +[#862]: https://github.com/uber-go/zap/pull/862 +[#865]: https://github.com/uber-go/zap/pull/865 +[#867]: https://github.com/uber-go/zap/pull/867 +[#881]: https://github.com/uber-go/zap/pull/881 +[#903]: https://github.com/uber-go/zap/pull/903 +[#912]: https://github.com/uber-go/zap/pull/912 +[#913]: https://github.com/uber-go/zap/pull/913 +[#928]: https://github.com/uber-go/zap/pull/928 +[#931]: https://github.com/uber-go/zap/pull/931 +[#936]: https://github.com/uber-go/zap/pull/936 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9454bbaf0..5cd965687 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,12 +25,6 @@ git remote add upstream https://github.com/uber-go/zap.git git fetch upstream ``` -Install zap's dependencies: - -``` -make dependencies -``` - Make sure that the tests and the linters pass: ``` diff --git a/FAQ.md b/FAQ.md index 4256d35c7..b183b20bc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -27,6 +27,13 @@ abstraction, and it lets us add methods without introducing breaking changes. Your applications should define and depend upon an interface that includes just the methods you use. +### Why are some of my logs missing? + +Logs are dropped intentionally by zap when sampling is enabled. The production +configuration (as returned by `NewProductionConfig()` enables sampling which will +cause repeated logs within a second to be sampled. See more details on why sampling +is enabled in [Why sample application logs](https://github.com/uber-go/zap/blob/master/FAQ.md#why-sample-application-logs). + ### Why sample application logs? Applications often experience runs of errors, either because of a bug or @@ -149,6 +156,8 @@ We're aware of the following extensions, but haven't used them ourselves: | `github.com/tchap/zapext` | Sentry, syslog | | `github.com/fgrosse/zaptest` | Ginkgo | | `github.com/blendle/zapdriver` | Stackdriver | +| `github.com/moul/zapgorm` | Gorm | +| `github.com/moul/zapfilter` | Advanced filtering rules | [go-proverbs]: https://go-proverbs.github.io/ [import-path]: https://golang.org/cmd/go/#hdr-Remote_import_paths diff --git a/Makefile b/Makefile index ef7893b3b..9b1bc3b0e 100644 --- a/Makefile +++ b/Makefile @@ -1,76 +1,73 @@ -export GO15VENDOREXPERIMENT=1 +export GOBIN ?= $(shell pwd)/bin +GOLINT = $(GOBIN)/golint +STATICCHECK = $(GOBIN)/staticcheck BENCH_FLAGS ?= -cpuprofile=cpu.pprof -memprofile=mem.pprof -benchmem -PKGS ?= $(shell glide novendor) -# Many Go tools take file globs or directories as arguments instead of packages. -PKG_FILES ?= *.go zapcore benchmarks buffer zapgrpc zaptest zaptest/observer internal/bufferpool internal/exit internal/color internal/ztest -# The linting tools evolve with each Go version, so run them only on the latest -# stable release. -GO_VERSION := $(shell go version | cut -d " " -f 3) -GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION))) -LINTABLE_MINOR_VERSIONS := 10 -ifneq ($(filter $(LINTABLE_MINOR_VERSIONS),$(GO_MINOR_VERSION)),) -SHOULD_LINT := true -endif +# Directories containing independent Go modules. +# +# We track coverage only for the main module. +MODULE_DIRS = . ./benchmarks ./zapgrpc/internal/test +# Many Go tools take file globs or directories as arguments instead of packages. +GO_FILES := $(shell \ + find . '(' -path '*/.*' -o -path './vendor' ')' -prune \ + -o -name '*.go' -print | cut -b3-) .PHONY: all all: lint test -.PHONY: dependencies -dependencies: - @echo "Installing Glide and locked dependencies..." - glide --version || go get -u -f github.com/Masterminds/glide - glide install - @echo "Installing test dependencies..." - go install ./vendor/github.com/axw/gocov/gocov - go install ./vendor/github.com/mattn/goveralls -ifdef SHOULD_LINT - @echo "Installing golint..." - go install ./vendor/github.com/golang/lint/golint -else - @echo "Not installing golint, since we don't expect to lint on" $(GO_VERSION) -endif - -# Disable printf-like invocation checking due to testify.assert.Error() -VET_RULES := -printf=false - .PHONY: lint -lint: -ifdef SHOULD_LINT +lint: $(GOLINT) $(STATICCHECK) @rm -rf lint.log @echo "Checking formatting..." - @gofmt -d -s $(PKG_FILES) 2>&1 | tee lint.log - @echo "Installing test dependencies for vet..." - @go test -i $(PKGS) + @gofmt -d -s $(GO_FILES) 2>&1 | tee lint.log @echo "Checking vet..." - @$(foreach dir,$(PKG_FILES),go tool vet $(VET_RULES) $(dir) 2>&1 | tee -a lint.log;) + @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go vet ./... 2>&1) &&) true | tee -a lint.log @echo "Checking lint..." - @$(foreach dir,$(PKGS),golint $(dir) 2>&1 | tee -a lint.log;) + @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(GOLINT) ./... 2>&1) &&) true | tee -a lint.log + @echo "Checking staticcheck..." + @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log @echo "Checking for unresolved FIXMEs..." - @git grep -i fixme | grep -v -e vendor -e Makefile | tee -a lint.log + @git grep -i fixme | grep -v -e Makefile | tee -a lint.log @echo "Checking for license headers..." - @./check_license.sh | tee -a lint.log + @./checklicense.sh | tee -a lint.log @[ ! -s lint.log ] -else - @echo "Skipping linters on" $(GO_VERSION) -endif + @echo "Checking 'go mod tidy'..." + @make tidy + @if ! git diff --quiet; then \ + echo "'go mod tidy' resulted in changes or working tree is dirty:"; \ + git --no-pager diff; \ + fi + +$(GOLINT): + cd tools && go install golang.org/x/lint/golint + +$(STATICCHECK): + cd tools && go install honnef.co/go/tools/cmd/staticcheck .PHONY: test test: - go test -race $(PKGS) + @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go test -race ./...) &&) true .PHONY: cover cover: - ./scripts/cover.sh $(PKGS) + go test -race -coverprofile=cover.out -coverpkg=./... ./... + go tool cover -html=cover.out -o cover.html .PHONY: bench BENCH ?= . bench: - @$(foreach pkg,$(PKGS),go test -bench=$(BENCH) -run="^$$" $(BENCH_FLAGS) $(pkg);) + @$(foreach dir,$(MODULE_DIRS), ( \ + cd $(dir) && \ + go list ./... | xargs -n1 go test -bench=$(BENCH) -run="^$$" $(BENCH_FLAGS) \ + ) &&) true .PHONY: updatereadme updatereadme: rm -f README.md cat .readme.tmpl | go run internal/readme/readme.go > README.md + +.PHONY: tidy +tidy: + @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go mod tidy) &&) true diff --git a/README.md b/README.md index f4fd1cb44..1e64d6cff 100644 --- a/README.md +++ b/README.md @@ -64,43 +64,40 @@ id="anchor-versions">[1](#footnote-versions) Log a message and 10 fields: -| Package | Time | Objects Allocated | -| :--- | :---: | :---: | -| :zap: zap | 3131 ns/op | 5 allocs/op | -| :zap: zap (sugared) | 4173 ns/op | 21 allocs/op | -| zerolog | 16154 ns/op | 90 allocs/op | -| lion | 16341 ns/op | 111 allocs/op | -| go-kit | 17049 ns/op | 126 allocs/op | -| logrus | 23662 ns/op | 142 allocs/op | -| log15 | 36351 ns/op | 149 allocs/op | -| apex/log | 42530 ns/op | 126 allocs/op | +| Package | Time | Time % to zap | Objects Allocated | +| :------ | :--: | :-----------: | :---------------: | +| :zap: zap | 862 ns/op | +0% | 5 allocs/op +| :zap: zap (sugared) | 1250 ns/op | +45% | 11 allocs/op +| zerolog | 4021 ns/op | +366% | 76 allocs/op +| go-kit | 4542 ns/op | +427% | 105 allocs/op +| apex/log | 26785 ns/op | +3007% | 115 allocs/op +| logrus | 29501 ns/op | +3322% | 125 allocs/op +| log15 | 29906 ns/op | +3369% | 122 allocs/op Log a message with a logger that already has 10 fields of context: -| Package | Time | Objects Allocated | -| :--- | :---: | :---: | -| :zap: zap | 380 ns/op | 0 allocs/op | -| :zap: zap (sugared) | 564 ns/op | 2 allocs/op | -| zerolog | 321 ns/op | 0 allocs/op | -| lion | 7092 ns/op | 39 allocs/op | -| go-kit | 20226 ns/op | 115 allocs/op | -| logrus | 22312 ns/op | 130 allocs/op | -| log15 | 28788 ns/op | 79 allocs/op | -| apex/log | 42063 ns/op | 115 allocs/op | +| Package | Time | Time % to zap | Objects Allocated | +| :------ | :--: | :-----------: | :---------------: | +| :zap: zap | 126 ns/op | +0% | 0 allocs/op +| :zap: zap (sugared) | 187 ns/op | +48% | 2 allocs/op +| zerolog | 88 ns/op | -30% | 0 allocs/op +| go-kit | 5087 ns/op | +3937% | 103 allocs/op +| log15 | 18548 ns/op | +14621% | 73 allocs/op +| apex/log | 26012 ns/op | +20544% | 104 allocs/op +| logrus | 27236 ns/op | +21516% | 113 allocs/op Log a static string, without any context or `printf`-style templating: -| Package | Time | Objects Allocated | -| :--- | :---: | :---: | -| :zap: zap | 361 ns/op | 0 allocs/op | -| :zap: zap (sugared) | 534 ns/op | 2 allocs/op | -| zerolog | 323 ns/op | 0 allocs/op | -| standard library | 575 ns/op | 2 allocs/op | -| go-kit | 922 ns/op | 13 allocs/op | -| lion | 1413 ns/op | 10 allocs/op | -| logrus | 2291 ns/op | 27 allocs/op | -| apex/log | 3690 ns/op | 11 allocs/op | -| log15 | 5954 ns/op | 26 allocs/op | +| Package | Time | Time % to zap | Objects Allocated | +| :------ | :--: | :-----------: | :---------------: | +| :zap: zap | 118 ns/op | +0% | 0 allocs/op +| :zap: zap (sugared) | 191 ns/op | +62% | 2 allocs/op +| zerolog | 93 ns/op | -21% | 0 allocs/op +| go-kit | 280 ns/op | +137% | 11 allocs/op +| standard library | 499 ns/op | +323% | 2 allocs/op +| apex/log | 1990 ns/op | +1586% | 10 allocs/op +| logrus | 3129 ns/op | +2552% | 24 allocs/op +| log15 | 3887 ns/op | +3194% | 23 allocs/op ## Development Status: Stable @@ -124,13 +121,14 @@ Released under the [MIT License](LICENSE.txt). 1 In particular, keep in mind that we may be benchmarking against slightly older versions of other packages. Versions are -pinned in zap's [glide.lock][] file. [↩](#anchor-versions) +pinned in the [benchmarks/go.mod][] file. [↩](#anchor-versions) -[doc-img]: https://godoc.org/go.uber.org/zap?status.svg -[doc]: https://godoc.org/go.uber.org/zap -[ci-img]: https://travis-ci.org/uber-go/zap.svg?branch=master -[ci]: https://travis-ci.org/uber-go/zap +[doc-img]: https://pkg.go.dev/badge/go.uber.org/zap +[doc]: https://pkg.go.dev/go.uber.org/zap +[ci-img]: https://github.com/uber-go/zap/actions/workflows/go.yml/badge.svg +[ci]: https://github.com/uber-go/zap/actions/workflows/go.yml [cov-img]: https://codecov.io/gh/uber-go/zap/branch/master/graph/badge.svg [cov]: https://codecov.io/gh/uber-go/zap [benchmarking suite]: https://github.com/uber-go/zap/tree/master/benchmarks -[glide.lock]: https://github.com/uber-go/zap/blob/master/glide.lock +[benchmarks/go.mod]: https://github.com/uber-go/zap/blob/master/benchmarks/go.mod + diff --git a/array_test.go b/array_test.go index 4f709f09c..961cb1cf1 100644 --- a/array_test.go +++ b/array_test.go @@ -76,7 +76,7 @@ func TestArrayWrappers(t *testing.T) { {"empty uint8s", Uint8s("", []uint8{}), []interface{}{}}, {"empty uintptrs", Uintptrs("", []uintptr{}), []interface{}{}}, {"bools", Bools("", []bool{true, false}), []interface{}{true, false}}, - {"byte strings", ByteStrings("", [][]byte{{1, 2}, {3, 4}}), []interface{}{[]byte{1, 2}, []byte{3, 4}}}, + {"byte strings", ByteStrings("", [][]byte{{1, 2}, {3, 4}}), []interface{}{"\x01\x02", "\x03\x04"}}, {"complex128s", Complex128s("", []complex128{1 + 2i, 3 + 4i}), []interface{}{1 + 2i, 3 + 4i}}, {"complex64s", Complex64s("", []complex64{1 + 2i, 3 + 4i}), []interface{}{complex64(1 + 2i), complex64(3 + 4i)}}, {"durations", Durations("", []time.Duration{1, 2}), []interface{}{time.Nanosecond, 2 * time.Nanosecond}}, diff --git a/benchmarks/go.mod b/benchmarks/go.mod new file mode 100644 index 000000000..de9fc1b5a --- /dev/null +++ b/benchmarks/go.mod @@ -0,0 +1,16 @@ +module go.uber.org/zap/benchmarks + +go 1.13 + +replace go.uber.org/zap => ../ + +require ( + github.com/apex/log v1.1.1 + github.com/go-kit/kit v0.9.0 + github.com/go-stack/stack v1.8.0 // indirect + github.com/rs/zerolog v1.16.0 + github.com/sirupsen/logrus v1.4.2 + go.uber.org/multierr v1.6.0 + go.uber.org/zap v0.0.0-00010101000000-000000000000 + gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec +) diff --git a/benchmarks/go.sum b/benchmarks/go.sum new file mode 100644 index 000000000..d6faee4a7 --- /dev/null +++ b/benchmarks/go.sum @@ -0,0 +1,111 @@ +github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA= +github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= +github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/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/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.16.0 h1:AaELmZdcJHT8m6oZ5py4213cdFK8XGXkB3dFdAQ+P7Q= +github.com/rs/zerolog v1.16.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20180909124046-d0be0721c37e/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-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/benchmarks/scenario_bench_test.go b/benchmarks/scenario_bench_test.go index 4e307d215..134045a05 100644 --- a/benchmarks/scenario_bench_test.go +++ b/benchmarks/scenario_bench_test.go @@ -315,15 +315,6 @@ func BenchmarkWithoutFields(b *testing.B) { } }) }) - b.Run("go.pedge.io/lion", func(b *testing.B) { - logger := newLion() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - logger.Printf(getMessage(0)) - } - }) - }) b.Run("stdlib.Println", func(b *testing.B) { logger := log.New(ioutil.Discard, "", log.LstdFlags) b.ResetTimer() @@ -462,15 +453,6 @@ func BenchmarkAccumulatedContext(b *testing.B) { } }) }) - b.Run("go.pedge.io/lion", func(b *testing.B) { - logger := newLion().WithFields(fakeLogrusFields()) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - logger.Infof(getMessage(0)) - } - }) - }) b.Run("rs/zerolog", func(b *testing.B) { logger := fakeZerologContext(newZerolog().With()).Logger() b.ResetTimer() @@ -582,15 +564,6 @@ func BenchmarkAddingFields(b *testing.B) { } }) }) - b.Run("go.pedge.io/lion", func(b *testing.B) { - logger := newLion() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - logger.WithFields(fakeLogrusFields()).Infof(getMessage(0)) - } - }) - }) b.Run("rs/zerolog", func(b *testing.B) { logger := newZerolog() b.ResetTimer() diff --git a/benchmarks/zap_test.go b/benchmarks/zap_test.go index 92f7120ec..84784152c 100644 --- a/benchmarks/zap_test.go +++ b/benchmarks/zap_test.go @@ -116,7 +116,7 @@ func newZapLogger(lvl zapcore.Level) *zap.Logger { } func newSampledLogger(lvl zapcore.Level) *zap.Logger { - return zap.New(zapcore.NewSampler( + return zap.New(zapcore.NewSamplerWithOptions( newZapLogger(zap.DebugLevel).Core(), 100*time.Millisecond, 10, // first diff --git a/buffer/buffer.go b/buffer/buffer.go index c2e3df8d2..9e929cd98 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -23,7 +23,10 @@ // package's zero-allocation formatters. package buffer // import "go.uber.org/zap/buffer" -import "strconv" +import ( + "strconv" + "time" +) const _size = 1024 // by default, create 1 KiB buffers @@ -49,6 +52,11 @@ func (b *Buffer) AppendInt(i int64) { b.bs = strconv.AppendInt(b.bs, i, 10) } +// AppendTime appends the time formatted using the specified layout. +func (b *Buffer) AppendTime(t time.Time, layout string) { + b.bs = t.AppendFormat(b.bs, layout) +} + // AppendUint appends an unsigned integer to the underlying buffer (assuming // base 10). func (b *Buffer) AppendUint(i uint64) { diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index 8cf1a37c6..b5cd191d2 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -24,6 +24,7 @@ import ( "bytes" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -46,6 +47,7 @@ func TestBufferWrites(t *testing.T) { // Intenationally introduce some floating-point error. {"AppendFloat32", func() { buf.AppendFloat(float64(float32(3.14)), 32) }, "3.14"}, {"AppendWrite", func() { buf.Write([]byte("foo")) }, "foo"}, + {"AppendTime", func() { buf.AppendTime(time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC), time.RFC3339) }, "2000-01-02T03:04:05Z"}, {"WriteByte", func() { buf.WriteByte('v') }, "v"}, {"WriteString", func() { buf.WriteString("foo") }, "foo"}, } diff --git a/check_license.sh b/checklicense.sh similarity index 100% rename from check_license.sh rename to checklicense.sh diff --git a/clock_test.go b/clock_test.go new file mode 100644 index 000000000..5b6b9dd8e --- /dev/null +++ b/clock_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zap + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest/observer" +) + +type constantClock time.Time + +func (c constantClock) Now() time.Time { return time.Time(c) } +func (c constantClock) NewTicker(d time.Duration) *time.Ticker { + return &time.Ticker{} +} + +func TestWithClock(t *testing.T) { + date := time.Date(2077, 1, 23, 10, 15, 13, 441, time.UTC) + clock := constantClock(date) + withLogger(t, DebugLevel, []Option{WithClock(clock)}, func(log *Logger, logs *observer.ObservedLogs) { + log.Info("") + require.Equal(t, 1, logs.Len(), "Expected only one log entry to be written.") + assert.Equal(t, date, logs.All()[0].Entry.Time, "Unexpected entry time.") + }) +} diff --git a/config.go b/config.go index 6fe17d9e0..55637fb0b 100644 --- a/config.go +++ b/config.go @@ -21,6 +21,7 @@ package zap import ( + "fmt" "sort" "time" @@ -31,10 +32,14 @@ import ( // global CPU and I/O load that logging puts on your process while attempting // to preserve a representative subset of your logs. // -// Values configured here are per-second. See zapcore.NewSampler for details. +// If specified, the Sampler will invoke the Hook after each decision. +// +// Values configured here are per-second. See zapcore.NewSamplerWithOptions for +// details. type SamplingConfig struct { - Initial int `json:"initial" yaml:"initial"` - Thereafter int `json:"thereafter" yaml:"thereafter"` + Initial int `json:"initial" yaml:"initial"` + Thereafter int `json:"thereafter" yaml:"thereafter"` + Hook func(zapcore.Entry, zapcore.SamplingDecision) `json:"-" yaml:"-"` } // Config offers a declarative way to construct a logger. It doesn't do @@ -96,6 +101,7 @@ func NewProductionEncoderConfig() zapcore.EncoderConfig { LevelKey: "level", NameKey: "logger", CallerKey: "caller", + FunctionKey: zapcore.OmitKey, MessageKey: "msg", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, @@ -135,6 +141,7 @@ func NewDevelopmentEncoderConfig() zapcore.EncoderConfig { LevelKey: "L", NameKey: "N", CallerKey: "C", + FunctionKey: zapcore.OmitKey, MessageKey: "M", StacktraceKey: "S", LineEnding: zapcore.DefaultLineEnding, @@ -174,6 +181,10 @@ func (cfg Config) Build(opts ...Option) (*Logger, error) { return nil, err } + if cfg.Level == (AtomicLevel{}) { + return nil, fmt.Errorf("missing Level") + } + log := New( zapcore.NewCore(enc, sink, cfg.Level), cfg.buildOptions(errSink)..., @@ -203,9 +214,19 @@ func (cfg Config) buildOptions(errSink zapcore.WriteSyncer) []Option { opts = append(opts, AddStacktrace(stackLevel)) } - if cfg.Sampling != nil { + if scfg := cfg.Sampling; scfg != nil { opts = append(opts, WrapCore(func(core zapcore.Core) zapcore.Core { - return zapcore.NewSampler(core, time.Second, int(cfg.Sampling.Initial), int(cfg.Sampling.Thereafter)) + var samplerOpts []zapcore.SamplerOption + if scfg.Hook != nil { + samplerOpts = append(samplerOpts, zapcore.SamplerHook(scfg.Hook)) + } + return zapcore.NewSamplerWithOptions( + core, + time.Second, + cfg.Sampling.Initial, + cfg.Sampling.Thereafter, + samplerOpts..., + ) })) } diff --git a/config_test.go b/config_test.go index 7a875703b..ac098aafe 100644 --- a/config_test.go +++ b/config_test.go @@ -27,6 +27,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "go.uber.org/zap/zapcore" ) func TestConfig(t *testing.T) { @@ -50,7 +52,7 @@ func TestConfig(t *testing.T) { expectRe: "DEBUG\tzap/config_test.go:" + `\d+` + "\tdebug\t" + `{"k": "v", "z": "zz"}` + "\n" + "INFO\tzap/config_test.go:" + `\d+` + "\tinfo\t" + `{"k": "v", "z": "zz"}` + "\n" + "WARN\tzap/config_test.go:" + `\d+` + "\twarn\t" + `{"k": "v", "z": "zz"}` + "\n" + - `testing.\w+`, + `go.uber.org/zap.TestConfig.\w+`, }, } @@ -106,3 +108,106 @@ func TestConfigWithInvalidPaths(t *testing.T) { }) } } + +func TestConfigWithMissingAttributes(t *testing.T) { + tests := []struct { + desc string + cfg Config + expectErr string + }{ + { + desc: "missing level", + cfg: Config{ + Encoding: "json", + }, + expectErr: "missing Level", + }, + { + desc: "missing encoder time in encoder config", + cfg: Config{ + Level: NewAtomicLevelAt(zapcore.InfoLevel), + Encoding: "json", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "msg", + TimeKey: "ts", + }, + }, + expectErr: "missing EncodeTime in EncoderConfig", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := tt.cfg + _, err := cfg.Build() + require.Error(t, err) + assert.Equal(t, tt.expectErr, err.Error()) + }) + } +} + +func makeSamplerCountingHook() (h func(zapcore.Entry, zapcore.SamplingDecision), + dropped, sampled *atomic.Int64) { + dropped = new(atomic.Int64) + sampled = new(atomic.Int64) + h = func(_ zapcore.Entry, dec zapcore.SamplingDecision) { + if dec&zapcore.LogDropped > 0 { + dropped.Inc() + } else if dec&zapcore.LogSampled > 0 { + sampled.Inc() + } + } + return h, dropped, sampled +} + +func TestConfigWithSamplingHook(t *testing.T) { + shook, dcount, scount := makeSamplerCountingHook() + cfg := Config{ + Level: NewAtomicLevelAt(InfoLevel), + Development: false, + Sampling: &SamplingConfig{ + Initial: 100, + Thereafter: 100, + Hook: shook, + }, + Encoding: "json", + EncoderConfig: NewProductionEncoderConfig(), + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + expectRe := `{"level":"info","caller":"zap/config_test.go:\d+","msg":"info","k":"v","z":"zz"}` + "\n" + + `{"level":"warn","caller":"zap/config_test.go:\d+","msg":"warn","k":"v","z":"zz"}` + "\n" + expectDropped := 99 // 200 - 100 initial - 1 thereafter + expectSampled := 103 // 2 from initial + 100 + 1 thereafter + + temp, err := ioutil.TempFile("", "zap-prod-config-test") + require.NoError(t, err, "Failed to create temp file.") + defer func() { + err := os.Remove(temp.Name()) + if err != nil { + return + } + }() + + cfg.OutputPaths = []string{temp.Name()} + cfg.EncoderConfig.TimeKey = "" // no timestamps in tests + cfg.InitialFields = map[string]interface{}{"z": "zz", "k": "v"} + + logger, err := cfg.Build() + require.NoError(t, err, "Unexpected error constructing logger.") + + logger.Debug("debug") + logger.Info("info") + logger.Warn("warn") + + byteContents, err := ioutil.ReadAll(temp) + require.NoError(t, err, "Couldn't read log contents from temp file.") + logs := string(byteContents) + assert.Regexp(t, expectRe, logs, "Unexpected log output.") + + for i := 0; i < 200; i++ { + logger.Info("sampling") + } + assert.Equal(t, int64(expectDropped), dcount.Load()) + assert.Equal(t, int64(expectSampled), scount.Load()) +} diff --git a/encoder.go b/encoder.go index 2e9d3c341..08ed83354 100644 --- a/encoder.go +++ b/encoder.go @@ -62,6 +62,10 @@ func RegisterEncoder(name string, constructor func(zapcore.EncoderConfig) (zapco } func newEncoder(name string, encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) { + if encoderConfig.TimeKey != "" && encoderConfig.EncodeTime == nil { + return nil, fmt.Errorf("missing EncodeTime in EncoderConfig") + } + _encoderMutex.RLock() defer _encoderMutex.RUnlock() if name == "" { diff --git a/example_test.go b/example_test.go index ab5733f45..28474d0cd 100644 --- a/example_test.go +++ b/example_test.go @@ -165,6 +165,45 @@ func ExampleNamespace() { // {"level":"info","msg":"tracked some metrics","metrics":{"counter":1}} } +type addr struct { + IP string + Port int +} + +type request struct { + URL string + Listen addr + Remote addr +} + +func (a addr) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("ip", a.IP) + enc.AddInt("port", a.Port) + return nil +} + +func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("url", r.URL) + zap.Inline(r.Listen).AddTo(enc) + return enc.AddObject("remote", r.Remote) +} + +func ExampleObject() { + logger := zap.NewExample() + defer logger.Sync() + + req := &request{ + URL: "/test", + Listen: addr{"127.0.0.1", 8080}, + Remote: addr{"127.0.0.1", 31200}, + } + logger.Info("new request, in nested object", zap.Object("req", req)) + logger.Info("new request, inline", zap.Inline(req)) + // Output: + // {"level":"info","msg":"new request, in nested object","req":{"url":"/test","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}}} + // {"level":"info","msg":"new request, inline","url":"/test","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}} +} + func ExampleNewStdLog() { logger := zap.NewExample() defer logger.Sync() diff --git a/field.go b/field.go index 5130e1347..bbb745db5 100644 --- a/field.go +++ b/field.go @@ -32,12 +32,23 @@ import ( // improves the navigability of this package's API documentation. type Field = zapcore.Field +var ( + _minTimeInt64 = time.Unix(0, math.MinInt64) + _maxTimeInt64 = time.Unix(0, math.MaxInt64) +) + // Skip constructs a no-op field, which is often useful when handling invalid // inputs in other Field constructors. func Skip() Field { return Field{Type: zapcore.SkipType} } +// nilField returns a field which will marshal explicitly as nil. See motivation +// in https://github.com/uber-go/zap/issues/753 . If we ever make breaking +// changes and add zapcore.NilType and zapcore.ObjectEncoder.AddNil, the +// implementation here should be changed to reflect that. +func nilField(key string) Field { return Reflect(key, nil) } + // Binary constructs a field that carries an opaque binary blob. // // Binary data is serialized in an encoding-appropriate format. For example, @@ -56,6 +67,15 @@ func Bool(key string, val bool) Field { return Field{Key: key, Type: zapcore.BoolType, Integer: ival} } +// Boolp constructs a field that carries a *bool. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Boolp(key string, val *bool) Field { + if val == nil { + return nilField(key) + } + return Bool(key, *val) +} + // ByteString constructs a field that carries UTF-8 encoded text as a []byte. // To log opaque binary blobs (which aren't necessarily valid UTF-8), use // Binary. @@ -70,6 +90,15 @@ func Complex128(key string, val complex128) Field { return Field{Key: key, Type: zapcore.Complex128Type, Interface: val} } +// Complex128p constructs a field that carries a *complex128. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Complex128p(key string, val *complex128) Field { + if val == nil { + return nilField(key) + } + return Complex128(key, *val) +} + // Complex64 constructs a field that carries a complex number. Unlike most // numeric fields, this costs an allocation (to convert the complex64 to // interface{}). @@ -77,6 +106,15 @@ func Complex64(key string, val complex64) Field { return Field{Key: key, Type: zapcore.Complex64Type, Interface: val} } +// Complex64p constructs a field that carries a *complex64. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Complex64p(key string, val *complex64) Field { + if val == nil { + return nilField(key) + } + return Complex64(key, *val) +} + // Float64 constructs a field that carries a float64. The way the // floating-point value is represented is encoder-dependent, so marshaling is // necessarily lazy. @@ -84,6 +122,15 @@ func Float64(key string, val float64) Field { return Field{Key: key, Type: zapcore.Float64Type, Integer: int64(math.Float64bits(val))} } +// Float64p constructs a field that carries a *float64. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Float64p(key string, val *float64) Field { + if val == nil { + return nilField(key) + } + return Float64(key, *val) +} + // Float32 constructs a field that carries a float32. The way the // floating-point value is represented is encoder-dependent, so marshaling is // necessarily lazy. @@ -91,66 +138,183 @@ func Float32(key string, val float32) Field { return Field{Key: key, Type: zapcore.Float32Type, Integer: int64(math.Float32bits(val))} } +// Float32p constructs a field that carries a *float32. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Float32p(key string, val *float32) Field { + if val == nil { + return nilField(key) + } + return Float32(key, *val) +} + // Int constructs a field with the given key and value. func Int(key string, val int) Field { return Int64(key, int64(val)) } +// Intp constructs a field that carries a *int. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Intp(key string, val *int) Field { + if val == nil { + return nilField(key) + } + return Int(key, *val) +} + // Int64 constructs a field with the given key and value. func Int64(key string, val int64) Field { return Field{Key: key, Type: zapcore.Int64Type, Integer: val} } +// Int64p constructs a field that carries a *int64. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Int64p(key string, val *int64) Field { + if val == nil { + return nilField(key) + } + return Int64(key, *val) +} + // Int32 constructs a field with the given key and value. func Int32(key string, val int32) Field { return Field{Key: key, Type: zapcore.Int32Type, Integer: int64(val)} } +// Int32p constructs a field that carries a *int32. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Int32p(key string, val *int32) Field { + if val == nil { + return nilField(key) + } + return Int32(key, *val) +} + // Int16 constructs a field with the given key and value. func Int16(key string, val int16) Field { return Field{Key: key, Type: zapcore.Int16Type, Integer: int64(val)} } +// Int16p constructs a field that carries a *int16. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Int16p(key string, val *int16) Field { + if val == nil { + return nilField(key) + } + return Int16(key, *val) +} + // Int8 constructs a field with the given key and value. func Int8(key string, val int8) Field { return Field{Key: key, Type: zapcore.Int8Type, Integer: int64(val)} } +// Int8p constructs a field that carries a *int8. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Int8p(key string, val *int8) Field { + if val == nil { + return nilField(key) + } + return Int8(key, *val) +} + // String constructs a field with the given key and value. func String(key string, val string) Field { return Field{Key: key, Type: zapcore.StringType, String: val} } +// Stringp constructs a field that carries a *string. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Stringp(key string, val *string) Field { + if val == nil { + return nilField(key) + } + return String(key, *val) +} + // Uint constructs a field with the given key and value. func Uint(key string, val uint) Field { return Uint64(key, uint64(val)) } +// Uintp constructs a field that carries a *uint. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uintp(key string, val *uint) Field { + if val == nil { + return nilField(key) + } + return Uint(key, *val) +} + // Uint64 constructs a field with the given key and value. func Uint64(key string, val uint64) Field { return Field{Key: key, Type: zapcore.Uint64Type, Integer: int64(val)} } +// Uint64p constructs a field that carries a *uint64. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uint64p(key string, val *uint64) Field { + if val == nil { + return nilField(key) + } + return Uint64(key, *val) +} + // Uint32 constructs a field with the given key and value. func Uint32(key string, val uint32) Field { return Field{Key: key, Type: zapcore.Uint32Type, Integer: int64(val)} } +// Uint32p constructs a field that carries a *uint32. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uint32p(key string, val *uint32) Field { + if val == nil { + return nilField(key) + } + return Uint32(key, *val) +} + // Uint16 constructs a field with the given key and value. func Uint16(key string, val uint16) Field { return Field{Key: key, Type: zapcore.Uint16Type, Integer: int64(val)} } +// Uint16p constructs a field that carries a *uint16. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uint16p(key string, val *uint16) Field { + if val == nil { + return nilField(key) + } + return Uint16(key, *val) +} + // Uint8 constructs a field with the given key and value. func Uint8(key string, val uint8) Field { return Field{Key: key, Type: zapcore.Uint8Type, Integer: int64(val)} } +// Uint8p constructs a field that carries a *uint8. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uint8p(key string, val *uint8) Field { + if val == nil { + return nilField(key) + } + return Uint8(key, *val) +} + // Uintptr constructs a field with the given key and value. func Uintptr(key string, val uintptr) Field { return Field{Key: key, Type: zapcore.UintptrType, Integer: int64(val)} } +// Uintptrp constructs a field that carries a *uintptr. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Uintptrp(key string, val *uintptr) Field { + if val == nil { + return nilField(key) + } + return Uintptr(key, *val) +} + // Reflect constructs a field with the given key and an arbitrary object. It uses // an encoding-appropriate, reflection-based function to lazily serialize nearly // any object into the logging context, but it's relatively slow and @@ -180,19 +344,37 @@ func Stringer(key string, val fmt.Stringer) Field { // Time constructs a Field with the given key and value. The encoder // controls how the time is serialized. func Time(key string, val time.Time) Field { + if val.Before(_minTimeInt64) || val.After(_maxTimeInt64) { + return Field{Key: key, Type: zapcore.TimeFullType, Interface: val} + } return Field{Key: key, Type: zapcore.TimeType, Integer: val.UnixNano(), Interface: val.Location()} } +// Timep constructs a field that carries a *time.Time. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Timep(key string, val *time.Time) Field { + if val == nil { + return nilField(key) + } + return Time(key, *val) +} + // Stack constructs a field that stores a stacktrace of the current goroutine // under provided key. Keep in mind that taking a stacktrace is eager and // expensive (relatively speaking); this function both makes an allocation and // takes about two microseconds. func Stack(key string) Field { + return StackSkip(key, 1) // skip Stack +} + +// StackSkip constructs a field similarly to Stack, but also skips the given +// number of frames from the top of the stacktrace. +func StackSkip(key string, skip int) Field { // Returning the stacktrace as a string costs an allocation, but saves us // from expanding the zapcore.Field union struct to include a byte slice. Since // taking a stacktrace is already so expensive (~10us), the extra allocation // is okay. - return String(key, takeStacktrace()) + return String(key, takeStacktrace(skip+1)) // skip StackSkip } // Duration constructs a field with the given key and value. The encoder @@ -201,6 +383,15 @@ func Duration(key string, val time.Duration) Field { return Field{Key: key, Type: zapcore.DurationType, Integer: int64(val)} } +// Durationp constructs a field that carries a *time.Duration. The returned Field will safely +// and explicitly represent `nil` when appropriate. +func Durationp(key string, val *time.Duration) Field { + if val == nil { + return nilField(key) + } + return Duration(key, *val) +} + // Object constructs a field with the given key and ObjectMarshaler. It // provides a flexible, but still type-safe and efficient, way to add map- or // struct-like user-defined types to the logging context. The struct's @@ -209,6 +400,16 @@ func Object(key string, val zapcore.ObjectMarshaler) Field { return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val} } +// Inline constructs a Field that is similar to Object, but it +// will add the elements of the provided ObjectMarshaler to the +// current namespace. +func Inline(val zapcore.ObjectMarshaler) Field { + return zapcore.Field{ + Type: zapcore.InlineMarshalerType, + Interface: val, + } +} + // Any takes a key and an arbitrary value and chooses the best way to represent // them as a field, falling back to a reflection-based approach only if // necessary. @@ -224,78 +425,116 @@ func Any(key string, value interface{}) Field { return Array(key, val) case bool: return Bool(key, val) + case *bool: + return Boolp(key, val) case []bool: return Bools(key, val) case complex128: return Complex128(key, val) + case *complex128: + return Complex128p(key, val) case []complex128: return Complex128s(key, val) case complex64: return Complex64(key, val) + case *complex64: + return Complex64p(key, val) case []complex64: return Complex64s(key, val) case float64: return Float64(key, val) + case *float64: + return Float64p(key, val) case []float64: return Float64s(key, val) case float32: return Float32(key, val) + case *float32: + return Float32p(key, val) case []float32: return Float32s(key, val) case int: return Int(key, val) + case *int: + return Intp(key, val) case []int: return Ints(key, val) case int64: return Int64(key, val) + case *int64: + return Int64p(key, val) case []int64: return Int64s(key, val) case int32: return Int32(key, val) + case *int32: + return Int32p(key, val) case []int32: return Int32s(key, val) case int16: return Int16(key, val) + case *int16: + return Int16p(key, val) case []int16: return Int16s(key, val) case int8: return Int8(key, val) + case *int8: + return Int8p(key, val) case []int8: return Int8s(key, val) case string: return String(key, val) + case *string: + return Stringp(key, val) case []string: return Strings(key, val) case uint: return Uint(key, val) + case *uint: + return Uintp(key, val) case []uint: return Uints(key, val) case uint64: return Uint64(key, val) + case *uint64: + return Uint64p(key, val) case []uint64: return Uint64s(key, val) case uint32: return Uint32(key, val) + case *uint32: + return Uint32p(key, val) case []uint32: return Uint32s(key, val) case uint16: return Uint16(key, val) + case *uint16: + return Uint16p(key, val) case []uint16: return Uint16s(key, val) case uint8: return Uint8(key, val) + case *uint8: + return Uint8p(key, val) case []byte: return Binary(key, val) case uintptr: return Uintptr(key, val) + case *uintptr: + return Uintptrp(key, val) case []uintptr: return Uintptrs(key, val) case time.Time: return Time(key, val) + case *time.Time: + return Timep(key, val) case []time.Time: return Times(key, val) case time.Duration: return Duration(key, val) + case *time.Duration: + return Durationp(key, val) case []time.Duration: return Durations(key, val) case error: diff --git a/field_test.go b/field_test.go index 069bfe372..010e6fb4d 100644 --- a/field_test.go +++ b/field_test.go @@ -21,7 +21,9 @@ package zap import ( + "math" "net" + "regexp" "sync" "testing" "time" @@ -63,6 +65,29 @@ func TestFieldConstructors(t *testing.T) { name := username("phil") ints := []int{5, 6} + // Helpful values for use in constructing pointers to primitives below. + var ( + boolVal bool = true + complex128Val complex128 = complex(0, 0) + complex64Val complex64 = complex(0, 0) + durationVal time.Duration = time.Second + float64Val float64 = 1.0 + float32Val float32 = 1.0 + intVal int = 1 + int64Val int64 = 1 + int32Val int32 = 1 + int16Val int16 = 1 + int8Val int8 = 1 + stringVal string = "hello" + timeVal time.Time = time.Unix(100000, 0) + uintVal uint = 1 + uint64Val uint64 = 1 + uint32Val uint32 = 1 + uint16Val uint16 = 1 + uint8Val uint8 = 1 + uintptrVal uintptr = 1 + ) + tests := []struct { name string field Field @@ -84,6 +109,10 @@ func TestFieldConstructors(t *testing.T) { {"String", Field{Key: "k", Type: zapcore.StringType, String: "foo"}, String("k", "foo")}, {"Time", Field{Key: "k", Type: zapcore.TimeType, Integer: 0, Interface: time.UTC}, Time("k", time.Unix(0, 0).In(time.UTC))}, {"Time", Field{Key: "k", Type: zapcore.TimeType, Integer: 1000, Interface: time.UTC}, Time("k", time.Unix(0, 1000).In(time.UTC))}, + {"Time", Field{Key: "k", Type: zapcore.TimeType, Integer: math.MinInt64, Interface: time.UTC}, Time("k", time.Unix(0, math.MinInt64).In(time.UTC))}, + {"Time", Field{Key: "k", Type: zapcore.TimeType, Integer: math.MaxInt64, Interface: time.UTC}, Time("k", time.Unix(0, math.MaxInt64).In(time.UTC))}, + {"Time", Field{Key: "k", Type: zapcore.TimeFullType, Interface: time.Time{}}, Time("k", time.Time{})}, + {"Time", Field{Key: "k", Type: zapcore.TimeFullType, Interface: time.Unix(math.MaxInt64, 0)}, Time("k", time.Unix(math.MaxInt64, 0))}, {"Uint", Field{Key: "k", Type: zapcore.Uint64Type, Integer: 1}, Uint("k", 1)}, {"Uint64", Field{Key: "k", Type: zapcore.Uint64Type, Integer: 1}, Uint64("k", 1)}, {"Uint32", Field{Key: "k", Type: zapcore.Uint32Type, Integer: 1}, Uint32("k", 1)}, @@ -91,8 +120,10 @@ func TestFieldConstructors(t *testing.T) { {"Uint8", Field{Key: "k", Type: zapcore.Uint8Type, Integer: 1}, Uint8("k", 1)}, {"Uintptr", Field{Key: "k", Type: zapcore.UintptrType, Integer: 10}, Uintptr("k", 0xa)}, {"Reflect", Field{Key: "k", Type: zapcore.ReflectType, Interface: ints}, Reflect("k", ints)}, + {"Reflect", Field{Key: "k", Type: zapcore.ReflectType}, Reflect("k", nil)}, {"Stringer", Field{Key: "k", Type: zapcore.StringerType, Interface: addr}, Stringer("k", addr)}, {"Object", Field{Key: "k", Type: zapcore.ObjectMarshalerType, Interface: name}, Object("k", name)}, + {"Inline", Field{Type: zapcore.InlineMarshalerType, Interface: name}, Inline(name)}, {"Any:ObjectMarshaler", Any("k", name), Object("k", name)}, {"Any:ArrayMarshaler", Any("k", bools([]bool{true})), Array("k", bools([]bool{true}))}, {"Any:Stringer", Any("k", addr), Stringer("k", addr)}, @@ -139,6 +170,82 @@ func TestFieldConstructors(t *testing.T) { {"Any:Duration", Any("k", time.Second), Duration("k", time.Second)}, {"Any:Durations", Any("k", []time.Duration{time.Second}), Durations("k", []time.Duration{time.Second})}, {"Any:Fallback", Any("k", struct{}{}), Reflect("k", struct{}{})}, + {"Ptr:Bool", Boolp("k", nil), nilField("k")}, + {"Ptr:Bool", Boolp("k", &boolVal), Bool("k", boolVal)}, + {"Any:PtrBool", Any("k", (*bool)(nil)), nilField("k")}, + {"Any:PtrBool", Any("k", &boolVal), Bool("k", boolVal)}, + {"Ptr:Complex128", Complex128p("k", nil), nilField("k")}, + {"Ptr:Complex128", Complex128p("k", &complex128Val), Complex128("k", complex128Val)}, + {"Any:PtrComplex128", Any("k", (*complex128)(nil)), nilField("k")}, + {"Any:PtrComplex128", Any("k", &complex128Val), Complex128("k", complex128Val)}, + {"Ptr:Complex64", Complex64p("k", nil), nilField("k")}, + {"Ptr:Complex64", Complex64p("k", &complex64Val), Complex64("k", complex64Val)}, + {"Any:PtrComplex64", Any("k", (*complex64)(nil)), nilField("k")}, + {"Any:PtrComplex64", Any("k", &complex64Val), Complex64("k", complex64Val)}, + {"Ptr:Duration", Durationp("k", nil), nilField("k")}, + {"Ptr:Duration", Durationp("k", &durationVal), Duration("k", durationVal)}, + {"Any:PtrDuration", Any("k", (*time.Duration)(nil)), nilField("k")}, + {"Any:PtrDuration", Any("k", &durationVal), Duration("k", durationVal)}, + {"Ptr:Float64", Float64p("k", nil), nilField("k")}, + {"Ptr:Float64", Float64p("k", &float64Val), Float64("k", float64Val)}, + {"Any:PtrFloat64", Any("k", (*float64)(nil)), nilField("k")}, + {"Any:PtrFloat64", Any("k", &float64Val), Float64("k", float64Val)}, + {"Ptr:Float32", Float32p("k", nil), nilField("k")}, + {"Ptr:Float32", Float32p("k", &float32Val), Float32("k", float32Val)}, + {"Any:PtrFloat32", Any("k", (*float32)(nil)), nilField("k")}, + {"Any:PtrFloat32", Any("k", &float32Val), Float32("k", float32Val)}, + {"Ptr:Int", Intp("k", nil), nilField("k")}, + {"Ptr:Int", Intp("k", &intVal), Int("k", intVal)}, + {"Any:PtrInt", Any("k", (*int)(nil)), nilField("k")}, + {"Any:PtrInt", Any("k", &intVal), Int("k", intVal)}, + {"Ptr:Int64", Int64p("k", nil), nilField("k")}, + {"Ptr:Int64", Int64p("k", &int64Val), Int64("k", int64Val)}, + {"Any:PtrInt64", Any("k", (*int64)(nil)), nilField("k")}, + {"Any:PtrInt64", Any("k", &int64Val), Int64("k", int64Val)}, + {"Ptr:Int32", Int32p("k", nil), nilField("k")}, + {"Ptr:Int32", Int32p("k", &int32Val), Int32("k", int32Val)}, + {"Any:PtrInt32", Any("k", (*int32)(nil)), nilField("k")}, + {"Any:PtrInt32", Any("k", &int32Val), Int32("k", int32Val)}, + {"Ptr:Int16", Int16p("k", nil), nilField("k")}, + {"Ptr:Int16", Int16p("k", &int16Val), Int16("k", int16Val)}, + {"Any:PtrInt16", Any("k", (*int16)(nil)), nilField("k")}, + {"Any:PtrInt16", Any("k", &int16Val), Int16("k", int16Val)}, + {"Ptr:Int8", Int8p("k", nil), nilField("k")}, + {"Ptr:Int8", Int8p("k", &int8Val), Int8("k", int8Val)}, + {"Any:PtrInt8", Any("k", (*int8)(nil)), nilField("k")}, + {"Any:PtrInt8", Any("k", &int8Val), Int8("k", int8Val)}, + {"Ptr:String", Stringp("k", nil), nilField("k")}, + {"Ptr:String", Stringp("k", &stringVal), String("k", stringVal)}, + {"Any:PtrString", Any("k", (*string)(nil)), nilField("k")}, + {"Any:PtrString", Any("k", &stringVal), String("k", stringVal)}, + {"Ptr:Time", Timep("k", nil), nilField("k")}, + {"Ptr:Time", Timep("k", &timeVal), Time("k", timeVal)}, + {"Any:PtrTime", Any("k", (*time.Time)(nil)), nilField("k")}, + {"Any:PtrTime", Any("k", &timeVal), Time("k", timeVal)}, + {"Ptr:Uint", Uintp("k", nil), nilField("k")}, + {"Ptr:Uint", Uintp("k", &uintVal), Uint("k", uintVal)}, + {"Any:PtrUint", Any("k", (*uint)(nil)), nilField("k")}, + {"Any:PtrUint", Any("k", &uintVal), Uint("k", uintVal)}, + {"Ptr:Uint64", Uint64p("k", nil), nilField("k")}, + {"Ptr:Uint64", Uint64p("k", &uint64Val), Uint64("k", uint64Val)}, + {"Any:PtrUint64", Any("k", (*uint64)(nil)), nilField("k")}, + {"Any:PtrUint64", Any("k", &uint64Val), Uint64("k", uint64Val)}, + {"Ptr:Uint32", Uint32p("k", nil), nilField("k")}, + {"Ptr:Uint32", Uint32p("k", &uint32Val), Uint32("k", uint32Val)}, + {"Any:PtrUint32", Any("k", (*uint32)(nil)), nilField("k")}, + {"Any:PtrUint32", Any("k", &uint32Val), Uint32("k", uint32Val)}, + {"Ptr:Uint16", Uint16p("k", nil), nilField("k")}, + {"Ptr:Uint16", Uint16p("k", &uint16Val), Uint16("k", uint16Val)}, + {"Any:PtrUint16", Any("k", (*uint16)(nil)), nilField("k")}, + {"Any:PtrUint16", Any("k", &uint16Val), Uint16("k", uint16Val)}, + {"Ptr:Uint8", Uint8p("k", nil), nilField("k")}, + {"Ptr:Uint8", Uint8p("k", &uint8Val), Uint8("k", uint8Val)}, + {"Any:PtrUint8", Any("k", (*uint8)(nil)), nilField("k")}, + {"Any:PtrUint8", Any("k", &uint8Val), Uint8("k", uint8Val)}, + {"Ptr:Uintptr", Uintptrp("k", nil), nilField("k")}, + {"Ptr:Uintptr", Uintptrp("k", &uintptrVal), Uintptr("k", uintptrVal)}, + {"Any:PtrUintptr", Any("k", (*uintptr)(nil)), nilField("k")}, + {"Any:PtrUintptr", Any("k", &uintptrVal), Uintptr("k", uintptrVal)}, {"Namespace", Namespace("k"), Field{Key: "k", Type: zapcore.NamespaceType}}, } @@ -154,6 +261,24 @@ func TestStackField(t *testing.T) { f := Stack("stacktrace") assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.") assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.") - assert.Equal(t, takeStacktrace(), f.String, "Unexpected stack trace") + r := regexp.MustCompile(`field_test.go:(\d+)`) + assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), "Unexpected stack trace") + assertCanBeReused(t, f) +} + +func TestStackSkipField(t *testing.T) { + f := StackSkip("stacktrace", 0) + assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.") + assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.") + r := regexp.MustCompile(`field_test.go:(\d+)`) + assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), f.String, "Unexpected stack trace") + assertCanBeReused(t, f) +} + +func TestStackSkipFieldWithSkip(t *testing.T) { + f := StackSkip("stacktrace", 1) + assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.") + assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.") + assert.Equal(t, takeStacktrace(1), f.String, "Unexpected stack trace") assertCanBeReused(t, f) } diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 881b462c0..000000000 --- a/glide.lock +++ /dev/null @@ -1,76 +0,0 @@ -hash: f073ba522c06c88ea3075bde32a8aaf0969a840a66cab6318a0897d141ffee92 -updated: 2017-07-22T18:06:49.598185334-07:00 -imports: -- name: go.uber.org/atomic - version: 4e336646b2ef9fc6e47be8e21594178f98e5ebcf -- name: go.uber.org/multierr - version: 3c4937480c32f4c13a875a1829af76c98ca3d40a -testImports: -- name: github.com/apex/log - version: d9b960447bfa720077b2da653cc79e533455b499 - subpackages: - - handlers/json -- name: github.com/axw/gocov - version: 3a69a0d2a4ef1f263e2d92b041a69593d6964fe8 - subpackages: - - gocov -- name: github.com/davecgh/go-spew - version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 - subpackages: - - spew -- name: github.com/fatih/color - version: 62e9147c64a1ed519147b62a56a14e83e2be02c1 -- name: github.com/go-kit/kit - version: e10f5bf035be9af21fd5b2fb4469d5716c6ab07d - subpackages: - - log -- name: github.com/go-logfmt/logfmt - version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 -- name: github.com/go-stack/stack - version: 54be5f394ed2c3e19dac9134a40a95ba5a017f7b -- name: github.com/golang/lint - version: c5fb716d6688a859aae56d26d3e6070808df29f7 - subpackages: - - golint -- name: github.com/kr/logfmt - version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 -- name: github.com/mattn/go-colorable - version: 3fa8c76f9daed4067e4a806fb7e4dc86455c6d6a -- name: github.com/mattn/go-isatty - version: fc9e8d8ef48496124e79ae0df75490096eccf6fe -- name: github.com/mattn/goveralls - version: 6efce81852ad1b7567c17ad71b03aeccc9dd9ae0 -- name: github.com/pborman/uuid - version: e790cca94e6cc75c7064b1332e63811d4aae1a53 -- name: github.com/pkg/errors - version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d - subpackages: - - difflib -- name: github.com/rs/zerolog - version: eed4c2b94d945e0b2456ad6aa518a443986b5f22 -- name: github.com/satori/go.uuid - version: 5bf94b69c6b68ee1b541973bb8e1144db23a194b -- name: github.com/sirupsen/logrus - version: 7dd06bf38e1e13df288d471a57d5adbac106be9e -- name: github.com/stretchr/testify - version: f6abca593680b2315d2075e0f5e2a9751e3f431a - subpackages: - - assert - - require -- name: go.pedge.io/lion - version: 87958e8713f1fa138d993087133b97e976642159 -- name: golang.org/x/sys - version: c4489faa6e5ab84c0ef40d6ee878f7a030281f0f - subpackages: - - unix -- name: golang.org/x/tools - version: 496819729719f9d07692195e0a94d6edd2251389 - subpackages: - - cover -- name: gopkg.in/inconshreveable/log15.v2 - version: b105bd37f74e5d9dc7b6ad7806715c7a2b83fd3f - subpackages: - - stack - - term diff --git a/glide.yaml b/glide.yaml index 94412594c..8e1d05e9a 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,12 +22,11 @@ testImport: - package: github.com/mattn/goveralls - package: github.com/pborman/uuid - package: github.com/pkg/errors -- package: go.pedge.io/lion - package: github.com/rs/zerolog - package: golang.org/x/tools subpackages: - cover -- package: github.com/golang/lint +- package: golang.org/x/lint subpackages: - golint - package: github.com/axw/gocov diff --git a/global.go b/global.go index d02232e39..c1ac0507c 100644 --- a/global.go +++ b/global.go @@ -31,7 +31,6 @@ import ( ) const ( - _stdLogDefaultDepth = 2 _loggerWriterDepth = 2 _programmerErrorTemplate = "You've found a bug in zap! Please file a bug at " + "https://github.com/uber-go/zap/issues/new and reference this error: %v" diff --git a/global_go112.go b/global_go112.go new file mode 100644 index 000000000..6b5dbda80 --- /dev/null +++ b/global_go112.go @@ -0,0 +1,26 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// See #682 for more information. +// +build go1.12 + +package zap + +const _stdLogDefaultDepth = 1 diff --git a/global_prego112.go b/global_prego112.go new file mode 100644 index 000000000..d3ab9af93 --- /dev/null +++ b/global_prego112.go @@ -0,0 +1,26 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// See #682 for more information. +// +build !go1.12 + +package zap + +const _stdLogDefaultDepth = 2 diff --git a/global_test.go b/global_test.go index e169f5179..7ae235e1d 100644 --- a/global_test.go +++ b/global_test.go @@ -273,7 +273,7 @@ func checkStdLogMessage(t *testing.T, msg string, logs *observer.ObservedLogs) { assert.Equal(t, "redirected", entry.Entry.Message, "Unexpected entry message.") assert.Regexp( t, - `go.uber.org/zap/global_test.go:\d+$`, + `/global_test.go:\d+$`, entry.Entry.Caller.String(), "Unexpected caller annotation.", ) diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..9455c99cc --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module go.uber.org/zap + +go 1.13 + +require ( + github.com/benbjohnson/clock v1.1.0 + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.7.0 + go.uber.org/atomic v1.7.0 + go.uber.org/goleak v1.1.10 + go.uber.org/multierr v1.6.0 + gopkg.in/yaml.v2 v2.2.8 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..b330071a0 --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http_handler.go b/http_handler.go index 1b0ecaca9..1297c33b3 100644 --- a/http_handler.go +++ b/http_handler.go @@ -23,6 +23,7 @@ package zap import ( "encoding/json" "fmt" + "io" "net/http" "go.uber.org/zap/zapcore" @@ -31,47 +32,63 @@ import ( // ServeHTTP is a simple JSON endpoint that can report on or change the current // logging level. // -// GET requests return a JSON description of the current logging level. PUT -// requests change the logging level and expect a payload like: +// GET +// +// The GET request returns a JSON description of the current logging level like: // {"level":"info"} // -// It's perfectly safe to change the logging level while a program is running. +// PUT +// +// The PUT request changes the logging level. It is perfectly safe to change the +// logging level while a program is running. Two content types are supported: +// +// Content-Type: application/x-www-form-urlencoded +// +// With this content type, the level can be provided through the request body or +// a query parameter. The log level is URL encoded like: +// +// level=debug +// +// The request body takes precedence over the query parameter, if both are +// specified. +// +// This content type is the default for a curl PUT request. Following are two +// example curl requests that both set the logging level to debug. +// +// curl -X PUT localhost:8080/log/level?level=debug +// curl -X PUT localhost:8080/log/level -d level=debug +// +// For any other content type, the payload is expected to be JSON encoded and +// look like: +// +// {"level":"info"} +// +// An example curl request could look like this: +// +// curl -X PUT localhost:8080/log/level -H "Content-Type: application/json" -d '{"level":"debug"}' +// func (lvl AtomicLevel) ServeHTTP(w http.ResponseWriter, r *http.Request) { type errorResponse struct { Error string `json:"error"` } type payload struct { - Level *zapcore.Level `json:"level"` + Level zapcore.Level `json:"level"` } enc := json.NewEncoder(w) switch r.Method { - case http.MethodGet: - current := lvl.Level() - enc.Encode(payload{Level: ¤t}) - + enc.Encode(payload{Level: lvl.Level()}) case http.MethodPut: - var req payload - - if errmess := func() string { - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fmt.Sprintf("Request body must be well-formed JSON: %v", err) - } - if req.Level == nil { - return "Must specify a logging level." - } - return "" - }(); errmess != "" { + requestedLvl, err := decodePutRequest(r.Header.Get("Content-Type"), r) + if err != nil { w.WriteHeader(http.StatusBadRequest) - enc.Encode(errorResponse{Error: errmess}) + enc.Encode(errorResponse{Error: err.Error()}) return } - - lvl.SetLevel(*req.Level) - enc.Encode(req) - + lvl.SetLevel(requestedLvl) + enc.Encode(payload{Level: lvl.Level()}) default: w.WriteHeader(http.StatusMethodNotAllowed) enc.Encode(errorResponse{ @@ -79,3 +96,37 @@ func (lvl AtomicLevel) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) } } + +// Decodes incoming PUT requests and returns the requested logging level. +func decodePutRequest(contentType string, r *http.Request) (zapcore.Level, error) { + if contentType == "application/x-www-form-urlencoded" { + return decodePutURL(r) + } + return decodePutJSON(r.Body) +} + +func decodePutURL(r *http.Request) (zapcore.Level, error) { + lvl := r.FormValue("level") + if lvl == "" { + return 0, fmt.Errorf("must specify logging level") + } + var l zapcore.Level + if err := l.UnmarshalText([]byte(lvl)); err != nil { + return 0, err + } + return l, nil +} + +func decodePutJSON(body io.Reader) (zapcore.Level, error) { + var pld struct { + Level *zapcore.Level `json:"level"` + } + if err := json.NewDecoder(body).Decode(&pld); err != nil { + return 0, fmt.Errorf("malformed request body: %v", err) + } + if pld.Level == nil { + return 0, fmt.Errorf("must specify logging level") + } + return *pld.Level, nil + +} diff --git a/http_handler_test.go b/http_handler_test.go index 474b3c7cd..9fa9c64c1 100644 --- a/http_handler_test.go +++ b/http_handler_test.go @@ -22,110 +22,169 @@ package zap_test import ( "encoding/json" - "fmt" - "io" - "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" - . "go.uber.org/zap" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func newHandler() (AtomicLevel, *Logger) { - lvl := NewAtomicLevel() - logger := New(zapcore.NewNopCore()) - return lvl, logger -} - -func assertCodeOK(t testing.TB, code int) { - assert.Equal(t, http.StatusOK, code, "Unexpected response status code.") -} - -func assertCodeBadRequest(t testing.TB, code int) { - assert.Equal(t, http.StatusBadRequest, code, "Unexpected response status code.") -} - -func assertCodeMethodNotAllowed(t testing.TB, code int) { - assert.Equal(t, http.StatusMethodNotAllowed, code, "Unexpected response status code.") -} - -func assertResponse(t testing.TB, expectedLevel zapcore.Level, actualBody string) { - assert.Equal(t, fmt.Sprintf(`{"level":"%s"}`, expectedLevel)+"\n", actualBody, "Unexpected response body.") -} - -func assertJSONError(t testing.TB, body string) { - // Don't need to test exact error message, but one should be present. - var payload map[string]interface{} - require.NoError(t, json.Unmarshal([]byte(body), &payload), "Expected error response to be JSON.") - - msg, ok := payload["error"] - require.True(t, ok, "Error message is an unexpected type.") - assert.NotEqual(t, "", msg, "Expected an error message in response.") -} - -func makeRequest(t testing.TB, method string, handler http.Handler, reader io.Reader) (int, string) { - ts := httptest.NewServer(handler) - defer ts.Close() - - req, err := http.NewRequest(method, ts.URL, reader) - require.NoError(t, err, "Error constructing %s request.", method) - - res, err := http.DefaultClient.Do(req) - require.NoError(t, err, "Error making %s request.", method) - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - require.NoError(t, err, "Error reading request body.") - - return res.StatusCode, string(body) -} - -func TestHTTPHandlerGetLevel(t *testing.T) { - lvl, _ := newHandler() - code, body := makeRequest(t, "GET", lvl, nil) - assertCodeOK(t, code) - assertResponse(t, lvl.Level(), body) -} - -func TestHTTPHandlerPutLevel(t *testing.T) { - lvl, _ := newHandler() - - code, body := makeRequest(t, "PUT", lvl, strings.NewReader(`{"level":"warn"}`)) - - assertCodeOK(t, code) - assertResponse(t, lvl.Level(), body) -} - -func TestHTTPHandlerPutUnrecognizedLevel(t *testing.T) { - lvl, _ := newHandler() - code, body := makeRequest(t, "PUT", lvl, strings.NewReader(`{"level":"unrecognized-level"}`)) - assertCodeBadRequest(t, code) - assertJSONError(t, body) -} - -func TestHTTPHandlerNotJSON(t *testing.T) { - lvl, _ := newHandler() - code, body := makeRequest(t, "PUT", lvl, strings.NewReader(`{`)) - assertCodeBadRequest(t, code) - assertJSONError(t, body) -} - -func TestHTTPHandlerNoLevelSpecified(t *testing.T) { - lvl, _ := newHandler() - code, body := makeRequest(t, "PUT", lvl, strings.NewReader(`{}`)) - assertCodeBadRequest(t, code) - assertJSONError(t, body) -} - -func TestHTTPHandlerMethodNotAllowed(t *testing.T) { - lvl, _ := newHandler() - code, body := makeRequest(t, "POST", lvl, strings.NewReader(`{`)) - assertCodeMethodNotAllowed(t, code) - assertJSONError(t, body) +func TestAtomicLevelServeHTTP(t *testing.T) { + tests := []struct { + desc string + method string + query string + contentType string + body string + expectedCode int + expectedLevel zapcore.Level + }{ + { + desc: "GET", + method: http.MethodGet, + expectedCode: http.StatusOK, + expectedLevel: zap.InfoLevel, + }, + { + desc: "PUT JSON", + method: http.MethodPut, + expectedCode: http.StatusOK, + expectedLevel: zap.WarnLevel, + body: `{"level":"warn"}`, + }, + { + desc: "PUT URL encoded", + method: http.MethodPut, + expectedCode: http.StatusOK, + expectedLevel: zap.WarnLevel, + contentType: "application/x-www-form-urlencoded", + body: "level=warn", + }, + { + desc: "PUT query parameters", + method: http.MethodPut, + query: "?level=warn", + expectedCode: http.StatusOK, + expectedLevel: zap.WarnLevel, + contentType: "application/x-www-form-urlencoded", + }, + { + desc: "body takes precedence over query", + method: http.MethodPut, + query: "?level=info", + expectedCode: http.StatusOK, + expectedLevel: zap.WarnLevel, + contentType: "application/x-www-form-urlencoded", + body: "level=warn", + }, + { + desc: "JSON ignores query", + method: http.MethodPut, + query: "?level=info", + expectedCode: http.StatusOK, + expectedLevel: zap.WarnLevel, + body: `{"level":"warn"}`, + }, + { + desc: "PUT JSON unrecognized", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + body: `{"level":"unrecognized"}`, + }, + { + desc: "PUT URL encoded unrecognized", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + contentType: "application/x-www-form-urlencoded", + body: "level=unrecognized", + }, + { + desc: "PUT JSON malformed", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + body: `{"level":"warn`, + }, + { + desc: "PUT URL encoded malformed", + method: http.MethodPut, + query: "?level=%", + expectedCode: http.StatusBadRequest, + contentType: "application/x-www-form-urlencoded", + }, + { + desc: "PUT Query parameters malformed", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + contentType: "application/x-www-form-urlencoded", + body: "level=%", + }, + { + desc: "PUT JSON unspecified", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + body: `{}`, + }, + { + desc: "PUT URL encoded unspecified", + method: http.MethodPut, + expectedCode: http.StatusBadRequest, + contentType: "application/x-www-form-urlencoded", + body: "", + }, + { + desc: "POST JSON", + method: http.MethodPost, + expectedCode: http.StatusMethodNotAllowed, + body: `{"level":"warn"}`, + }, + { + desc: "POST URL", + method: http.MethodPost, + expectedCode: http.StatusMethodNotAllowed, + contentType: "application/x-www-form-urlencoded", + body: "level=warn", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + lvl := zap.NewAtomicLevel() + lvl.SetLevel(zapcore.InfoLevel) + + server := httptest.NewServer(lvl) + defer server.Close() + + req, err := http.NewRequest(tt.method, server.URL+tt.query, strings.NewReader(tt.body)) + require.NoError(t, err, "Error constructing %s request.", req.Method) + if tt.contentType != "" { + req.Header.Set("Content-Type", tt.contentType) + } + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err, "Error making %s request.", req.Method) + defer res.Body.Close() + + require.Equal(t, tt.expectedCode, res.StatusCode, "Unexpected status code.") + if tt.expectedCode != http.StatusOK { + // Don't need to test exact error message, but one should be present. + var pld struct { + Error string `json:"error"` + } + require.NoError(t, json.NewDecoder(res.Body).Decode(&pld), "Decoding response body") + assert.NotEmpty(t, pld.Error, "Expected an error message") + return + } + + var pld struct { + Level zapcore.Level `json:"level"` + } + require.NoError(t, json.NewDecoder(res.Body).Decode(&pld), "Decoding response body") + assert.Equal(t, tt.expectedLevel, pld.Level, "Unexpected logging level returned") + }) + } } diff --git a/increase_level_test.go b/increase_level_test.go new file mode 100644 index 000000000..2d883807e --- /dev/null +++ b/increase_level_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zap + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func newLoggedEntry(level zapcore.Level, msg string, fields ...zapcore.Field) observer.LoggedEntry { + if len(fields) == 0 { + fields = []zapcore.Field{} + } + return observer.LoggedEntry{ + Entry: zapcore.Entry{Level: level, Message: msg}, + Context: fields, + } +} + +func TestIncreaseLevelTryDecrease(t *testing.T) { + errorOut := &bytes.Buffer{} + opts := []Option{ + ErrorOutput(zapcore.AddSync(errorOut)), + } + withLogger(t, WarnLevel, opts, func(logger *Logger, logs *observer.ObservedLogs) { + logger.Warn("original warn log") + + debugLogger := logger.WithOptions(IncreaseLevel(DebugLevel)) + debugLogger.Debug("ignored debug log") + debugLogger.Warn("increase level warn log") + debugLogger.Error("increase level error log") + + assert.Equal(t, []observer.LoggedEntry{ + newLoggedEntry(WarnLevel, "original warn log"), + newLoggedEntry(WarnLevel, "increase level warn log"), + newLoggedEntry(ErrorLevel, "increase level error log"), + }, logs.AllUntimed(), "unexpected logs") + assert.Equal(t, + "failed to IncreaseLevel: invalid increase level, as level \"info\" is allowed by increased level, but not by existing core\n", + errorOut.String(), + "unexpected error output", + ) + }) +} + +func TestIncreaseLevel(t *testing.T) { + errorOut := &bytes.Buffer{} + opts := []Option{ + ErrorOutput(zapcore.AddSync(errorOut)), + } + withLogger(t, WarnLevel, opts, func(logger *Logger, logs *observer.ObservedLogs) { + logger.Warn("original warn log") + + errorLogger := logger.WithOptions(IncreaseLevel(ErrorLevel)) + errorLogger.Debug("ignored debug log") + errorLogger.Warn("ignored warn log") + errorLogger.Error("increase level error log") + + withFields := errorLogger.With(String("k", "v")) + withFields.Debug("ignored debug log with fields") + withFields.Warn("ignored warn log with fields") + withFields.Error("increase level error log with fields") + + assert.Equal(t, []observer.LoggedEntry{ + newLoggedEntry(WarnLevel, "original warn log"), + newLoggedEntry(ErrorLevel, "increase level error log"), + newLoggedEntry(ErrorLevel, "increase level error log with fields", String("k", "v")), + }, logs.AllUntimed(), "unexpected logs") + + assert.Empty(t, errorOut.String(), "expect no error output") + }) +} diff --git a/internal/readme/readme.go b/internal/readme/readme.go index 55414573e..691005df9 100644 --- a/internal/readme/readme.go +++ b/internal/readme/readme.go @@ -43,7 +43,6 @@ var ( "go-kit/kit/log": "go-kit", "inconshreveable/log15": "log15", "apex/log": "apex/log", - "go.pedge.io/lion": "lion", "rs/zerolog": "zerolog", } ) @@ -96,9 +95,18 @@ func getBenchmarkRows(benchmarkName string) (string, error) { if err != nil { return "", err } + + // get the Zap time (unsugared) as baseline to compare with other loggers + baseline, err := getBenchmarkRow(benchmarkOutput, benchmarkName, "Zap", nil) + if err != nil { + return "", err + } + var benchmarkRows []*benchmarkRow for libraryName := range libraryNameToMarkdownName { - benchmarkRow, err := getBenchmarkRow(benchmarkOutput, benchmarkName, libraryName) + benchmarkRow, err := getBenchmarkRow( + benchmarkOutput, benchmarkName, libraryName, baseline, + ) if err != nil { return "", err } @@ -109,8 +117,8 @@ func getBenchmarkRows(benchmarkName string) (string, error) { } sort.Sort(benchmarkRowsByTime(benchmarkRows)) rows := []string{ - "| Package | Time | Objects Allocated |", - "| :--- | :---: | :---: |", + "| Package | Time | Time % to zap | Objects Allocated |", + "| :------ | :--: | :-----------: | :---------------: |", } for _, benchmarkRow := range benchmarkRows { rows = append(rows, benchmarkRow.String()) @@ -118,7 +126,9 @@ func getBenchmarkRows(benchmarkName string) (string, error) { return strings.Join(rows, "\n"), nil } -func getBenchmarkRow(input []string, benchmarkName string, libraryName string) (*benchmarkRow, error) { +func getBenchmarkRow( + input []string, benchmarkName string, libraryName string, baseline *benchmarkRow, +) (*benchmarkRow, error) { line, err := findUniqueSubstring(input, fmt.Sprintf("%s/%s-", benchmarkName, libraryName)) if err != nil { return nil, err @@ -142,12 +152,20 @@ func getBenchmarkRow(input []string, benchmarkName string, libraryName string) ( if err != nil { return nil, err } - return &benchmarkRow{ - libraryNameToMarkdownName[libraryName], - duration, - allocatedBytes, - allocatedObjects, - }, nil + r := &benchmarkRow{ + Name: libraryNameToMarkdownName[libraryName], + Time: duration, + AllocatedBytes: allocatedBytes, + AllocatedObjects: allocatedObjects, + } + + if baseline != nil { + r.ZapTime = baseline.Time + r.ZapAllocatedBytes = baseline.AllocatedBytes + r.ZapAllocatedObjects = baseline.AllocatedObjects + } + + return r, nil } func findUniqueSubstring(input []string, substring string) (string, error) { @@ -164,13 +182,11 @@ func findUniqueSubstring(input []string, substring string) (string, error) { } func getBenchmarkOutput(benchmarkName string) ([]string, error) { - return getOutput("go", "test", fmt.Sprintf("-bench=%s", benchmarkName), "-benchmem", "./benchmarks") -} - -func getOutput(name string, arg ...string) ([]string, error) { - output, err := exec.Command(name, arg...).CombinedOutput() + cmd := exec.Command("go", "test", fmt.Sprintf("-bench=%s", benchmarkName), "-benchmem") + cmd.Dir = "benchmarks" + output, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("error running %s %s: %v\n%s", name, strings.Join(arg, " "), err, string(output)) + return nil, fmt.Errorf("error running 'go test -bench=%q': %v\n%s", benchmarkName, err, string(output)) } return strings.Split(string(output), "\n"), nil } @@ -182,14 +198,31 @@ type tmplData struct { } type benchmarkRow struct { - Name string + Name string + Time time.Duration AllocatedBytes int AllocatedObjects int + + ZapTime time.Duration + ZapAllocatedBytes int + ZapAllocatedObjects int } func (b *benchmarkRow) String() string { - return fmt.Sprintf("| %s | %d ns/op | %d allocs/op |", b.Name, b.Time.Nanoseconds(), b.AllocatedObjects) + pct := func(val, baseline int64) string { + return fmt.Sprintf( + "%+0.f%%", + ((float64(val)/float64(baseline))*100)-100, + ) + } + t := b.Time.Nanoseconds() + tp := pct(t, b.ZapTime.Nanoseconds()) + + return fmt.Sprintf( + "| %s | %d ns/op | %s | %d allocs/op", b.Name, + t, tp, b.AllocatedObjects, + ) } type benchmarkRowsByTime []*benchmarkRow @@ -201,7 +234,7 @@ func (b benchmarkRowsByTime) Less(i, j int) bool { leftZap, rightZap := strings.Contains(left.Name, "zap"), strings.Contains(right.Name, "zap") // If neither benchmark is for zap or both are, sort by time. - if !(leftZap || rightZap) || (leftZap && rightZap) { + if leftZap == rightZap { return left.Time.Nanoseconds() < right.Time.Nanoseconds() } // Sort zap benchmark first. diff --git a/benchmarks/lion_test.go b/leak_test.go similarity index 85% rename from benchmarks/lion_test.go rename to leak_test.go index 6c41cb110..474ed2f2e 100644 --- a/benchmarks/lion_test.go +++ b/leak_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Uber Technologies, Inc. +// Copyright (c) 2021 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -18,14 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -package benchmarks +package zap import ( - "io/ioutil" + "testing" - "go.pedge.io/lion" + "go.uber.org/goleak" ) -func newLion() lion.Logger { - return lion.NewLogger(lion.NewJSONWritePusher(ioutil.Discard)) +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) } diff --git a/logger.go b/logger.go index dc8f6e3a4..c6ab4b0ef 100644 --- a/logger.go +++ b/logger.go @@ -26,7 +26,6 @@ import ( "os" "runtime" "strings" - "time" "go.uber.org/zap/zapcore" ) @@ -42,13 +41,17 @@ type Logger struct { core zapcore.Core development bool + addCaller bool + onFatal zapcore.CheckWriteAction // default is WriteThenFatal + name string errorOutput zapcore.WriteSyncer - addCaller bool - addStack zapcore.LevelEnabler + addStack zapcore.LevelEnabler callerSkip int + + clock zapcore.Clock } // New constructs a new Logger from the provided zapcore.Core and Options. If @@ -69,6 +72,7 @@ func New(core zapcore.Core, options ...Option) *Logger { core: core, errorOutput: zapcore.Lock(os.Stderr), addStack: zapcore.FatalLevel + 1, + clock: zapcore.DefaultClock, } return log.WithOptions(options...) } @@ -258,11 +262,17 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { // (e.g., Check, Info, Fatal). const callerSkipOffset = 2 + // Check the level first to reduce the cost of disabled log calls. + // Since Panic and higher may exit, we skip the optimization for those levels. + if lvl < zapcore.DPanicLevel && !log.core.Enabled(lvl) { + return nil + } + // Create basic checked entry thru the core; this will be non-nil if the // log message will actually be written somewhere. ent := zapcore.Entry{ LoggerName: log.name, - Time: time.Now(), + Time: log.clock.Now(), Level: lvl, Message: msg, } @@ -274,7 +284,13 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { case zapcore.PanicLevel: ce = ce.Should(ent, zapcore.WriteThenPanic) case zapcore.FatalLevel: - ce = ce.Should(ent, zapcore.WriteThenFatal) + onFatal := log.onFatal + // Noop is the default value for CheckWriteAction, and it leads to + // continued execution after a Fatal which is unexpected. + if onFatal == zapcore.WriteThenNoop { + onFatal = zapcore.WriteThenFatal + } + ce = ce.Should(ent, onFatal) case zapcore.DPanicLevel: if log.development { ce = ce.Should(ent, zapcore.WriteThenPanic) @@ -291,15 +307,41 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { // Thread the error output through to the CheckedEntry. ce.ErrorOutput = log.errorOutput if log.addCaller { - ce.Entry.Caller = zapcore.NewEntryCaller(runtime.Caller(log.callerSkip + callerSkipOffset)) - if !ce.Entry.Caller.Defined { - fmt.Fprintf(log.errorOutput, "%v Logger.check error: failed to get caller\n", time.Now().UTC()) + frame, defined := getCallerFrame(log.callerSkip + callerSkipOffset) + if !defined { + fmt.Fprintf(log.errorOutput, "%v Logger.check error: failed to get caller\n", ent.Time.UTC()) log.errorOutput.Sync() } + + ce.Entry.Caller = zapcore.EntryCaller{ + Defined: defined, + PC: frame.PC, + File: frame.File, + Line: frame.Line, + Function: frame.Function, + } } if log.addStack.Enabled(ce.Entry.Level) { - ce.Entry.Stack = Stack("").String + ce.Entry.Stack = StackSkip("", log.callerSkip+callerSkipOffset).String } return ce } + +// getCallerFrame gets caller frame. The argument skip is the number of stack +// frames to ascend, with 0 identifying the caller of getCallerFrame. The +// boolean ok is false if it was not possible to recover the information. +// +// Note: This implementation is similar to runtime.Caller, but it returns the whole frame. +func getCallerFrame(skip int) (frame runtime.Frame, ok bool) { + const skipOffset = 2 // skip getCallerFrame and Callers + + pc := make([]uintptr, 1) + numFrames := runtime.Callers(skip+skipOffset, pc) + if numFrames < 1 { + return + } + + frame, _ = runtime.CallersFrames(pc).Next() + return frame, frame.PC != 0 +} diff --git a/logger_test.go b/logger_test.go index a9f705788..0fb0600e7 100644 --- a/logger_test.go +++ b/logger_test.go @@ -339,7 +339,12 @@ func TestLoggerAddCaller(t *testing.T) { options []Option pat string }{ + {opts(), `^undefined$`}, + {opts(WithCaller(false)), `^undefined$`}, {opts(AddCaller()), `.+/logger_test.go:[\d]+$`}, + {opts(AddCaller(), WithCaller(false)), `^undefined$`}, + {opts(WithCaller(true)), `.+/logger_test.go:[\d]+$`}, + {opts(WithCaller(true), WithCaller(false)), `^undefined$`}, {opts(AddCaller(), AddCallerSkip(1), AddCallerSkip(-1)), `.+/zap/logger_test.go:[\d]+$`}, {opts(AddCaller(), AddCallerSkip(1)), `.+/zap/common_test.go:[\d]+$`}, {opts(AddCaller(), AddCallerSkip(1), AddCallerSkip(3)), `.+/src/runtime/.*:[\d]+$`}, @@ -361,10 +366,89 @@ func TestLoggerAddCaller(t *testing.T) { } } +func TestLoggerAddCallerFunction(t *testing.T) { + tests := []struct { + options []Option + loggerFunction string + sugaredFunction string + }{ + { + options: opts(), + loggerFunction: "", + sugaredFunction: "", + }, + { + options: opts(WithCaller(false)), + loggerFunction: "", + sugaredFunction: "", + }, + { + options: opts(AddCaller()), + loggerFunction: "go.uber.org/zap.infoLog", + sugaredFunction: "go.uber.org/zap.infoLogSugared", + }, + { + options: opts(AddCaller(), WithCaller(false)), + loggerFunction: "", + sugaredFunction: "", + }, + { + options: opts(WithCaller(true)), + loggerFunction: "go.uber.org/zap.infoLog", + sugaredFunction: "go.uber.org/zap.infoLogSugared", + }, + { + options: opts(WithCaller(true), WithCaller(false)), + loggerFunction: "", + sugaredFunction: "", + }, + { + options: opts(AddCaller(), AddCallerSkip(1), AddCallerSkip(-1)), + loggerFunction: "go.uber.org/zap.infoLog", + sugaredFunction: "go.uber.org/zap.infoLogSugared", + }, + { + options: opts(AddCaller(), AddCallerSkip(2)), + loggerFunction: "go.uber.org/zap.withLogger", + sugaredFunction: "go.uber.org/zap.withLogger", + }, + { + options: opts(AddCaller(), AddCallerSkip(2), AddCallerSkip(3)), + loggerFunction: "runtime.goexit", + sugaredFunction: "runtime.goexit", + }, + } + for _, tt := range tests { + withLogger(t, DebugLevel, tt.options, func(logger *Logger, logs *observer.ObservedLogs) { + // Make sure that sugaring and desugaring resets caller skip properly. + logger = logger.Sugar().Desugar() + infoLog(logger, "") + infoLogSugared(logger.Sugar(), "") + infoLog(logger.Sugar().Desugar(), "") + + entries := logs.AllUntimed() + assert.Equal(t, 3, len(entries), "Unexpected number of logs written out.") + for _, entry := range []observer.LoggedEntry{entries[0], entries[2]} { + assert.Regexp( + t, + tt.loggerFunction, + entry.Entry.Caller.Function, + "Expected to find function name in output.", + ) + } + assert.Regexp( + t, + tt.sugaredFunction, + entries[1].Entry.Caller.Function, + "Expected to find function name in output.", + ) + }) + } +} + func TestLoggerAddCallerFail(t *testing.T) { errBuf := &ztest.Buffer{} - withLogger(t, DebugLevel, opts(AddCaller(), ErrorOutput(errBuf)), func(log *Logger, logs *observer.ObservedLogs) { - log.callerSkip = 1e3 + withLogger(t, DebugLevel, opts(AddCaller(), AddCallerSkip(1e3), ErrorOutput(errBuf)), func(log *Logger, logs *observer.ObservedLogs) { log.Info("Failure.") assert.Regexp( t, @@ -377,6 +461,11 @@ func TestLoggerAddCallerFail(t *testing.T) { logs.AllUntimed()[0].Entry.Message, "Failure.", "Expected original message to survive failures in runtime.Caller.") + assert.Equal( + t, + logs.AllUntimed()[0].Entry.Caller.Function, + "", + "Expected function name to be empty string.") }) } @@ -392,6 +481,21 @@ func TestLoggerReplaceCore(t *testing.T) { }) } +func TestLoggerIncreaseLevel(t *testing.T) { + withLogger(t, DebugLevel, opts(IncreaseLevel(WarnLevel)), func(logger *Logger, logs *observer.ObservedLogs) { + logger.Info("logger.Info") + logger.Warn("logger.Warn") + logger.Error("logger.Error") + require.Equal(t, 2, logs.Len(), "expected only warn + error logs due to IncreaseLevel.") + assert.Equal( + t, + logs.AllUntimed()[0].Entry.Message, + "logger.Warn", + "Expected first logged message to be warn level message", + ) + }) +} + func TestLoggerHooks(t *testing.T) { hook, seen := makeCountingHook() withLogger(t, DebugLevel, opts(Hooks(hook)), func(logger *Logger, logs *observer.ObservedLogs) { @@ -430,3 +534,56 @@ func TestLoggerConcurrent(t *testing.T) { } }) } + +func TestLoggerCustomOnFatal(t *testing.T) { + tests := []struct { + msg string + onFatal zapcore.CheckWriteAction + recoverValue interface{} + }{ + { + msg: "panic", + onFatal: zapcore.WriteThenPanic, + recoverValue: "fatal", + }, + { + msg: "goexit", + onFatal: zapcore.WriteThenGoexit, + recoverValue: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.msg, func(t *testing.T) { + withLogger(t, InfoLevel, opts(OnFatal(tt.onFatal)), func(logger *Logger, logs *observer.ObservedLogs) { + + var finished bool + recovered := make(chan interface{}) + go func() { + defer func() { + recovered <- recover() + }() + + logger.Fatal("fatal") + finished = true + }() + + assert.Equal(t, tt.recoverValue, <-recovered, "unexpected value from recover()") + assert.False(t, finished, "expect goroutine to not finish after Fatal") + + assert.Equal(t, []observer.LoggedEntry{{ + Entry: zapcore.Entry{Level: FatalLevel, Message: "fatal"}, + Context: []Field{}, + }}, logs.AllUntimed(), "unexpected logs") + }) + }) + } +} + +func infoLog(logger *Logger, msg string, fields ...Field) { + logger.Info(msg, fields...) +} + +func infoLogSugared(logger *SugaredLogger, args ...interface{}) { + logger.Info(args...) +} diff --git a/options.go b/options.go index 7a6b0fca1..e9e66161f 100644 --- a/options.go +++ b/options.go @@ -20,7 +20,11 @@ package zap -import "go.uber.org/zap/zapcore" +import ( + "fmt" + + "go.uber.org/zap/zapcore" +) // An Option configures a Logger. type Option interface { @@ -82,11 +86,18 @@ func Development() Option { }) } -// AddCaller configures the Logger to annotate each message with the filename -// and line number of zap's caller. +// AddCaller configures the Logger to annotate each message with the filename, +// line number, and function name of zap's caller. See also WithCaller. func AddCaller() Option { + return WithCaller(true) +} + +// WithCaller configures the Logger to annotate each message with the filename, +// line number, and function name of zap's caller, or not, depending on the +// value of enabled. This is a generalized form of AddCaller. +func WithCaller(enabled bool) Option { return optionFunc(func(log *Logger) { - log.addCaller = true + log.addCaller = enabled }) } @@ -107,3 +118,31 @@ func AddStacktrace(lvl zapcore.LevelEnabler) Option { log.addStack = lvl }) } + +// IncreaseLevel increase the level of the logger. It has no effect if +// the passed in level tries to decrease the level of the logger. +func IncreaseLevel(lvl zapcore.LevelEnabler) Option { + return optionFunc(func(log *Logger) { + core, err := zapcore.NewIncreaseLevelCore(log.core, lvl) + if err != nil { + fmt.Fprintf(log.errorOutput, "failed to IncreaseLevel: %v\n", err) + } else { + log.core = core + } + }) +} + +// OnFatal sets the action to take on fatal logs. +func OnFatal(action zapcore.CheckWriteAction) Option { + return optionFunc(func(log *Logger) { + log.onFatal = action + }) +} + +// WithClock specifies the clock used by the logger to determine the current +// time for logged entries. Defaults to the system clock with time.Now. +func WithClock(clock zapcore.Clock) Option { + return optionFunc(func(log *Logger) { + log.clock = clock + }) +} diff --git a/scripts/cover.sh b/scripts/cover.sh deleted file mode 100755 index 0da503cba..000000000 --- a/scripts/cover.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > cover.out - -for d in $(go list $@); do - go test -race -coverprofile=profile.out $d - if [ -f profile.out ]; then - cat profile.out >> cover.out - rm profile.out - fi -done diff --git a/sink.go b/sink.go index ff0becfe5..df46fa87a 100644 --- a/sink.go +++ b/sink.go @@ -136,7 +136,7 @@ func newFileSink(u *url.URL) (Sink, error) { case "stderr": return nopCloserSink{os.Stderr}, nil } - return os.OpenFile(u.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + return os.OpenFile(u.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) } func normalizeScheme(s string) (string, error) { diff --git a/stacktrace.go b/stacktrace.go index 100fac216..0cf8c1ddf 100644 --- a/stacktrace.go +++ b/stacktrace.go @@ -22,28 +22,20 @@ package zap import ( "runtime" - "strings" "sync" "go.uber.org/zap/internal/bufferpool" ) -const _zapPackage = "go.uber.org/zap" - var ( _stacktracePool = sync.Pool{ New: func() interface{} { return newProgramCounters(64) }, } - - // We add "." and "/" suffixes to the package name to ensure we only match - // the exact package and not any package with the same prefix. - _zapStacktracePrefixes = addPrefix(_zapPackage, ".", "/") - _zapStacktraceVendorContains = addPrefix("/vendor/", _zapStacktracePrefixes...) ) -func takeStacktrace() string { +func takeStacktrace(skip int) string { buffer := bufferpool.Get() defer buffer.Free() programCounters := _stacktracePool.Get().(*programCounters) @@ -51,9 +43,9 @@ func takeStacktrace() string { var numFrames int for { - // Skip the call to runtime.Counters and takeStacktrace so that the + // Skip the call to runtime.Callers and takeStacktrace so that the // program counters start at the caller of takeStacktrace. - numFrames = runtime.Callers(2, programCounters.pcs) + numFrames = runtime.Callers(skip+2, programCounters.pcs) if numFrames < len(programCounters.pcs) { break } @@ -63,19 +55,12 @@ func takeStacktrace() string { } i := 0 - skipZapFrames := true // skip all consecutive zap frames at the beginning. frames := runtime.CallersFrames(programCounters.pcs[:numFrames]) // Note: On the last iteration, frames.Next() returns false, with a valid // frame, but we ignore this frame. The last frame is a a runtime frame which // adds noise, since it's only either runtime.main or runtime.goexit. for frame, more := frames.Next(); more; frame, more = frames.Next() { - if skipZapFrames && isZapFrame(frame.Function) { - continue - } else { - skipZapFrames = false - } - if i != 0 { buffer.AppendByte('\n') } @@ -91,24 +76,6 @@ func takeStacktrace() string { return buffer.String() } -func isZapFrame(function string) bool { - for _, prefix := range _zapStacktracePrefixes { - if strings.HasPrefix(function, prefix) { - return true - } - } - - // We can't use a prefix match here since the location of the vendor - // directory affects the prefix. Instead we do a contains match. - for _, contains := range _zapStacktraceVendorContains { - if strings.Contains(function, contains) { - return true - } - } - - return false -} - type programCounters struct { pcs []uintptr } @@ -116,11 +83,3 @@ type programCounters struct { func newProgramCounters(size int) *programCounters { return &programCounters{make([]uintptr, size)} } - -func addPrefix(prefix string, ss ...string) []string { - withPrefix := make([]string, len(ss)) - for i, s := range ss { - withPrefix[i] = prefix + s - } - return withPrefix -} diff --git a/stacktrace_ext_test.go b/stacktrace_ext_test.go index b7e71b6b8..3b56070a9 100644 --- a/stacktrace_ext_test.go +++ b/stacktrace_ext_test.go @@ -22,6 +22,7 @@ package zap_test import ( "bytes" + "encoding/json" "io/ioutil" "os" "os/exec" @@ -84,10 +85,15 @@ func TestStacktraceFiltersZapMarshal(t *testing.T) { } func TestStacktraceFiltersVendorZap(t *testing.T) { - // We need to simulate a zap as a vendor library, so we're going to create a fake GOPATH - // and run the above test which will contain zap in the vendor directory. + // We already have the dependencies downloaded so this should be + // instant. + deps := downloadDependencies(t) + + // We need to simulate a zap as a vendor library, so we're going to + // create a fake GOPATH and run the above test which will contain zap + // in the vendor directory. withGoPath(t, func(goPath string) { - curDir, err := os.Getwd() + zapDir, err := os.Getwd() require.NoError(t, err, "Failed to get current directory") testDir := filepath.Join(goPath, "src/go.uber.org/zap_test/") @@ -95,28 +101,49 @@ func TestStacktraceFiltersVendorZap(t *testing.T) { require.NoError(t, os.MkdirAll(testDir, 0777), "Failed to create source director") curFile := getSelfFilename(t) - //copyFile(t, curFile, filepath.Join(testDir, curFile)) setupSymlink(t, curFile, filepath.Join(testDir, curFile)) // Set up symlinks for zap, and for any test dependencies. - setupSymlink(t, curDir, filepath.Join(vendorDir, "go.uber.org/zap")) - for _, testDep := range []string{"github.com/stretchr/testify"} { - target := filepath.Join(curDir, "vendor", testDep) - _, err := os.Stat(target) - require.NoError(t, err, "Required dependency (%v) not installed in vendor", target) - setupSymlink(t, target, filepath.Join(vendorDir, testDep)) + setupSymlink(t, zapDir, filepath.Join(vendorDir, "go.uber.org/zap")) + for _, dep := range deps { + setupSymlink(t, dep.Dir, filepath.Join(vendorDir, dep.ImportPath)) } - // Now run the above test which ensures we filter out zap stacktraces, but this time - // zap is in a vendor + // Now run the above test which ensures we filter out zap + // stacktraces, but this time zap is in a vendor cmd := exec.Command("go", "test", "-v", "-run", "TestStacktraceFiltersZap") cmd.Dir = testDir + cmd.Env = append(os.Environ(), "GO111MODULE=off") out, err := cmd.CombinedOutput() require.NoError(t, err, "Failed to run test in vendor directory, output: %s", out) assert.Contains(t, string(out), "PASS") }) } +func TestStacktraceWithoutCallerSkip(t *testing.T) { + withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) { + func() { + logger.Error("test log") + }() + + require.Contains(t, out.String(), "TestStacktraceWithoutCallerSkip.", "Should not skip too much") + verifyNoZap(t, out.String()) + }) +} + +func TestStacktraceWithCallerSkip(t *testing.T) { + withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) { + logger = logger.WithOptions(zap.AddCallerSkip(2)) + func() { + logger.Error("test log") + }() + + require.NotContains(t, out.String(), "TestStacktraceWithCallerSkip.", "Should skip as requested by caller skip") + require.Contains(t, out.String(), "TestStacktraceWithCallerSkip", "Should not skip too much") + verifyNoZap(t, out.String()) + }) +} + // withLogger sets up a logger with a real encoder set up, so that any marshal functions are called. // The inbuilt observer does not call Marshal for objects/arrays, which we need for some tests. func withLogger(t *testing.T, fn func(logger *zap.Logger, out *bytes.Buffer)) { @@ -162,3 +189,27 @@ func setupSymlink(t *testing.T, src, dst string) { require.NoError(t, os.Symlink(srcAbs, dst), "Failed to set up symlink") } + +type dependency struct { + ImportPath string `json:"Path"` // import path of the dependency + Dir string `json:"Dir"` // location on disk +} + +// Downloads all dependencies for the current Go module and reports their +// module paths and locations on disk. +func downloadDependencies(t *testing.T) []dependency { + cmd := exec.Command("go", "mod", "download", "-json") + + stdout, err := cmd.Output() + require.NoError(t, err, "Failed to run 'go mod download'") + + var deps []dependency + dec := json.NewDecoder(bytes.NewBuffer(stdout)) + for dec.More() { + var d dependency + require.NoError(t, dec.Decode(&d), "Failed to decode dependency") + deps = append(deps, d) + } + + return deps +} diff --git a/stacktrace_test.go b/stacktrace_test.go index 3c9a41cfd..d473029ee 100644 --- a/stacktrace_test.go +++ b/stacktrace_test.go @@ -29,47 +29,46 @@ import ( ) func TestTakeStacktrace(t *testing.T) { - trace := takeStacktrace() + trace := takeStacktrace(0) lines := strings.Split(trace, "\n") - require.True(t, len(lines) > 0, "Expected stacktrace to have at least one frame.") + require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.") assert.Contains( t, lines[0], - "testing.", - "Expected stacktrace to start with the test runner (zap frames are filtered out) %s.", lines[0], + "go.uber.org/zap.TestTakeStacktrace", + "Expected stacktrace to start with the test.", ) } -func TestIsZapFrame(t *testing.T) { - zapFrames := []string{ - "go.uber.org/zap.Stack", - "go.uber.org/zap.(*SugaredLogger).log", - "go.uber.org/zap/zapcore.(ArrayMarshalerFunc).MarshalLogArray", - "github.com/uber/tchannel-go/vendor/go.uber.org/zap.Stack", - "github.com/uber/tchannel-go/vendor/go.uber.org/zap.(*SugaredLogger).log", - "github.com/uber/tchannel-go/vendor/go.uber.org/zap/zapcore.(ArrayMarshalerFunc).MarshalLogArray", - } - nonZapFrames := []string{ - "github.com/uber/tchannel-go.NewChannel", - "go.uber.org/not-zap.New", - "go.uber.org/zapext.ctx", - "go.uber.org/zap_ext/ctx.New", - } +func TestTakeStacktraceWithSkip(t *testing.T) { + trace := takeStacktrace(1) + lines := strings.Split(trace, "\n") + require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.") + assert.Contains( + t, + lines[0], + "testing.", + "Expected stacktrace to start with the test runner (skipping our own frame).", + ) +} - t.Run("zap frames", func(t *testing.T) { - for _, f := range zapFrames { - require.True(t, isZapFrame(f), f) - } - }) - t.Run("non-zap frames", func(t *testing.T) { - for _, f := range nonZapFrames { - require.False(t, isZapFrame(f), f) - } - }) +func TestTakeStacktraceWithSkipInnerFunc(t *testing.T) { + var trace string + func() { + trace = takeStacktrace(2) + }() + lines := strings.Split(trace, "\n") + require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.") + assert.Contains( + t, + lines[0], + "testing.", + "Expected stacktrace to start with the test function (skipping the test function).", + ) } func BenchmarkTakeStacktrace(b *testing.B) { for i := 0; i < b.N; i++ { - takeStacktrace() + takeStacktrace(0) } } diff --git a/sugar.go b/sugar.go index 77ca227f4..0b9651981 100644 --- a/sugar.go +++ b/sugar.go @@ -222,19 +222,30 @@ func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interf return } - // Format with Sprint, Sprintf, or neither. - msg := template - if msg == "" && len(fmtArgs) > 0 { - msg = fmt.Sprint(fmtArgs...) - } else if msg != "" && len(fmtArgs) > 0 { - msg = fmt.Sprintf(template, fmtArgs...) - } - + msg := getMessage(template, fmtArgs) if ce := s.base.Check(lvl, msg); ce != nil { ce.Write(s.sweetenFields(context)...) } } +// getMessage format with Sprint, Sprintf, or neither. +func getMessage(template string, fmtArgs []interface{}) string { + if len(fmtArgs) == 0 { + return template + } + + if template != "" { + return fmt.Sprintf(template, fmtArgs...) + } + + if len(fmtArgs) == 1 { + if str, ok := fmtArgs[0].(string); ok { + return str + } + } + return fmt.Sprint(fmtArgs...) +} + func (s *SugaredLogger) sweetenFields(args []interface{}) []Field { if len(args) == 0 { return nil @@ -255,7 +266,7 @@ func (s *SugaredLogger) sweetenFields(args []interface{}) []Field { // Make sure this element isn't a dangling key. if i == len(args)-1 { - s.base.DPanic(_oddNumberErrMsg, Any("ignored", args[i])) + s.base.Error(_oddNumberErrMsg, Any("ignored", args[i])) break } @@ -276,7 +287,7 @@ func (s *SugaredLogger) sweetenFields(args []interface{}) []Field { // If we encountered any invalid key-value pairs, log an error. if len(invalid) > 0 { - s.base.DPanic(_nonStringKeyErrMsg, Array("invalid", invalid)) + s.base.Error(_nonStringKeyErrMsg, Array("invalid", invalid)) } return fields } diff --git a/sugar_test.go b/sugar_test.go index 5b1dcfacd..a68f7b5f5 100644 --- a/sugar_test.go +++ b/sugar_test.go @@ -36,13 +36,13 @@ func TestSugarWith(t *testing.T) { // Convenience functions to create expected error logs. ignored := func(msg interface{}) observer.LoggedEntry { return observer.LoggedEntry{ - Entry: zapcore.Entry{Level: DPanicLevel, Message: _oddNumberErrMsg}, + Entry: zapcore.Entry{Level: ErrorLevel, Message: _oddNumberErrMsg}, Context: []Field{Any("ignored", msg)}, } } nonString := func(pairs ...invalidPair) observer.LoggedEntry { return observer.LoggedEntry{ - Entry: zapcore.Entry{Level: DPanicLevel, Message: _nonStringKeyErrMsg}, + Entry: zapcore.Entry{Level: ErrorLevel, Message: _nonStringKeyErrMsg}, Context: []Field{Array("invalid", invalidPairs(pairs))}, } } @@ -159,10 +159,6 @@ func TestSugarFieldsInvalidPairs(t *testing.T) { }) } -type stringerF func() string - -func (f stringerF) String() string { return f() } - func TestSugarStructuredLogging(t *testing.T) { tests := []struct { msg string @@ -372,3 +368,11 @@ func TestSugarAddCallerFail(t *testing.T) { "Expected original message to survive failures in runtime.Caller.") }) } + +func BenchmarkSugarSingleStrArg(b *testing.B) { + withSugar(b, InfoLevel, nil /* opts* */, func(log *SugaredLogger, logs *observer.ObservedLogs) { + for i := 0; i < b.N; i++ { + log.Info("hello world") + } + }) +} diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 000000000..ecf102536 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,9 @@ +module go.uber.org/zap/tools + +require ( + golang.org/x/lint v0.0.0-20190930215403-16217165b5de + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 // indirect + honnef.co/go/tools v0.0.1-2019.2.3 +) + +go 1.13 diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 000000000..6273acd76 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,29 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 000000000..e12570210 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,29 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// +build tools + +package tools + +import ( + // Tools we use during development. + _ "golang.org/x/lint/golint" + _ "honnef.co/go/tools/cmd/staticcheck" +) diff --git a/writer_test.go b/writer_test.go index 0dc8312b4..b9b5389ca 100644 --- a/writer_test.go +++ b/writer_test.go @@ -72,8 +72,8 @@ func TestOpen(t *testing.T) { "open /baz/quux: no such file or directory", }, }, - {[]string{"file:///stderr"}, []string{"open /stderr: permission denied"}}, - {[]string{"file:///stdout"}, []string{"open /stdout: permission denied"}}, + {[]string{"file:///stderr"}, []string{"open /stderr:"}}, + {[]string{"file:///stdout"}, []string{"open /stdout:"}}, {[]string{"file://host01.test.com" + tempName}, []string{"empty or use localhost"}}, {[]string{"file://rms@localhost" + tempName}, []string{"user and password not allowed"}}, {[]string{"file://localhost" + tempName + "#foo"}, []string{"fragments not allowed"}}, diff --git a/zapcore/buffered_write_syncer.go b/zapcore/buffered_write_syncer.go new file mode 100644 index 000000000..0c1436f76 --- /dev/null +++ b/zapcore/buffered_write_syncer.go @@ -0,0 +1,188 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "bufio" + "sync" + "time" + + "go.uber.org/multierr" +) + +const ( + // _defaultBufferSize specifies the default size used by Buffer. + _defaultBufferSize = 256 * 1024 // 256 kB + + // _defaultFlushInterval specifies the default flush interval for + // Buffer. + _defaultFlushInterval = 30 * time.Second +) + +// A BufferedWriteSyncer is a WriteSyncer that buffers writes in-memory before +// flushing them to a wrapped WriteSyncer after reaching some limit, or at some +// fixed interval--whichever comes first. +// +// BufferedWriteSyncer is safe for concurrent use. You don't need to use +// zapcore.Lock for WriteSyncers with BufferedWriteSyncer. +type BufferedWriteSyncer struct { + // WS is the WriteSyncer around which BufferedWriteSyncer will buffer + // writes. + // + // This field is required. + WS WriteSyncer + + // Size specifies the maximum amount of data the writer will buffered + // before flushing. + // + // Defaults to 256 kB if unspecified. + Size int + + // FlushInterval specifies how often the writer should flush data if + // there have been no writes. + // + // Defaults to 30 seconds if unspecified. + FlushInterval time.Duration + + // Clock, if specified, provides control of the source of time for the + // writer. + // + // Defaults to the system clock. + Clock Clock + + // unexported fields for state + mu sync.Mutex + initialized bool // whether initialize() has run + writer *bufio.Writer + ticker *time.Ticker + stop chan struct{} // closed when flushLoop should stop + stopped bool // whether Stop() has run + done chan struct{} // closed when flushLoop has stopped +} + +func (s *BufferedWriteSyncer) initialize() { + size := s.Size + if size == 0 { + size = _defaultBufferSize + } + + flushInterval := s.FlushInterval + if flushInterval == 0 { + flushInterval = _defaultFlushInterval + } + + if s.Clock == nil { + s.Clock = DefaultClock + } + + s.ticker = s.Clock.NewTicker(flushInterval) + s.writer = bufio.NewWriterSize(s.WS, size) + s.stop = make(chan struct{}) + s.done = make(chan struct{}) + s.initialized = true + go s.flushLoop() +} + +// Write writes log data into buffer syncer directly, multiple Write calls will be batched, +// and log data will be flushed to disk when the buffer is full or periodically. +func (s *BufferedWriteSyncer) Write(bs []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.initialized { + s.initialize() + } + + // To avoid partial writes from being flushed, we manually flush the existing buffer if: + // * The current write doesn't fit into the buffer fully, and + // * The buffer is not empty (since bufio will not split large writes when the buffer is empty) + if len(bs) > s.writer.Available() && s.writer.Buffered() > 0 { + if err := s.writer.Flush(); err != nil { + return 0, err + } + } + + return s.writer.Write(bs) +} + +// Sync flushes buffered log data into disk directly. +func (s *BufferedWriteSyncer) Sync() error { + s.mu.Lock() + defer s.mu.Unlock() + + var err error + if s.initialized { + err = s.writer.Flush() + } + + return multierr.Append(err, s.WS.Sync()) +} + +// flushLoop flushes the buffer at the configured interval until Stop is +// called. +func (s *BufferedWriteSyncer) flushLoop() { + defer close(s.done) + + for { + select { + case <-s.ticker.C: + // we just simply ignore error here + // because the underlying bufio writer stores any errors + // and we return any error from Sync() as part of the close + _ = s.Sync() + case <-s.stop: + return + } + } +} + +// Stop closes the buffer, cleans up background goroutines, and flushes +// remaining unwritten data. +func (s *BufferedWriteSyncer) Stop() (err error) { + var stopped bool + + // Critical section. + func() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.initialized { + return + } + + stopped = s.stopped + if stopped { + return + } + s.stopped = true + + s.ticker.Stop() + close(s.stop) // tell flushLoop to stop + <-s.done // and wait until it has + }() + + // Don't call Sync on consecutive Stops. + if !stopped { + err = s.Sync() + } + + return err +} diff --git a/zapcore/buffered_write_syncer_bench_test.go b/zapcore/buffered_write_syncer_bench_test.go new file mode 100644 index 000000000..dd1583c14 --- /dev/null +++ b/zapcore/buffered_write_syncer_bench_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func BenchmarkBufferedWriteSyncer(b *testing.B) { + b.Run("write file with buffer", func(b *testing.B) { + file, err := ioutil.TempFile("", "log") + require.NoError(b, err) + + defer func() { + assert.NoError(b, file.Close()) + assert.NoError(b, os.Remove(file.Name())) + }() + + w := &BufferedWriteSyncer{ + WS: AddSync(file), + } + defer w.Stop() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + w.Write([]byte("foobarbazbabble")) + } + }) + }) +} diff --git a/zapcore/buffered_write_syncer_test.go b/zapcore/buffered_write_syncer_test.go new file mode 100644 index 000000000..72d4d6f88 --- /dev/null +++ b/zapcore/buffered_write_syncer_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/internal/ztest" +) + +func TestBufferWriter(t *testing.T) { + // If we pass a plain io.Writer, make sure that we still get a WriteSyncer + // with a no-op Sync. + t.Run("sync", func(t *testing.T) { + buf := &bytes.Buffer{} + ws := &BufferedWriteSyncer{WS: AddSync(buf)} + + requireWriteWorks(t, ws) + assert.Empty(t, buf.String(), "Unexpected log calling a no-op Write method.") + assert.NoError(t, ws.Sync(), "Unexpected error calling a no-op Sync method.") + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + assert.NoError(t, ws.Stop()) + }) + + t.Run("stop", func(t *testing.T) { + buf := &bytes.Buffer{} + ws := &BufferedWriteSyncer{WS: AddSync(buf)} + requireWriteWorks(t, ws) + assert.Empty(t, buf.String(), "Unexpected log calling a no-op Write method.") + assert.NoError(t, ws.Stop()) + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + }) + + t.Run("stop twice", func(t *testing.T) { + ws := &BufferedWriteSyncer{WS: &ztest.FailWriter{}} + _, err := ws.Write([]byte("foo")) + require.NoError(t, err, "Unexpected error writing to WriteSyncer.") + assert.Error(t, ws.Stop(), "Expected stop to fail.") + assert.NoError(t, ws.Stop(), "Expected stop to not fail.") + }) + + t.Run("wrap twice", func(t *testing.T) { + buf := &bytes.Buffer{} + bufsync := &BufferedWriteSyncer{WS: AddSync(buf)} + ws := &BufferedWriteSyncer{WS: bufsync} + requireWriteWorks(t, ws) + assert.Empty(t, buf.String(), "Unexpected log calling a no-op Write method.") + require.NoError(t, ws.Sync()) + assert.Equal(t, "foo", buf.String()) + assert.NoError(t, ws.Stop()) + assert.NoError(t, bufsync.Stop()) + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + }) + + t.Run("small buffer", func(t *testing.T) { + buf := &bytes.Buffer{} + ws := &BufferedWriteSyncer{WS: AddSync(buf), Size: 5} + + requireWriteWorks(t, ws) + assert.Equal(t, "", buf.String(), "Unexpected log calling a no-op Write method.") + requireWriteWorks(t, ws) + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + assert.NoError(t, ws.Stop()) + }) + + t.Run("with lockedWriteSyncer", func(t *testing.T) { + buf := &bytes.Buffer{} + ws := &BufferedWriteSyncer{WS: Lock(AddSync(buf)), Size: 5} + + requireWriteWorks(t, ws) + assert.Equal(t, "", buf.String(), "Unexpected log calling a no-op Write method.") + requireWriteWorks(t, ws) + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + assert.NoError(t, ws.Stop()) + }) + + t.Run("flush error", func(t *testing.T) { + ws := &BufferedWriteSyncer{WS: &ztest.FailWriter{}, Size: 4} + n, err := ws.Write([]byte("foo")) + require.NoError(t, err, "Unexpected error writing to WriteSyncer.") + require.Equal(t, 3, n, "Wrote an unexpected number of bytes.") + ws.Write([]byte("foo")) + assert.Error(t, ws.Stop(), "Expected stop to fail.") + }) + + t.Run("flush timer", func(t *testing.T) { + buf := &bytes.Buffer{} + clock := newControlledClock() + ws := &BufferedWriteSyncer{ + WS: AddSync(buf), + Size: 6, + FlushInterval: time.Microsecond, + Clock: clock, + } + requireWriteWorks(t, ws) + clock.Add(10 * time.Microsecond) + assert.Equal(t, "foo", buf.String(), "Unexpected log string") + + // flush twice to validate loop logic + requireWriteWorks(t, ws) + clock.Add(10 * time.Microsecond) + assert.Equal(t, "foofoo", buf.String(), "Unexpected log string") + assert.NoError(t, ws.Stop()) + }) +} + +func TestBufferWriterWithoutStart(t *testing.T) { + t.Run("stop", func(t *testing.T) { + ws := &BufferedWriteSyncer{WS: AddSync(new(bytes.Buffer))} + assert.NoError(t, ws.Stop(), "Stop must not fail") + }) + + t.Run("Sync", func(t *testing.T) { + ws := &BufferedWriteSyncer{WS: AddSync(new(bytes.Buffer))} + assert.NoError(t, ws.Sync(), "Sync must not fail") + }) +} diff --git a/zapcore/clock.go b/zapcore/clock.go new file mode 100644 index 000000000..d2ea95b39 --- /dev/null +++ b/zapcore/clock.go @@ -0,0 +1,50 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "time" +) + +// DefaultClock is the default clock used by Zap in operations that require +// time. This clock uses the system clock for all operations. +var DefaultClock = systemClock{} + +// Clock is a source of time for logged entries. +type Clock interface { + // Now returns the current local time. + Now() time.Time + + // NewTicker returns *time.Ticker that holds a channel + // that delivers "ticks" of a clock. + NewTicker(time.Duration) *time.Ticker +} + +// systemClock implements default Clock that uses system time. +type systemClock struct{} + +func (systemClock) Now() time.Time { + return time.Now() +} + +func (systemClock) NewTicker(duration time.Duration) *time.Ticker { + return time.NewTicker(duration) +} diff --git a/zapcore/clock_test.go b/zapcore/clock_test.go new file mode 100644 index 000000000..aab682fec --- /dev/null +++ b/zapcore/clock_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" +) + +// controlledClock provides control over the time via a mock clock. +type controlledClock struct{ *clock.Mock } + +func newControlledClock() *controlledClock { + return &controlledClock{clock.NewMock()} +} + +func (c *controlledClock) NewTicker(d time.Duration) *time.Ticker { + return &time.Ticker{C: c.Ticker(d).C} +} + +func TestControlledClock_NewTicker(t *testing.T) { + var n atomic.Int32 + ctrlMock := newControlledClock() + + done := make(chan struct{}) + defer func() { <-done }() // wait for end + + quit := make(chan struct{}) + // Create a channel to increment every microsecond. + go func(ticker *time.Ticker) { + defer close(done) + for { + select { + case <-quit: + ticker.Stop() + return + case <-ticker.C: + n.Inc() + } + } + }(ctrlMock.NewTicker(time.Microsecond)) + + // Move clock forward. + ctrlMock.Add(2 * time.Microsecond) + assert.Equal(t, int32(2), n.Load()) + close(quit) +} + +func TestSystemClock_NewTicker(t *testing.T) { + want := 3 + + var n int + timer := DefaultClock.NewTicker(time.Millisecond) + for range timer.C { + n++ + if n == want { + return + } + } +} diff --git a/zapcore/console_encoder.go b/zapcore/console_encoder.go index b7875966f..2307af404 100644 --- a/zapcore/console_encoder.go +++ b/zapcore/console_encoder.go @@ -56,6 +56,10 @@ type consoleEncoder struct { // encoder configuration, it will omit any element whose key is set to the empty // string. func NewConsoleEncoder(cfg EncoderConfig) Encoder { + if cfg.ConsoleSeparator == "" { + // Use a default delimiter of '\t' for backwards compatibility + cfg.ConsoleSeparator = "\t" + } return consoleEncoder{newJSONEncoder(cfg, true)} } @@ -89,12 +93,17 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, nameEncoder(ent.LoggerName, arr) } - if ent.Caller.Defined && c.CallerKey != "" && c.EncodeCaller != nil { - c.EncodeCaller(ent.Caller, arr) + if ent.Caller.Defined { + if c.CallerKey != "" && c.EncodeCaller != nil { + c.EncodeCaller(ent.Caller, arr) + } + if c.FunctionKey != "" { + arr.AppendString(ent.Caller.Function) + } } for i := range arr.elems { if i > 0 { - line.AppendByte('\t') + line.AppendString(c.ConsoleSeparator) } fmt.Fprint(line, arr.elems[i]) } @@ -102,7 +111,7 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, // Add the message itself. if c.MessageKey != "" { - c.addTabIfNecessary(line) + c.addSeparatorIfNecessary(line) line.AppendString(ent.Message) } @@ -126,7 +135,12 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []Field) { context := c.jsonEncoder.Clone().(*jsonEncoder) - defer context.buf.Free() + defer func() { + // putJSONEncoder assumes the buffer is still used, but we write out the buffer so + // we can free it. + context.buf.Free() + putJSONEncoder(context) + }() addFields(context, extra) context.closeOpenNamespaces() @@ -134,14 +148,14 @@ func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []Field) { return } - c.addTabIfNecessary(line) + c.addSeparatorIfNecessary(line) line.AppendByte('{') line.Write(context.buf.Bytes()) line.AppendByte('}') } -func (c consoleEncoder) addTabIfNecessary(line *buffer.Buffer) { +func (c consoleEncoder) addSeparatorIfNecessary(line *buffer.Buffer) { if line.Len() > 0 { - line.AppendByte('\t') + line.AppendString(c.ConsoleSeparator) } } diff --git a/zapcore/console_encoder_test.go b/zapcore/console_encoder_test.go new file mode 100644 index 000000000..b03f1a728 --- /dev/null +++ b/zapcore/console_encoder_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package zapcore_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + . "go.uber.org/zap/zapcore" +) + +var ( + testEntry = Entry{ + LoggerName: "main", + Level: InfoLevel, + Message: `hello`, + Time: _epoch, + Stack: "fake-stack", + Caller: EntryCaller{Defined: true, File: "foo.go", Line: 42, Function: "foo.Foo"}, + } +) + +func TestConsoleSeparator(t *testing.T) { + tests := []struct { + desc string + separator string + wantConsole string + }{ + { + desc: "space console separator", + separator: " ", + wantConsole: "0 info main foo.go:42 foo.Foo hello\nfake-stack\n", + }, + { + desc: "default console separator", + separator: "", + wantConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", + }, + { + desc: "tag console separator", + separator: "\t", + wantConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", + }, + { + desc: "dash console separator", + separator: "--", + wantConsole: "0--info--main--foo.go:42--foo.Foo--hello\nfake-stack\n", + }, + } + + for _, tt := range tests { + console := NewConsoleEncoder(encoderTestEncoderConfig(tt.separator)) + t.Run(tt.desc, func(t *testing.T) { + entry := testEntry + consoleOut, err := console.EncodeEntry(entry, nil) + if !assert.NoError(t, err) { + return + } + assert.Equal( + t, + tt.wantConsole, + consoleOut.String(), + "Unexpected console output", + ) + }) + + } +} + +func encoderTestEncoderConfig(separator string) EncoderConfig { + testEncoder := testEncoderConfig() + testEncoder.ConsoleSeparator = separator + return testEncoder +} diff --git a/zapcore/encoder.go b/zapcore/encoder.go index f0509522b..6601ca166 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -21,6 +21,7 @@ package zapcore import ( + "encoding/json" "time" "go.uber.org/zap/buffer" @@ -31,6 +32,9 @@ import ( // behavior. const DefaultLineEnding = "\n" +// OmitKey defines the key to use when callers want to remove a key from log output. +const OmitKey = "" + // A LevelEncoder serializes a Level to a primitive type. type LevelEncoder func(Level, PrimitiveArrayEncoder) @@ -109,17 +113,66 @@ func EpochNanosTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendInt64(t.UnixNano()) } +func encodeTimeLayout(t time.Time, layout string, enc PrimitiveArrayEncoder) { + type appendTimeEncoder interface { + AppendTimeLayout(time.Time, string) + } + + if enc, ok := enc.(appendTimeEncoder); ok { + enc.AppendTimeLayout(t, layout) + return + } + + enc.AppendString(t.Format(layout)) +} + // ISO8601TimeEncoder serializes a time.Time to an ISO8601-formatted string // with millisecond precision. +// +// If enc supports AppendTimeLayout(t time.Time,layout string), it's used +// instead of appending a pre-formatted string value. func ISO8601TimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { - enc.AppendString(t.Format("2006-01-02T15:04:05.000Z0700")) + encodeTimeLayout(t, "2006-01-02T15:04:05.000Z0700", enc) +} + +// RFC3339TimeEncoder serializes a time.Time to an RFC3339-formatted string. +// +// If enc supports AppendTimeLayout(t time.Time,layout string), it's used +// instead of appending a pre-formatted string value. +func RFC3339TimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { + encodeTimeLayout(t, time.RFC3339, enc) +} + +// RFC3339NanoTimeEncoder serializes a time.Time to an RFC3339-formatted string +// with nanosecond precision. +// +// If enc supports AppendTimeLayout(t time.Time,layout string), it's used +// instead of appending a pre-formatted string value. +func RFC3339NanoTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { + encodeTimeLayout(t, time.RFC3339Nano, enc) } -// UnmarshalText unmarshals text to a TimeEncoder. "iso8601" and "ISO8601" are -// unmarshaled to ISO8601TimeEncoder, "millis" is unmarshaled to -// EpochMillisTimeEncoder, and anything else is unmarshaled to EpochTimeEncoder. +// TimeEncoderOfLayout returns TimeEncoder which serializes a time.Time using +// given layout. +func TimeEncoderOfLayout(layout string) TimeEncoder { + return func(t time.Time, enc PrimitiveArrayEncoder) { + encodeTimeLayout(t, layout, enc) + } +} + +// UnmarshalText unmarshals text to a TimeEncoder. +// "rfc3339nano" and "RFC3339Nano" are unmarshaled to RFC3339NanoTimeEncoder. +// "rfc3339" and "RFC3339" are unmarshaled to RFC3339TimeEncoder. +// "iso8601" and "ISO8601" are unmarshaled to ISO8601TimeEncoder. +// "millis" is unmarshaled to EpochMillisTimeEncoder. +// "nanos" is unmarshaled to EpochNanosEncoder. +// Anything else is unmarshaled to EpochTimeEncoder. func (e *TimeEncoder) UnmarshalText(text []byte) error { switch string(text) { + case "rfc3339nano", "RFC3339Nano": + *e = RFC3339NanoTimeEncoder + case "rfc3339", "RFC3339": + *e = RFC3339TimeEncoder case "iso8601", "ISO8601": *e = ISO8601TimeEncoder case "millis": @@ -132,6 +185,35 @@ func (e *TimeEncoder) UnmarshalText(text []byte) error { return nil } +// UnmarshalYAML unmarshals YAML to a TimeEncoder. +// If value is an object with a "layout" field, it will be unmarshaled to TimeEncoder with given layout. +// timeEncoder: +// layout: 06/01/02 03:04pm +// If value is string, it uses UnmarshalText. +// timeEncoder: iso8601 +func (e *TimeEncoder) UnmarshalYAML(unmarshal func(interface{}) error) error { + var o struct { + Layout string `json:"layout" yaml:"layout"` + } + if err := unmarshal(&o); err == nil { + *e = TimeEncoderOfLayout(o.Layout) + return nil + } + + var s string + if err := unmarshal(&s); err != nil { + return err + } + return e.UnmarshalText([]byte(s)) +} + +// UnmarshalJSON unmarshals JSON to a TimeEncoder as same way UnmarshalYAML does. +func (e *TimeEncoder) UnmarshalJSON(data []byte) error { + return e.UnmarshalYAML(func(v interface{}) error { + return json.Unmarshal(data, v) + }) +} + // A DurationEncoder serializes a time.Duration to a primitive type. type DurationEncoder func(time.Duration, PrimitiveArrayEncoder) @@ -146,6 +228,12 @@ func NanosDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) { enc.AppendInt64(int64(d)) } +// MillisDurationEncoder serializes a time.Duration to an integer number of +// milliseconds elapsed. +func MillisDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) { + enc.AppendInt64(d.Nanoseconds() / 1e6) +} + // StringDurationEncoder serializes a time.Duration using its built-in String // method. func StringDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) { @@ -161,6 +249,8 @@ func (e *DurationEncoder) UnmarshalText(text []byte) error { *e = StringDurationEncoder case "nanos": *e = NanosDurationEncoder + case "ms": + *e = MillisDurationEncoder default: *e = SecondsDurationEncoder } @@ -227,6 +317,7 @@ type EncoderConfig struct { TimeKey string `json:"timeKey" yaml:"timeKey"` NameKey string `json:"nameKey" yaml:"nameKey"` CallerKey string `json:"callerKey" yaml:"callerKey"` + FunctionKey string `json:"functionKey" yaml:"functionKey"` StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"` LineEnding string `json:"lineEnding" yaml:"lineEnding"` // Configure the primitive representations of common complex types. For @@ -239,6 +330,9 @@ type EncoderConfig struct { // Unlike the other primitive type encoders, EncodeName is optional. The // zero value falls back to FullNameEncoder. EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` + // Configures the field separator used by the console encoder. Defaults + // to tab. + ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"` } // ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a @@ -272,8 +366,8 @@ type ObjectEncoder interface { AddUint8(key string, value uint8) AddUintptr(key string, value uintptr) - // AddReflected uses reflection to serialize arbitrary objects, so it's slow - // and allocation-heavy. + // AddReflected uses reflection to serialize arbitrary objects, so it can be + // slow and allocation-heavy. AddReflected(key string, value interface{}) error // OpenNamespace opens an isolated namespace where all subsequent fields will // be added. Applications can use namespaces to prevent key collisions when @@ -343,6 +437,7 @@ type Encoder interface { Clone() Encoder // EncodeEntry encodes an entry and fields, along with any accumulated - // context, into a byte buffer and returns it. + // context, into a byte buffer and returns it. Any fields that are empty, + // including fields on the `Entry` type, should be omitted. EncodeEntry(Entry, []Field) (*buffer.Buffer, error) } diff --git a/zapcore/encoder_test.go b/zapcore/encoder_test.go index 04641678c..b53a90849 100644 --- a/zapcore/encoder_test.go +++ b/zapcore/encoder_test.go @@ -21,12 +21,14 @@ package zapcore_test import ( + "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" . "go.uber.org/zap/zapcore" ) @@ -39,7 +41,7 @@ var ( Message: `hello`, Time: _epoch, Stack: "fake-stack", - Caller: EntryCaller{Defined: true, File: "foo.go", Line: 42}, + Caller: EntryCaller{Defined: true, File: "foo.go", Line: 42, Function: "foo.Foo"}, } ) @@ -50,6 +52,7 @@ func testEncoderConfig() EncoderConfig { NameKey: "name", TimeKey: "ts", CallerKey: "caller", + FunctionKey: "func", StacktraceKey: "stacktrace", LineEnding: "\n", EncodeTime: EpochTimeEncoder, @@ -67,14 +70,6 @@ func humanEncoderConfig() EncoderConfig { return cfg } -func withJSONEncoder(f func(Encoder)) { - f(NewJSONEncoder(testEncoderConfig())) -} - -func withConsoleEncoder(f func(Encoder)) { - f(NewConsoleEncoder(humanEncoderConfig())) -} - func capitalNameEncoder(loggerName string, enc PrimitiveArrayEncoder) { enc.AppendString(strings.ToUpper(loggerName)) } @@ -97,8 +92,8 @@ func TestEncoderConfiguration(t *testing.T) { ent.Message = `hello\` return ent }, - expectedJSON: `{"level":"info","ts":0,"name":"main","caller":"foo.go:42","msg":"hello\\","stacktrace":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\\\nfake-stack\n", + expectedJSON: `{"level":"info","ts":0,"name":"main","caller":"foo.go:42","func":"foo.Foo","msg":"hello\\","stacktrace":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\\\nfake-stack\n", }, { desc: "use custom entry keys in JSON output and ignore them in console output", @@ -108,6 +103,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -115,17 +111,18 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "skip level if LevelKey is omitted", cfg: EncoderConfig{ - LevelKey: "", + LevelKey: OmitKey, TimeKey: "T", MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -133,17 +130,18 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "skip timestamp if TimeKey is omitted", cfg: EncoderConfig{ LevelKey: "L", - TimeKey: "", + TimeKey: OmitKey, MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -151,17 +149,18 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "info\tmain\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "info\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "skip message if MessageKey is omitted", cfg: EncoderConfig{ LevelKey: "L", TimeKey: "T", - MessageKey: "", + MessageKey: OmitKey, NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -169,8 +168,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\nfake-stack\n", }, { desc: "skip name if NameKey is omitted", @@ -178,8 +177,9 @@ func TestEncoderConfiguration(t *testing.T) { LevelKey: "L", TimeKey: "T", MessageKey: "M", - NameKey: "", + NameKey: OmitKey, CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -187,8 +187,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "skip caller if CallerKey is omitted", @@ -197,7 +197,8 @@ func TestEncoderConfiguration(t *testing.T) { TimeKey: "T", MessageKey: "M", NameKey: "N", - CallerKey: "", + CallerKey: OmitKey, + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -205,8 +206,27 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n", + }, + { + desc: "skip function if FunctionKey is omitted", + cfg: EncoderConfig{ + LevelKey: "L", + TimeKey: "T", + MessageKey: "M", + NameKey: "N", + CallerKey: "C", + FunctionKey: OmitKey, + StacktraceKey: "S", + LineEnding: base.LineEnding, + EncodeTime: base.EncodeTime, + EncodeDuration: base.EncodeDuration, + EncodeLevel: base.EncodeLevel, + EncodeCaller: base.EncodeCaller, + }, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack\n", }, { desc: "skip stacktrace if StacktraceKey is omitted", @@ -216,15 +236,16 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", - StacktraceKey: "", + FunctionKey: "F", + StacktraceKey: OmitKey, LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\n", }, { desc: "use the supplied EncodeTime, for both the entry and any times added", @@ -234,6 +255,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: func(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendString(t.String()) }, @@ -248,8 +270,8 @@ func TestEncoderConfiguration(t *testing.T) { return nil })) }, - expectedJSON: `{"L":"info","T":"1970-01-01 00:00:00 +0000 UTC","N":"main","C":"foo.go:42","M":"hello","extra":"1970-01-01 00:00:00 +0000 UTC","extras":["1970-01-01 00:00:00 +0000 UTC"],"S":"fake-stack"}` + "\n", - expectedConsole: "1970-01-01 00:00:00 +0000 UTC\tinfo\tmain\tfoo.go:42\thello\t" + // plain-text preamble + expectedJSON: `{"L":"info","T":"1970-01-01 00:00:00 +0000 UTC","N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","extra":"1970-01-01 00:00:00 +0000 UTC","extras":["1970-01-01 00:00:00 +0000 UTC"],"S":"fake-stack"}` + "\n", + expectedConsole: "1970-01-01 00:00:00 +0000 UTC\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + // plain-text preamble `{"extra": "1970-01-01 00:00:00 +0000 UTC", "extras": ["1970-01-01 00:00:00 +0000 UTC"]}` + // JSON context "\nfake-stack\n", // stacktrace after newline }, @@ -261,6 +283,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -275,8 +298,8 @@ func TestEncoderConfiguration(t *testing.T) { return nil })) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","extra":"1s","extras":["1m0s"],"S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + // preamble + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","extra":"1s","extras":["1m0s"],"S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + // preamble `{"extra": "1s", "extras": ["1m0s"]}` + // context "\nfake-stack\n", // stacktrace }, @@ -288,6 +311,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -295,8 +319,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: CapitalLevelEncoder, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tINFO\tmain\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tINFO\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "use the supplied EncodeName", @@ -306,6 +330,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -314,8 +339,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeCaller: base.EncodeCaller, EncodeName: capitalNameEncoder, }, - expectedJSON: `{"L":"info","T":0,"N":"MAIN","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tMAIN\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"MAIN","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tMAIN\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "close all open namespaces", @@ -325,6 +350,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -338,8 +364,8 @@ func TestEncoderConfiguration(t *testing.T) { enc.AddString("foo", "bar") enc.OpenNamespace("innermost") }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","outer":{"inner":{"foo":"bar","innermost":{}}},"S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","outer":{"inner":{"foo":"bar","innermost":{}}},"S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + `{"outer": {"inner": {"foo": "bar", "innermost": {}}}}` + "\nfake-stack\n", }, @@ -351,6 +377,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: func(time.Time, PrimitiveArrayEncoder) {}, @@ -359,8 +386,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeCaller: base.EncodeCaller, }, extra: func(enc Encoder) { enc.AddTime("sometime", time.Unix(0, 100)) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","sometime":100,"S":"fake-stack"}` + "\n", - expectedConsole: "info\tmain\tfoo.go:42\thello\t" + `{"sometime": 100}` + "\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","sometime":100,"S":"fake-stack"}` + "\n", + expectedConsole: "info\tmain\tfoo.go:42\tfoo.Foo\thello\t" + `{"sometime": 100}` + "\nfake-stack\n", }, { desc: "handle no-op EncodeDuration", @@ -370,6 +397,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -378,8 +406,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeCaller: base.EncodeCaller, }, extra: func(enc Encoder) { enc.AddDuration("someduration", time.Microsecond) }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","someduration":1000,"S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\t" + `{"someduration": 1000}` + "\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","someduration":1000,"S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + `{"someduration": 1000}` + "\nfake-stack\n", }, { desc: "handle no-op EncodeLevel", @@ -389,6 +417,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -396,8 +425,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: func(Level, PrimitiveArrayEncoder) {}, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tmain\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "handle no-op EncodeCaller", @@ -407,6 +436,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -414,8 +444,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: func(EntryCaller, PrimitiveArrayEncoder) {}, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tmain\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n", }, { desc: "handle no-op EncodeName", @@ -425,6 +455,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: base.LineEnding, EncodeTime: base.EncodeTime, @@ -433,8 +464,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeCaller: base.EncodeCaller, EncodeName: func(string, PrimitiveArrayEncoder) {}, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n", - expectedConsole: "0\tinfo\tfoo.go:42\thello\nfake-stack\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n", + expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n", }, { desc: "use custom line separator", @@ -444,6 +475,7 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", LineEnding: "\r\n", EncodeTime: base.EncodeTime, @@ -451,8 +483,8 @@ func TestEncoderConfiguration(t *testing.T) { EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\r\n", - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack\r\n", + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\r\n", + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\r\n", }, { desc: "omit line separator definition - fall back to default", @@ -462,14 +494,15 @@ func TestEncoderConfiguration(t *testing.T) { MessageKey: "M", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", EncodeTime: base.EncodeTime, EncodeDuration: base.EncodeDuration, EncodeLevel: base.EncodeLevel, EncodeCaller: base.EncodeCaller, }, - expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + DefaultLineEnding, - expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack" + DefaultLineEnding, + expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + DefaultLineEnding, + expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack" + DefaultLineEnding, }, } @@ -531,25 +564,65 @@ func TestLevelEncoders(t *testing.T) { func TestTimeEncoders(t *testing.T) { moment := time.Unix(100, 50005000).UTC() tests := []struct { - name string + yamlDoc string + expected interface{} // output of serializing moment + }{ + {"timeEncoder: iso8601", "1970-01-01T00:01:40.050Z"}, + {"timeEncoder: ISO8601", "1970-01-01T00:01:40.050Z"}, + {"timeEncoder: millis", 100050.005}, + {"timeEncoder: nanos", int64(100050005000)}, + {"timeEncoder: {layout: 06/01/02 03:04pm}", "70/01/01 12:01am"}, + {"timeEncoder: ''", 100.050005}, + {"timeEncoder: something-random", 100.050005}, + {"timeEncoder: rfc3339", "1970-01-01T00:01:40Z"}, + {"timeEncoder: RFC3339", "1970-01-01T00:01:40Z"}, + {"timeEncoder: rfc3339nano", "1970-01-01T00:01:40.050005Z"}, + {"timeEncoder: RFC3339Nano", "1970-01-01T00:01:40.050005Z"}, + } + + for _, tt := range tests { + cfg := EncoderConfig{} + require.NoError(t, yaml.Unmarshal([]byte(tt.yamlDoc), &cfg), "Unexpected error unmarshaling %q.", tt.yamlDoc) + require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.yamlDoc) + assertAppended( + t, + tt.expected, + func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) }, + "Unexpected output serializing %v with %q.", moment, tt.yamlDoc, + ) + } +} + +func TestTimeEncodersWrongYAML(t *testing.T) { + tests := []string{ + "timeEncoder: [1, 2, 3]", // wrong type + "timeEncoder: {foo:bar", // broken yaml + } + for _, tt := range tests { + cfg := EncoderConfig{} + assert.Error(t, yaml.Unmarshal([]byte(tt), &cfg), "Expected unmarshaling %q to become error, but not.", tt) + } +} + +func TestTimeEncodersParseFromJSON(t *testing.T) { + moment := time.Unix(100, 50005000).UTC() + tests := []struct { + jsonDoc string expected interface{} // output of serializing moment }{ - {"iso8601", "1970-01-01T00:01:40.050Z"}, - {"ISO8601", "1970-01-01T00:01:40.050Z"}, - {"millis", 100050.005}, - {"nanos", int64(100050005000)}, - {"", 100.050005}, - {"something-random", 100.050005}, + {`{"timeEncoder": "iso8601"}`, "1970-01-01T00:01:40.050Z"}, + {`{"timeEncoder": {"layout": "06/01/02 03:04pm"}}`, "70/01/01 12:01am"}, } for _, tt := range tests { - var te TimeEncoder - require.NoError(t, te.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name) + cfg := EncoderConfig{} + require.NoError(t, json.Unmarshal([]byte(tt.jsonDoc), &cfg), "Unexpected error unmarshaling %q.", tt.jsonDoc) + require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.jsonDoc) assertAppended( t, tt.expected, - func(arr ArrayEncoder) { te(moment, arr) }, - "Unexpected output serializing %v with %q.", moment, tt.name, + func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) }, + "Unexpected output serializing %v with %q.", moment, tt.jsonDoc, ) } } @@ -562,6 +635,7 @@ func TestDurationEncoders(t *testing.T) { }{ {"string", "1.0000005s"}, {"nanos", int64(1000000500)}, + {"ms", int64(1000)}, {"", 1.0000005}, {"something-random", 1.0000005}, } diff --git a/zapcore/entry.go b/zapcore/entry.go index 7d9893f33..2d815feb8 100644 --- a/zapcore/entry.go +++ b/zapcore/entry.go @@ -22,6 +22,7 @@ package zapcore import ( "fmt" + "runtime" "strings" "sync" "time" @@ -70,10 +71,11 @@ func NewEntryCaller(pc uintptr, file string, line int, ok bool) EntryCaller { // EntryCaller represents the caller of a logging function. type EntryCaller struct { - Defined bool - PC uintptr - File string - Line int + Defined bool + PC uintptr + File string + Line int + Function string } // String returns the full path and line number of the caller. @@ -136,7 +138,8 @@ func (ec EntryCaller) TrimmedPath() string { // An Entry represents a complete log message. The entry's structured context // is already serialized, but the log level, time, message, and call site -// information are available for inspection and modification. +// information are available for inspection and modification. Any fields left +// empty will be omitted when encoding. // // Entries are pooled, so any functions that accept them MUST be careful not to // retain references to them. @@ -157,6 +160,8 @@ const ( // WriteThenNoop indicates that nothing special needs to be done. It's the // default behavior. WriteThenNoop CheckWriteAction = iota + // WriteThenGoexit runs runtime.Goexit after Write. + WriteThenGoexit // WriteThenPanic causes a panic after Write. WriteThenPanic // WriteThenFatal causes a fatal os.Exit after Write. @@ -203,7 +208,7 @@ func (ce *CheckedEntry) Write(fields ...Field) { // If the entry is dirty, log an internal error; because the // CheckedEntry is being used after it was returned to the pool, // the message may be an amalgamation from multiple call sites. - fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.\n", time.Now(), ce.Entry) + fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.\n", ce.Time, ce.Entry) ce.ErrorOutput.Sync() } return @@ -216,7 +221,7 @@ func (ce *CheckedEntry) Write(fields ...Field) { } if ce.ErrorOutput != nil { if err != nil { - fmt.Fprintf(ce.ErrorOutput, "%v write error: %v\n", time.Now(), err) + fmt.Fprintf(ce.ErrorOutput, "%v write error: %v\n", ce.Time, err) ce.ErrorOutput.Sync() } } @@ -229,6 +234,8 @@ func (ce *CheckedEntry) Write(fields ...Field) { panic(msg) case WriteThenFatal: exit.Exit() + case WriteThenGoexit: + runtime.Goexit() } } diff --git a/zapcore/entry_test.go b/zapcore/entry_test.go index 569c4e1e0..4c2d67eae 100644 --- a/zapcore/entry_test.go +++ b/zapcore/entry_test.go @@ -29,6 +29,22 @@ import ( "github.com/stretchr/testify/assert" ) +func assertGoexit(t *testing.T, f func()) { + var finished bool + recovered := make(chan interface{}) + go func() { + defer func() { + recovered <- recover() + }() + + f() + finished = true + }() + + assert.Nil(t, <-recovered, "Goexit should cause recover to return nil") + assert.False(t, finished, "Goroutine should not finish after Goexit") +} + func TestPutNilEntry(t *testing.T) { // Pooling nil entries defeats the purpose. var wg sync.WaitGroup @@ -88,20 +104,29 @@ func TestEntryCaller(t *testing.T) { } func TestCheckedEntryWrite(t *testing.T) { - // Nil checked entries are safe. - var ce *CheckedEntry - assert.NotPanics(t, func() { ce.Write() }, "Unexpected panic writing nil CheckedEntry.") - - // WriteThenPanic - ce = ce.Should(Entry{}, WriteThenPanic) - assert.Panics(t, func() { ce.Write() }, "Expected to panic when WriteThenPanic is set.") - ce.reset() - - // WriteThenFatal - ce = ce.Should(Entry{}, WriteThenFatal) - stub := exit.WithStub(func() { - ce.Write() + t.Run("nil is safe", func(t *testing.T) { + var ce *CheckedEntry + assert.NotPanics(t, func() { ce.Write() }, "Unexpected panic writing nil CheckedEntry.") + }) + + t.Run("WriteThenPanic", func(t *testing.T) { + var ce *CheckedEntry + ce = ce.Should(Entry{}, WriteThenPanic) + assert.Panics(t, func() { ce.Write() }, "Expected to panic when WriteThenPanic is set.") + }) + + t.Run("WriteThenGoexit", func(t *testing.T) { + var ce *CheckedEntry + ce = ce.Should(Entry{}, WriteThenGoexit) + assertGoexit(t, func() { ce.Write() }) + }) + + t.Run("WriteThenFatal", func(t *testing.T) { + var ce *CheckedEntry + ce = ce.Should(Entry{}, WriteThenFatal) + stub := exit.WithStub(func() { + ce.Write() + }) + assert.True(t, stub.Exited, "Expected to exit when WriteThenFatal is set.") }) - assert.True(t, stub.Exited, "Expected to exit when WriteThenFatal is set.") - ce.reset() } diff --git a/zapcore/error.go b/zapcore/error.go index a67c7bacc..f2a07d786 100644 --- a/zapcore/error.go +++ b/zapcore/error.go @@ -22,6 +22,7 @@ package zapcore import ( "fmt" + "reflect" "sync" ) @@ -42,7 +43,23 @@ import ( // ... // ], // } -func encodeError(key string, err error, enc ObjectEncoder) error { +func encodeError(key string, err error, enc ObjectEncoder) (retErr error) { + // Try to capture panics (from nil references or otherwise) when calling + // the Error() method + defer func() { + if rerr := recover(); rerr != nil { + // If it's a nil pointer, just say "". The likeliest causes are a + // error that fails to guard against nil or a nil pointer for a + // value receiver, and in either case, "" is a nice result. + if v := reflect.ValueOf(err); v.Kind() == reflect.Ptr && v.IsNil() { + enc.AddString(key, "") + return + } + + retErr = fmt.Errorf("PANIC=%v", rerr) + } + }() + basic := err.Error() enc.AddString(key, basic) @@ -66,11 +83,6 @@ type errorGroup interface { Errors() []error } -type causer interface { - // Provides access to the error that caused this error. - Cause() error -} - // Note that errArry and errArrayElem are very similar to the version // implemented in the top-level error.go file. We can't re-use this because // that would require exporting errArray as part of the zapcore API. diff --git a/zapcore/field.go b/zapcore/field.go index 6a5e33e2f..95bdb0a12 100644 --- a/zapcore/field.go +++ b/zapcore/field.go @@ -65,8 +65,11 @@ const ( Int8Type // StringType indicates that the field carries a string. StringType - // TimeType indicates that the field carries a time.Time. + // TimeType indicates that the field carries a time.Time that is + // representable by a UnixNano() stored as an int64. TimeType + // TimeFullType indicates that the field carries a time.Time stored as-is. + TimeFullType // Uint64Type indicates that the field carries a uint64. Uint64Type // Uint32Type indicates that the field carries a uint32. @@ -89,6 +92,10 @@ const ( ErrorType // SkipType indicates that the field is a no-op. SkipType + + // InlineMarshalerType indicates that the field carries an ObjectMarshaler + // that should be inlined. + InlineMarshalerType ) // A Field is a marshaling operation used to add a key-value pair to a logger's @@ -112,6 +119,8 @@ func (f Field) AddTo(enc ObjectEncoder) { err = enc.AddArray(f.Key, f.Interface.(ArrayMarshaler)) case ObjectMarshalerType: err = enc.AddObject(f.Key, f.Interface.(ObjectMarshaler)) + case InlineMarshalerType: + err = f.Interface.(ObjectMarshaler).MarshalLogObject(enc) case BinaryType: enc.AddBinary(f.Key, f.Interface.([]byte)) case BoolType: @@ -145,6 +154,8 @@ func (f Field) AddTo(enc ObjectEncoder) { // Fall back to UTC if location is nil. enc.AddTime(f.Key, time.Unix(0, f.Integer)) } + case TimeFullType: + enc.AddTime(f.Key, f.Interface.(time.Time)) case Uint64Type: enc.AddUint64(f.Key, uint64(f.Integer)) case Uint32Type: @@ -160,9 +171,9 @@ func (f Field) AddTo(enc ObjectEncoder) { case NamespaceType: enc.OpenNamespace(f.Key) case StringerType: - enc.AddString(f.Key, f.Interface.(fmt.Stringer).String()) + err = encodeStringer(f.Key, f.Interface, enc) case ErrorType: - encodeError(f.Key, f.Interface.(error), enc) + err = encodeError(f.Key, f.Interface.(error), enc) case SkipType: break default: @@ -199,3 +210,24 @@ func addFields(enc ObjectEncoder, fields []Field) { fields[i].AddTo(enc) } } + +func encodeStringer(key string, stringer interface{}, enc ObjectEncoder) (retErr error) { + // Try to capture panics (from nil references or otherwise) when calling + // the String() method, similar to https://golang.org/src/fmt/print.go#L540 + defer func() { + if err := recover(); err != nil { + // If it's a nil pointer, just say "". The likeliest causes are a + // Stringer that fails to guard against nil or a nil pointer for a + // value receiver, and in either case, "" is a nice result. + if v := reflect.ValueOf(stringer); v.Kind() == reflect.Ptr && v.IsNil() { + enc.AddString(key, "") + return + } + + retErr = fmt.Errorf("PANIC=%v", err) + } + }() + + enc.AddString(key, stringer.(fmt.Stringer).String()) + return nil +} diff --git a/zapcore/field_test.go b/zapcore/field_test.go index 9a5fe0189..c4363297c 100644 --- a/zapcore/field_test.go +++ b/zapcore/field_test.go @@ -24,13 +24,13 @@ import ( "errors" "fmt" "math" + "net/url" "testing" "time" - "go.uber.org/zap" - "github.com/stretchr/testify/assert" - + "github.com/stretchr/testify/require" + "go.uber.org/zap" . "go.uber.org/zap/zapcore" ) @@ -58,6 +58,41 @@ func (u users) MarshalLogArray(enc ArrayEncoder) error { return nil } +type obj struct { + kind int +} + +func (o *obj) String() string { + if o == nil { + return "nil obj" + } + + if o.kind == 1 { + panic("panic with string") + } else if o.kind == 2 { + panic(errors.New("panic with error")) + } else if o.kind == 3 { + // panic with an arbitrary object that causes a panic itself + // when being converted to a string + panic((*url.URL)(nil)) + } + + return "obj" +} + +type errObj struct { + kind int + errMsg string +} + +func (eobj *errObj) Error() string { + if eobj.kind == 1 { + panic("panic in Error() method") + } else { + return eobj.errMsg + } +} + func TestUnknownFieldType(t *testing.T) { unknown := Field{Key: "k", String: "foo"} assert.Equal(t, UnknownType, unknown.Type, "Expected zero value of FieldType to be UnknownType.") @@ -67,19 +102,28 @@ func TestUnknownFieldType(t *testing.T) { } func TestFieldAddingError(t *testing.T) { + var empty interface{} tests := []struct { - t FieldType - want interface{} + t FieldType + iface interface{} + want interface{} + err string }{ - {ArrayMarshalerType, []interface{}{}}, - {ObjectMarshalerType, map[string]interface{}{}}, + {t: ArrayMarshalerType, iface: users(-1), want: []interface{}{}, err: "too few users"}, + {t: ObjectMarshalerType, iface: users(-1), want: map[string]interface{}{}, err: "too few users"}, + {t: InlineMarshalerType, iface: users(-1), want: nil, err: "too few users"}, + {t: StringerType, iface: obj{}, want: empty, err: "PANIC=interface conversion: zapcore_test.obj is not fmt.Stringer: missing method String"}, + {t: StringerType, iface: &obj{1}, want: empty, err: "PANIC=panic with string"}, + {t: StringerType, iface: &obj{2}, want: empty, err: "PANIC=panic with error"}, + {t: StringerType, iface: &obj{3}, want: empty, err: "PANIC="}, + {t: ErrorType, iface: &errObj{kind: 1}, want: empty, err: "PANIC=panic in Error() method"}, } for _, tt := range tests { - f := Field{Key: "k", Interface: users(-1), Type: tt.t} + f := Field{Key: "k", Interface: tt.iface, Type: tt.t} enc := NewMapObjectEncoder() assert.NotPanics(t, func() { f.AddTo(enc) }, "Unexpected panic when adding fields returns an error.") assert.Equal(t, tt.want, enc.Fields["k"], "On error, expected zero value in field.Key.") - assert.Equal(t, "too few users", enc.Fields["kError"], "Expected error message in log context.") + assert.Equal(t, tt.err, enc.Fields["kError"], "Expected error message in log context.") } } @@ -93,7 +137,6 @@ func TestFields(t *testing.T) { }{ {t: ArrayMarshalerType, iface: users(2), want: []interface{}{"user", "user"}}, {t: ObjectMarshalerType, iface: users(2), want: map[string]interface{}{"users": 2}}, - {t: BinaryType, iface: []byte("foo"), want: []byte("foo")}, {t: BoolType, i: 0, want: false}, {t: ByteStringType, iface: []byte("foo"), want: "foo"}, {t: Complex128Type, iface: 1 + 2i, want: 1 + 2i}, @@ -116,7 +159,12 @@ func TestFields(t *testing.T) { {t: ReflectType, iface: users(2), want: users(2)}, {t: NamespaceType, want: map[string]interface{}{}}, {t: StringerType, iface: users(2), want: "2 users"}, + {t: StringerType, iface: &obj{}, want: "obj"}, + {t: StringerType, iface: (*obj)(nil), want: "nil obj"}, {t: SkipType, want: interface{}(nil)}, + {t: StringerType, iface: (*url.URL)(nil), want: ""}, + {t: StringerType, iface: (*users)(nil), want: ""}, + {t: ErrorType, iface: (*errObj)(nil), want: ""}, } for _, tt := range tests { @@ -132,7 +180,36 @@ func TestFields(t *testing.T) { } } +func TestInlineMarshaler(t *testing.T) { + enc := NewMapObjectEncoder() + + topLevelStr := Field{Key: "k", Type: StringType, String: "s"} + topLevelStr.AddTo(enc) + + inlineObj := Field{Key: "ignored", Type: InlineMarshalerType, Interface: users(10)} + inlineObj.AddTo(enc) + + nestedObj := Field{Key: "nested", Type: ObjectMarshalerType, Interface: users(11)} + nestedObj.AddTo(enc) + + assert.Equal(t, map[string]interface{}{ + "k": "s", + "users": 10, + "nested": map[string]interface{}{ + "users": 11, + }, + }, enc.Fields) +} + func TestEquals(t *testing.T) { + // Values outside the UnixNano range were encoded incorrectly (#737, #803). + timeOutOfRangeHigh := time.Unix(0, math.MaxInt64).Add(time.Nanosecond) + timeOutOfRangeLow := time.Unix(0, math.MinInt64).Add(-time.Nanosecond) + timeOutOfRangeHighNano := time.Unix(0, timeOutOfRangeHigh.UnixNano()) + timeOutOfRangeLowNano := time.Unix(0, timeOutOfRangeLow.UnixNano()) + require.False(t, timeOutOfRangeHigh.Equal(timeOutOfRangeHighNano), "should be different as value is > UnixNano range") + require.False(t, timeOutOfRangeHigh.Equal(timeOutOfRangeHighNano), "should be different as value is < UnixNano range") + tests := []struct { a, b Field want bool @@ -167,6 +244,16 @@ func TestEquals(t *testing.T) { b: zap.Time("k", time.Unix(1000, 1000).In(time.FixedZone("TEST", -8))), want: false, }, + { + a: zap.Time("k", timeOutOfRangeLow), + b: zap.Time("k", timeOutOfRangeLowNano), + want: false, + }, + { + a: zap.Time("k", timeOutOfRangeHigh), + b: zap.Time("k", timeOutOfRangeHighNano), + want: false, + }, { a: zap.Time("k", time.Unix(1000, 1000)), b: zap.Time("k", time.Unix(1000, 2000)), diff --git a/zapcore/increase_level.go b/zapcore/increase_level.go new file mode 100644 index 000000000..5a1749261 --- /dev/null +++ b/zapcore/increase_level.go @@ -0,0 +1,66 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import "fmt" + +type levelFilterCore struct { + core Core + level LevelEnabler +} + +// NewIncreaseLevelCore creates a core that can be used to increase the level of +// an existing Core. It cannot be used to decrease the logging level, as it acts +// as a filter before calling the underlying core. If level decreases the log level, +// an error is returned. +func NewIncreaseLevelCore(core Core, level LevelEnabler) (Core, error) { + for l := _maxLevel; l >= _minLevel; l-- { + if !core.Enabled(l) && level.Enabled(l) { + return nil, fmt.Errorf("invalid increase level, as level %q is allowed by increased level, but not by existing core", l) + } + } + + return &levelFilterCore{core, level}, nil +} + +func (c *levelFilterCore) Enabled(lvl Level) bool { + return c.level.Enabled(lvl) +} + +func (c *levelFilterCore) With(fields []Field) Core { + return &levelFilterCore{c.core.With(fields), c.level} +} + +func (c *levelFilterCore) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { + if !c.Enabled(ent.Level) { + return ce + } + + return c.core.Check(ent, ce) +} + +func (c *levelFilterCore) Write(ent Entry, fields []Field) error { + return c.core.Write(ent, fields) +} + +func (c *levelFilterCore) Sync() error { + return c.core.Sync() +} diff --git a/zapcore/increase_level_test.go b/zapcore/increase_level_test.go new file mode 100644 index 000000000..acb8700f7 --- /dev/null +++ b/zapcore/increase_level_test.go @@ -0,0 +1,122 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + . "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func TestIncreaseLevel(t *testing.T) { + tests := []struct { + coreLevel Level + increaseLevel Level + wantErr bool + with []Field + }{ + { + coreLevel: InfoLevel, + increaseLevel: DebugLevel, + wantErr: true, + }, + { + coreLevel: InfoLevel, + increaseLevel: InfoLevel, + }, + { + coreLevel: InfoLevel, + increaseLevel: ErrorLevel, + }, + { + coreLevel: InfoLevel, + increaseLevel: ErrorLevel, + with: []Field{zap.String("k", "v")}, + }, + { + coreLevel: ErrorLevel, + increaseLevel: DebugLevel, + wantErr: true, + }, + { + coreLevel: ErrorLevel, + increaseLevel: InfoLevel, + wantErr: true, + }, + { + coreLevel: ErrorLevel, + increaseLevel: WarnLevel, + wantErr: true, + }, + { + coreLevel: ErrorLevel, + increaseLevel: PanicLevel, + }, + } + + for _, tt := range tests { + msg := fmt.Sprintf("increase %v to %v", tt.coreLevel, tt.increaseLevel) + t.Run(msg, func(t *testing.T) { + logger, logs := observer.New(tt.coreLevel) + + filteredLogger, err := NewIncreaseLevelCore(logger, tt.increaseLevel) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid increase level") + return + } + + if len(tt.with) > 0 { + filteredLogger = filteredLogger.With(tt.with) + } + + require.NoError(t, err) + + for l := DebugLevel; l <= FatalLevel; l++ { + enabled := filteredLogger.Enabled(l) + entry := Entry{Level: l} + ce := filteredLogger.Check(entry, nil) + ce.Write() + entries := logs.TakeAll() + + if l >= tt.increaseLevel { + assert.True(t, enabled, "expect %v to be enabled", l) + assert.NotNil(t, ce, "expect non-nil Check") + assert.NotEmpty(t, entries, "Expect log to be written") + } else { + assert.False(t, enabled, "expect %v to be disabled", l) + assert.Nil(t, ce, "expect nil Check") + assert.Empty(t, entries, "No logs should have been written") + } + + // Write should always log the entry as per the Core interface + require.NoError(t, filteredLogger.Write(entry, nil), "Write failed") + require.NoError(t, filteredLogger.Sync(), "Sync failed") + assert.NotEmpty(t, logs.TakeAll(), "Write should always log") + } + }) + } +} diff --git a/zapcore/json_encoder.go b/zapcore/json_encoder.go index 2dc67d81e..5cf7d917e 100644 --- a/zapcore/json_encoder.go +++ b/zapcore/json_encoder.go @@ -137,20 +137,37 @@ func (enc *jsonEncoder) resetReflectBuf() { if enc.reflectBuf == nil { enc.reflectBuf = bufferpool.Get() enc.reflectEnc = json.NewEncoder(enc.reflectBuf) + + // For consistency with our custom JSON encoder. + enc.reflectEnc.SetEscapeHTML(false) } else { enc.reflectBuf.Reset() } } -func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error { +var nullLiteralBytes = []byte("null") + +// Only invoke the standard JSON encoder if there is actually something to +// encode; otherwise write JSON null literal directly. +func (enc *jsonEncoder) encodeReflected(obj interface{}) ([]byte, error) { + if obj == nil { + return nullLiteralBytes, nil + } enc.resetReflectBuf() - err := enc.reflectEnc.Encode(obj) + if err := enc.reflectEnc.Encode(obj); err != nil { + return nil, err + } + enc.reflectBuf.TrimNewline() + return enc.reflectBuf.Bytes(), nil +} + +func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error { + valueBytes, err := enc.encodeReflected(obj) if err != nil { return err } - enc.reflectBuf.TrimNewline() enc.addKey(key) - _, err = enc.buf.Write(enc.reflectBuf.Bytes()) + _, err = enc.buf.Write(valueBytes) return err } @@ -219,7 +236,9 @@ func (enc *jsonEncoder) AppendComplex128(val complex128) { func (enc *jsonEncoder) AppendDuration(val time.Duration) { cur := enc.buf.Len() - enc.EncodeDuration(val, enc) + if e := enc.EncodeDuration; e != nil { + e(val, enc) + } if cur == enc.buf.Len() { // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep // JSON valid. @@ -233,14 +252,12 @@ func (enc *jsonEncoder) AppendInt64(val int64) { } func (enc *jsonEncoder) AppendReflected(val interface{}) error { - enc.resetReflectBuf() - err := enc.reflectEnc.Encode(val) + valueBytes, err := enc.encodeReflected(val) if err != nil { return err } - enc.reflectBuf.TrimNewline() enc.addElementSeparator() - _, err = enc.buf.Write(enc.reflectBuf.Bytes()) + _, err = enc.buf.Write(valueBytes) return err } @@ -251,9 +268,18 @@ func (enc *jsonEncoder) AppendString(val string) { enc.buf.AppendByte('"') } +func (enc *jsonEncoder) AppendTimeLayout(time time.Time, layout string) { + enc.addElementSeparator() + enc.buf.AppendByte('"') + enc.buf.AppendTime(time, layout) + enc.buf.AppendByte('"') +} + func (enc *jsonEncoder) AppendTime(val time.Time) { cur := enc.buf.Len() - enc.EncodeTime(val, enc) + if e := enc.EncodeTime; e != nil { + e(val, enc) + } if cur == enc.buf.Len() { // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep // output JSON valid. @@ -340,14 +366,20 @@ func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, final.AppendString(ent.LoggerName) } } - if ent.Caller.Defined && final.CallerKey != "" { - final.addKey(final.CallerKey) - cur := final.buf.Len() - final.EncodeCaller(ent.Caller, final) - if cur == final.buf.Len() { - // User-supplied EncodeCaller was a no-op. Fall back to strings to - // keep output JSON valid. - final.AppendString(ent.Caller.String()) + if ent.Caller.Defined { + if final.CallerKey != "" { + final.addKey(final.CallerKey) + cur := final.buf.Len() + final.EncodeCaller(ent.Caller, final) + if cur == final.buf.Len() { + // User-supplied EncodeCaller was a no-op. Fall back to strings to + // keep output JSON valid. + final.AppendString(ent.Caller.String()) + } + } + if final.FunctionKey != "" { + final.addKey(final.FunctionKey) + final.AppendString(ent.Caller.Function) } } if final.MessageKey != "" { diff --git a/zapcore/json_encoder_impl_test.go b/zapcore/json_encoder_impl_test.go index 563d5f6b6..3f11a8cca 100644 --- a/zapcore/json_encoder_impl_test.go +++ b/zapcore/json_encoder_impl_test.go @@ -36,6 +36,11 @@ import ( "go.uber.org/multierr" ) +var _defaultEncoderConfig = EncoderConfig{ + EncodeTime: EpochTimeEncoder, + EncodeDuration: SecondsDurationEncoder, +} + func TestJSONClone(t *testing.T) { // The parent encoder is created with plenty of excess capacity. parent := &jsonEncoder{buf: bufferpool.Get()} @@ -195,9 +200,9 @@ func TestJSONEncoderObjectFields(t *testing.T) { }, { desc: "reflect (success)", - expected: `"k":{"loggable":"yes"}`, + expected: `"k":{"escape":"<&>","loggable":"yes"}`, f: func(e Encoder) { - assert.NoError(t, e.AddReflected("k", map[string]string{"loggable": "yes"}), "Unexpected error JSON-serializing a map.") + assert.NoError(t, e.AddReflected("k", map[string]string{"escape": "<&>", "loggable": "yes"}), "Unexpected error JSON-serializing a map.") }, }, { @@ -224,7 +229,55 @@ func TestJSONEncoderObjectFields(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - assertOutput(t, tt.expected, tt.f) + assertOutput(t, _defaultEncoderConfig, tt.expected, tt.f) + }) + } +} + +func TestJSONEncoderTimeFormats(t *testing.T) { + date := time.Date(2000, time.January, 2, 3, 4, 5, 6, time.UTC) + + f := func(e Encoder) { + e.AddTime("k", date) + e.AddArray("a", ArrayMarshalerFunc(func(enc ArrayEncoder) error { + enc.AppendTime(date) + return nil + })) + } + tests := []struct { + desc string + cfg EncoderConfig + expected string + }{ + { + desc: "time.Time ISO8601", + cfg: EncoderConfig{ + EncodeDuration: NanosDurationEncoder, + EncodeTime: ISO8601TimeEncoder, + }, + expected: `"k":"2000-01-02T03:04:05.000Z","a":["2000-01-02T03:04:05.000Z"]`, + }, + { + desc: "time.Time RFC3339", + cfg: EncoderConfig{ + EncodeDuration: NanosDurationEncoder, + EncodeTime: RFC3339TimeEncoder, + }, + expected: `"k":"2000-01-02T03:04:05Z","a":["2000-01-02T03:04:05Z"]`, + }, + { + desc: "time.Time RFC3339Nano", + cfg: EncoderConfig{ + EncodeDuration: NanosDurationEncoder, + EncodeTime: RFC3339NanoTimeEncoder, + }, + expected: `"k":"2000-01-02T03:04:05.000000006Z","a":["2000-01-02T03:04:05.000000006Z"]`, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + assertOutput(t, tt.cfg, tt.expected, f) }) } } @@ -324,7 +377,7 @@ func TestJSONEncoderArrays(t *testing.T) { return nil })) } - assertOutput(t, `"array":`+tt.expected, func(enc Encoder) { + assertOutput(t, _defaultEncoderConfig, `"array":`+tt.expected, func(enc Encoder) { err := f(enc) assert.NoError(t, err, "Unexpected error adding array to JSON encoder.") }) @@ -332,15 +385,64 @@ func TestJSONEncoderArrays(t *testing.T) { } } +func TestJSONEncoderTimeArrays(t *testing.T) { + times := []time.Time{ + time.Unix(1008720000, 0).UTC(), // 2001-12-19 + time.Unix(1040169600, 0).UTC(), // 2002-12-18 + time.Unix(1071619200, 0).UTC(), // 2003-12-17 + } + + tests := []struct { + desc string + encoder TimeEncoder + want string + }{ + { + desc: "epoch", + encoder: EpochTimeEncoder, + want: `[1008720000,1040169600,1071619200]`, + }, + { + desc: "epoch millis", + encoder: EpochMillisTimeEncoder, + want: `[1008720000000,1040169600000,1071619200000]`, + }, + { + desc: "iso8601", + encoder: ISO8601TimeEncoder, + want: `["2001-12-19T00:00:00.000Z","2002-12-18T00:00:00.000Z","2003-12-17T00:00:00.000Z"]`, + }, + { + desc: "rfc3339", + encoder: RFC3339TimeEncoder, + want: `["2001-12-19T00:00:00Z","2002-12-18T00:00:00Z","2003-12-17T00:00:00Z"]`, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := _defaultEncoderConfig + cfg.EncodeTime = tt.encoder + + enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &cfg} + err := enc.AddArray("array", ArrayMarshalerFunc(func(arr ArrayEncoder) error { + for _, time := range times { + arr.AppendTime(time) + } + return nil + })) + assert.NoError(t, err) + assert.Equal(t, `"array":`+tt.want, enc.buf.String()) + }) + } +} + func assertJSON(t *testing.T, expected string, enc *jsonEncoder) { assert.Equal(t, expected, enc.buf.String(), "Encoded JSON didn't match expectations.") } -func assertOutput(t testing.TB, expected string, f func(Encoder)) { - enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &EncoderConfig{ - EncodeTime: EpochTimeEncoder, - EncodeDuration: SecondsDurationEncoder, - }} +func assertOutput(t testing.TB, cfg EncoderConfig, expected string, f func(Encoder)) { + enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &cfg} f(enc) assert.Equal(t, expected, enc.buf.String(), "Unexpected encoder output after adding.") diff --git a/zapcore/json_encoder_test.go b/zapcore/json_encoder_test.go index 31e0852df..4baa04549 100644 --- a/zapcore/json_encoder_test.go +++ b/zapcore/json_encoder_test.go @@ -64,6 +64,8 @@ func TestJSONEncodeEntry(t *testing.T) { "so": "passes", "answer": 42, "common_pie": 3.14, + "null_value": null, + "array_with_null_elements": [{}, null, null, 2], "such": { "aee": "lol", "bee": 123, @@ -84,6 +86,13 @@ func TestJSONEncodeEntry(t *testing.T) { zap.String("so", "passes"), zap.Int("answer", 42), zap.Float64("common_pie", 3.14), + // Cover special-cased handling of nil in AddReflect() and + // AppendReflect(). Note that for the latter, we explicitly test + // correct results for both the nil static interface{} value + // (`nil`), as well as the non-nil interface value with a + // dynamic type and nil value (`(*struct{})(nil)`). + zap.Reflect("null_value", nil), + zap.Reflect("array_with_null_elements", []interface{}{&struct{}{}, nil, (*struct{})(nil), 2}), zap.Reflect("such", foo{ A: "lol", B: 123, @@ -103,6 +112,7 @@ func TestJSONEncodeEntry(t *testing.T) { TimeKey: "T", NameKey: "N", CallerKey: "C", + FunctionKey: "F", StacktraceKey: "S", EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, @@ -120,3 +130,40 @@ func TestJSONEncodeEntry(t *testing.T) { }) } } + +func TestJSONEmptyConfig(t *testing.T) { + tests := []struct { + name string + field zapcore.Field + expected string + }{ + { + name: "time", + field: zap.Time("foo", time.Unix(1591287718, 0)), // 2020-06-04 09:21:58 -0700 PDT + expected: `{"foo": 1591287718000000000}`, + }, + { + name: "duration", + field: zap.Duration("bar", time.Microsecond), + expected: `{"bar": 1000}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{}) + + buf, err := enc.EncodeEntry(zapcore.Entry{ + Level: zapcore.DebugLevel, + Time: time.Now(), + LoggerName: "mylogger", + Message: "things happened", + }, []zapcore.Field{tt.field}) + if assert.NoError(t, err, "Unexpected JSON encoding error.") { + assert.JSONEq(t, tt.expected, buf.String(), "Incorrect encoded JSON entry.") + } + + buf.Free() + }) + } +} diff --git a/zapcore/leak_test.go b/zapcore/leak_test.go new file mode 100644 index 000000000..4ef412e37 --- /dev/null +++ b/zapcore/leak_test.go @@ -0,0 +1,31 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/zapcore/marshaler.go b/zapcore/marshaler.go index 2627a653d..c3c55ba0d 100644 --- a/zapcore/marshaler.go +++ b/zapcore/marshaler.go @@ -23,6 +23,10 @@ package zapcore // ObjectMarshaler allows user-defined types to efficiently add themselves to the // logging context, and to selectively omit information which shouldn't be // included in logs (e.g., passwords). +// +// Note: ObjectMarshaler is only used when zap.Object is used or when +// passed directly to zap.Any. It is not used when reflection-based +// encoding is used. type ObjectMarshaler interface { MarshalLogObject(ObjectEncoder) error } @@ -39,6 +43,10 @@ func (f ObjectMarshalerFunc) MarshalLogObject(enc ObjectEncoder) error { // ArrayMarshaler allows user-defined types to efficiently add themselves to the // logging context, and to selectively omit information which shouldn't be // included in logs (e.g., passwords). +// +// Note: ArrayMarshaler is only used when zap.Array is used or when +// passed directly to zap.Any. It is not used when reflection-based +// encoding is used. type ArrayMarshaler interface { MarshalLogArray(ArrayEncoder) error } diff --git a/zapcore/memory_encoder.go b/zapcore/memory_encoder.go index 6ef85b09c..dfead0829 100644 --- a/zapcore/memory_encoder.go +++ b/zapcore/memory_encoder.go @@ -158,7 +158,7 @@ func (s *sliceArrayEncoder) AppendReflected(v interface{}) error { } func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) } -func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, string(v)) } func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) } func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) } func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) } diff --git a/zapcore/memory_encoder_test.go b/zapcore/memory_encoder_test.go index 5ca9577ae..7a3f51acc 100644 --- a/zapcore/memory_encoder_test.go +++ b/zapcore/memory_encoder_test.go @@ -87,6 +87,11 @@ func TestMapObjectEncoderAdd(t *testing.T) { f: func(e ObjectEncoder) { e.AddBinary("k", []byte("foo")) }, expected: []byte("foo"), }, + { + desc: "AddByteString", + f: func(e ObjectEncoder) { e.AddByteString("k", []byte("foo")) }, + expected: "foo", + }, { desc: "AddBool", f: func(e ObjectEncoder) { e.AddBool("k", true) }, @@ -228,6 +233,7 @@ func TestSliceArrayEncoderAppend(t *testing.T) { // AppendObject and AppendArray are covered by the AddObject (nested) and // AddArray (nested) cases above. {"AppendBool", func(e ArrayEncoder) { e.AppendBool(true) }, true}, + {"AppendByteString", func(e ArrayEncoder) { e.AppendByteString([]byte("foo")) }, "foo"}, {"AppendComplex128", func(e ArrayEncoder) { e.AppendComplex128(1 + 2i) }, 1 + 2i}, {"AppendComplex64", func(e ArrayEncoder) { e.AppendComplex64(1 + 2i) }, complex64(1 + 2i)}, {"AppendDuration", func(e ArrayEncoder) { e.AppendDuration(time.Second) }, time.Second}, diff --git a/zapcore/sampler.go b/zapcore/sampler.go index e31641863..25f10ca1d 100644 --- a/zapcore/sampler.go +++ b/zapcore/sampler.go @@ -81,33 +81,104 @@ func (c *counter) IncCheckReset(t time.Time, tick time.Duration) uint64 { return 1 } -type sampler struct { - Core +// SamplingDecision is a decision represented as a bit field made by sampler. +// More decisions may be added in the future. +type SamplingDecision uint32 - counts *counters - tick time.Duration - first, thereafter uint64 +const ( + // LogDropped indicates that the Sampler dropped a log entry. + LogDropped SamplingDecision = 1 << iota + // LogSampled indicates that the Sampler sampled a log entry. + LogSampled +) + +// optionFunc wraps a func so it satisfies the SamplerOption interface. +type optionFunc func(*sampler) + +func (f optionFunc) apply(s *sampler) { + f(s) +} + +// SamplerOption configures a Sampler. +type SamplerOption interface { + apply(*sampler) } -// NewSampler creates a Core that samples incoming entries, which caps the CPU -// and I/O load of logging while attempting to preserve a representative subset -// of your logs. +// nopSamplingHook is the default hook used by sampler. +func nopSamplingHook(Entry, SamplingDecision) {} + +// SamplerHook registers a function which will be called when Sampler makes a +// decision. +// +// This hook may be used to get visibility into the performance of the sampler. +// For example, use it to track metrics of dropped versus sampled logs. +// +// var dropped atomic.Int64 +// zapcore.SamplerHook(func(ent zapcore.Entry, dec zapcore.SamplingDecision) { +// if dec&zapcore.LogDropped > 0 { +// dropped.Inc() +// } +// }) +func SamplerHook(hook func(entry Entry, dec SamplingDecision)) SamplerOption { + return optionFunc(func(s *sampler) { + s.hook = hook + }) +} + +// NewSamplerWithOptions creates a Core that samples incoming entries, which +// caps the CPU and I/O load of logging while attempting to preserve a +// representative subset of your logs. // // Zap samples by logging the first N entries with a given level and message // each tick. If more Entries with the same level and message are seen during // the same interval, every Mth message is logged and the rest are dropped. // +// Sampler can be configured to report sampling decisions with the SamplerHook +// option. +// // Keep in mind that zap's sampling implementation is optimized for speed over // absolute precision; under load, each tick may be slightly over- or // under-sampled. -func NewSampler(core Core, tick time.Duration, first, thereafter int) Core { - return &sampler{ +func NewSamplerWithOptions(core Core, tick time.Duration, first, thereafter int, opts ...SamplerOption) Core { + s := &sampler{ Core: core, tick: tick, counts: newCounters(), first: uint64(first), thereafter: uint64(thereafter), + hook: nopSamplingHook, } + for _, opt := range opts { + opt.apply(s) + } + + return s +} + +type sampler struct { + Core + + counts *counters + tick time.Duration + first, thereafter uint64 + hook func(Entry, SamplingDecision) +} + +// NewSampler creates a Core that samples incoming entries, which +// caps the CPU and I/O load of logging while attempting to preserve a +// representative subset of your logs. +// +// Zap samples by logging the first N entries with a given level and message +// each tick. If more Entries with the same level and message are seen during +// the same interval, every Mth message is logged and the rest are dropped. +// +// Keep in mind that zap's sampling implementation is optimized for speed over +// absolute precision; under load, each tick may be slightly over- or +// under-sampled. +// +// Deprecated: use NewSamplerWithOptions. +func NewSampler(core Core, tick time.Duration, first, thereafter int) Core { + return NewSamplerWithOptions(core, tick, first, thereafter) } func (s *sampler) With(fields []Field) Core { @@ -117,6 +188,7 @@ func (s *sampler) With(fields []Field) Core { counts: s.counts, first: s.first, thereafter: s.thereafter, + hook: s.hook, } } @@ -128,7 +200,9 @@ func (s *sampler) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { counter := s.counts.get(ent.Level, ent.Message) n := counter.IncCheckReset(ent.Time, s.tick) if n > s.first && (n-s.first)%s.thereafter != 0 { + s.hook(ent, LogDropped) return ce } + s.hook(ent, LogSampled) return s.Core.Check(ent, ce) } diff --git a/zapcore/sampler_bench_test.go b/zapcore/sampler_bench_test.go index af2e89782..a918be2e2 100644 --- a/zapcore/sampler_bench_test.go +++ b/zapcore/sampler_bench_test.go @@ -25,6 +25,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" "go.uber.org/zap/internal/ztest" . "go.uber.org/zap/zapcore" ) @@ -203,7 +205,7 @@ var counterTestCases = [][]string{ func BenchmarkSampler_Check(b *testing.B) { for _, keys := range counterTestCases { b.Run(fmt.Sprintf("%v keys", len(keys)), func(b *testing.B) { - fac := NewSampler( + fac := NewSamplerWithOptions( NewCore( NewJSONEncoder(testEncoderConfig()), &ztest.Discarder{}, @@ -228,3 +230,54 @@ func BenchmarkSampler_Check(b *testing.B) { }) } } + +func makeSamplerCountingHook() (func(_ Entry, dec SamplingDecision), *atomic.Int64, *atomic.Int64) { + droppedCount := new(atomic.Int64) + sampledCount := new(atomic.Int64) + h := func(_ Entry, dec SamplingDecision) { + if dec&LogDropped > 0 { + droppedCount.Inc() + } else if dec&LogSampled > 0 { + sampledCount.Inc() + } + } + return h, droppedCount, sampledCount +} + +func BenchmarkSampler_CheckWithHook(b *testing.B) { + hook, dropped, sampled := makeSamplerCountingHook() + for _, keys := range counterTestCases { + b.Run(fmt.Sprintf("%v keys", len(keys)), func(b *testing.B) { + fac := NewSamplerWithOptions( + NewCore( + NewJSONEncoder(testEncoderConfig()), + &ztest.Discarder{}, + DebugLevel, + ), + time.Millisecond, + 1, + 1000, + SamplerHook(hook), + ) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + ent := Entry{ + Level: DebugLevel + Level(i%4), + Message: keys[i], + } + _ = fac.Check(ent, nil) + i++ + if n := len(keys); i >= n { + i -= n + } + } + }) + }) + } + // We expect to see 1000 dropped messages for every sampled per settings, + // with a delta due to less 1000 messages getting dropped after initial one + // is sampled. + assert.Greater(b, dropped.Load()/1000, sampled.Load()-1000) +} diff --git a/zapcore/sampler_test.go b/zapcore/sampler_test.go index 9ba278b0b..71db0f9bd 100644 --- a/zapcore/sampler_test.go +++ b/zapcore/sampler_test.go @@ -37,6 +37,7 @@ import ( func fakeSampler(lvl LevelEnabler, tick time.Duration, first, thereafter int) (Core, *observer.ObservedLogs) { core, logs := observer.New(lvl) + // Keep using deprecated constructor for cc. core = NewSampler(core, tick, first, thereafter) return core, logs } @@ -162,7 +163,7 @@ func TestSamplerConcurrent(t *testing.T) { tick := ztest.Timeout(10 * time.Millisecond) cc := &countingCore{} - sampler := NewSampler(cc, tick, logsPerTick, 100000) + sampler := NewSamplerWithOptions(cc, tick, logsPerTick, 100000) var ( done atomic.Bool diff --git a/zapcore/write_syncer.go b/zapcore/write_syncer.go index 209e25fe2..d4a1af3d0 100644 --- a/zapcore/write_syncer.go +++ b/zapcore/write_syncer.go @@ -91,8 +91,7 @@ func NewMultiWriteSyncer(ws ...WriteSyncer) WriteSyncer { if len(ws) == 1 { return ws[0] } - // Copy to protect against https://github.com/golang/go/issues/7809 - return multiWriteSyncer(append([]WriteSyncer(nil), ws...)) + return multiWriteSyncer(ws) } // See https://golang.org/src/io/multi.go diff --git a/zapcore/write_syncer_bench_test.go b/zapcore/write_syncer_bench_test.go index 0209d0f61..0793805d5 100644 --- a/zapcore/write_syncer_bench_test.go +++ b/zapcore/write_syncer_bench_test.go @@ -21,13 +21,16 @@ package zapcore import ( + "io/ioutil" + "os" "testing" + "github.com/stretchr/testify/assert" "go.uber.org/zap/internal/ztest" ) func BenchmarkMultiWriteSyncer(b *testing.B) { - b.Run("2", func(b *testing.B) { + b.Run("2 discarder", func(b *testing.B) { w := NewMultiWriteSyncer( &ztest.Discarder{}, &ztest.Discarder{}, @@ -39,7 +42,7 @@ func BenchmarkMultiWriteSyncer(b *testing.B) { } }) }) - b.Run("4", func(b *testing.B) { + b.Run("4 discarder", func(b *testing.B) { w := NewMultiWriteSyncer( &ztest.Discarder{}, &ztest.Discarder{}, @@ -53,4 +56,38 @@ func BenchmarkMultiWriteSyncer(b *testing.B) { } }) }) + b.Run("4 discarder with buffer", func(b *testing.B) { + w := &BufferedWriteSyncer{ + WS: NewMultiWriteSyncer( + &ztest.Discarder{}, + &ztest.Discarder{}, + &ztest.Discarder{}, + &ztest.Discarder{}, + ), + } + defer w.Stop() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + w.Write([]byte("foobarbazbabble")) + } + }) + }) +} + +func BenchmarkWriteSyncer(b *testing.B) { + b.Run("write file with no buffer", func(b *testing.B) { + file, err := ioutil.TempFile("", "log") + assert.NoError(b, err) + defer file.Close() + defer os.Remove(file.Name()) + + w := AddSync(file) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + w.Write([]byte("foobarbazbabble")) + } + }) + }) } diff --git a/zapcore/write_syncer_test.go b/zapcore/write_syncer_test.go index 3ccb0af24..4748be7f5 100644 --- a/zapcore/write_syncer_test.go +++ b/zapcore/write_syncer_test.go @@ -23,9 +23,8 @@ package zapcore import ( "bytes" "errors" - "testing" - "io" + "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/zapgrpc/internal/test/README.md b/zapgrpc/internal/test/README.md new file mode 100644 index 000000000..b2f3f0f80 --- /dev/null +++ b/zapgrpc/internal/test/README.md @@ -0,0 +1,2 @@ +This submodule exists to test zapgrpc against grpc-go without adding a +dependency on grpc-go to Zap. diff --git a/zapgrpc/internal/test/go.mod b/zapgrpc/internal/test/go.mod new file mode 100644 index 000000000..c8efff4d9 --- /dev/null +++ b/zapgrpc/internal/test/go.mod @@ -0,0 +1,11 @@ +module go.uber.org/zap/zapgrpc/internal/test + +go 1.15 + +require ( + github.com/stretchr/testify v1.7.0 + go.uber.org/zap v1.16.0 + google.golang.org/grpc v1.35.0 +) + +replace go.uber.org/zap => ../../.. diff --git a/zapgrpc/internal/test/go.sum b/zapgrpc/internal/test/go.sum new file mode 100644 index 000000000..b4eae018c --- /dev/null +++ b/zapgrpc/internal/test/go.sum @@ -0,0 +1,111 @@ +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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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/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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.2/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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/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_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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/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.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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/zapgrpc/internal/test/grpc_test.go b/zapgrpc/internal/test/grpc_test.go new file mode 100644 index 000000000..f4befeb26 --- /dev/null +++ b/zapgrpc/internal/test/grpc_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zapgrpc" + "go.uber.org/zap/zaptest/observer" + "google.golang.org/grpc/grpclog" +) + +func TestLoggerV2(t *testing.T) { + core, observedLogs := observer.New(zapcore.InfoLevel) + zlog := zap.New(core) + + grpclog.SetLoggerV2(zapgrpc.NewLogger(zlog)) + + grpclog.Info("hello from grpc") + + logs := observedLogs.TakeAll() + require.Len(t, logs, 1, "Expected one log entry.") + entry := logs[0] + + assert.Equal(t, zapcore.InfoLevel, entry.Level, + "Log entry level did not match.") + assert.Equal(t, "hello from grpc", entry.Message, + "Log entry message did not match.") +} diff --git a/zapgrpc/zapgrpc.go b/zapgrpc/zapgrpc.go index 1181e6a0d..356e12741 100644 --- a/zapgrpc/zapgrpc.go +++ b/zapgrpc/zapgrpc.go @@ -21,7 +21,31 @@ // Package zapgrpc provides a logger that is compatible with grpclog. package zapgrpc // import "go.uber.org/zap/zapgrpc" -import "go.uber.org/zap" +import ( + "fmt" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// See https://github.com/grpc/grpc-go/blob/v1.35.0/grpclog/loggerv2.go#L77-L86 +const ( + grpcLvlInfo = 0 + grpcLvlWarn = 1 + grpcLvlError = 2 + grpcLvlFatal = 3 +) + +var ( + // _grpcToZapLevel maps gRPC log levels to zap log levels. + // See https://pkg.go.dev/go.uber.org/zap@v1.16.0/zapcore#Level + _grpcToZapLevel = map[int]zapcore.Level{ + grpcLvlInfo: zapcore.InfoLevel, + grpcLvlWarn: zapcore.WarnLevel, + grpcLvlError: zapcore.ErrorLevel, + grpcLvlFatal: zapcore.FatalLevel, + } +) // An Option overrides a Logger's default configuration. type Option interface { @@ -36,23 +60,49 @@ func (f optionFunc) apply(log *Logger) { // WithDebug configures a Logger to print at zap's DebugLevel instead of // InfoLevel. +// It only affects the Printf, Println and Print methods, which are only used in the gRPC v1 grpclog.Logger API. +// Deprecated: use grpclog.SetLoggerV2() for v2 API. func WithDebug() Option { return optionFunc(func(logger *Logger) { - logger.print = (*zap.SugaredLogger).Debug - logger.printf = (*zap.SugaredLogger).Debugf + logger.print = &printer{ + enab: logger.levelEnabler, + level: zapcore.DebugLevel, + print: logger.delegate.Debug, + printf: logger.delegate.Debugf, + } + }) +} + +// withWarn redirects the fatal level to the warn level, which makes testing +// easier. This is intentionally unexported. +func withWarn() Option { + return optionFunc(func(logger *Logger) { + logger.fatal = &printer{ + enab: logger.levelEnabler, + level: zapcore.WarnLevel, + print: logger.delegate.Warn, + printf: logger.delegate.Warnf, + } }) } // NewLogger returns a new Logger. -// -// By default, Loggers print at zap's InfoLevel. func NewLogger(l *zap.Logger, options ...Option) *Logger { logger := &Logger{ - log: l.Sugar(), - fatal: (*zap.SugaredLogger).Fatal, - fatalf: (*zap.SugaredLogger).Fatalf, - print: (*zap.SugaredLogger).Info, - printf: (*zap.SugaredLogger).Infof, + delegate: l.Sugar(), + levelEnabler: l.Core(), + } + logger.print = &printer{ + enab: logger.levelEnabler, + level: zapcore.InfoLevel, + print: logger.delegate.Info, + printf: logger.delegate.Infof, + } + logger.fatal = &printer{ + enab: logger.levelEnabler, + level: zapcore.FatalLevel, + print: logger.delegate.Fatal, + printf: logger.delegate.Fatalf, } for _, option := range options { option.apply(logger) @@ -60,41 +110,132 @@ func NewLogger(l *zap.Logger, options ...Option) *Logger { return logger } -// Logger adapts zap's Logger to be compatible with grpclog.Logger. -type Logger struct { - log *zap.SugaredLogger - fatal func(*zap.SugaredLogger, ...interface{}) - fatalf func(*zap.SugaredLogger, string, ...interface{}) - print func(*zap.SugaredLogger, ...interface{}) - printf func(*zap.SugaredLogger, string, ...interface{}) +// printer implements Print, Printf, and Println operations for a Zap level. +// +// We use it to customize Debug vs Info, and Warn vs Fatal for Print and Fatal +// respectively. +type printer struct { + enab zapcore.LevelEnabler + level zapcore.Level + print func(...interface{}) + printf func(string, ...interface{}) } -// Fatal implements grpclog.Logger. -func (l *Logger) Fatal(args ...interface{}) { - l.fatal(l.log, args...) +func (v *printer) Print(args ...interface{}) { + v.print(args...) } -// Fatalf implements grpclog.Logger. -func (l *Logger) Fatalf(format string, args ...interface{}) { - l.fatalf(l.log, format, args...) +func (v *printer) Printf(format string, args ...interface{}) { + v.printf(format, args...) } -// Fatalln implements grpclog.Logger. -func (l *Logger) Fatalln(args ...interface{}) { - l.fatal(l.log, args...) +func (v *printer) Println(args ...interface{}) { + if v.enab.Enabled(v.level) { + v.print(sprintln(args)) + } +} + +// Logger adapts zap's Logger to be compatible with grpclog.LoggerV2 and the deprecated grpclog.Logger. +type Logger struct { + delegate *zap.SugaredLogger + levelEnabler zapcore.LevelEnabler + print *printer + fatal *printer + // printToDebug bool + // fatalToWarn bool } // Print implements grpclog.Logger. +// Deprecated: use Info(). func (l *Logger) Print(args ...interface{}) { - l.print(l.log, args...) + l.print.Print(args...) } // Printf implements grpclog.Logger. +// Deprecated: use Infof(). func (l *Logger) Printf(format string, args ...interface{}) { - l.printf(l.log, format, args...) + l.print.Printf(format, args...) } // Println implements grpclog.Logger. +// Deprecated: use Info(). func (l *Logger) Println(args ...interface{}) { - l.print(l.log, args...) + l.print.Println(args...) +} + +// Info implements grpclog.LoggerV2. +func (l *Logger) Info(args ...interface{}) { + l.delegate.Info(args...) +} + +// Infoln implements grpclog.LoggerV2. +func (l *Logger) Infoln(args ...interface{}) { + if l.levelEnabler.Enabled(zapcore.InfoLevel) { + l.delegate.Info(sprintln(args)) + } +} + +// Infof implements grpclog.LoggerV2. +func (l *Logger) Infof(format string, args ...interface{}) { + l.delegate.Infof(format, args...) +} + +// Warning implements grpclog.LoggerV2. +func (l *Logger) Warning(args ...interface{}) { + l.delegate.Warn(args...) +} + +// Warningln implements grpclog.LoggerV2. +func (l *Logger) Warningln(args ...interface{}) { + if l.levelEnabler.Enabled(zapcore.WarnLevel) { + l.delegate.Warn(sprintln(args)) + } +} + +// Warningf implements grpclog.LoggerV2. +func (l *Logger) Warningf(format string, args ...interface{}) { + l.delegate.Warnf(format, args...) +} + +// Error implements grpclog.LoggerV2. +func (l *Logger) Error(args ...interface{}) { + l.delegate.Error(args...) +} + +// Errorln implements grpclog.LoggerV2. +func (l *Logger) Errorln(args ...interface{}) { + if l.levelEnabler.Enabled(zapcore.ErrorLevel) { + l.delegate.Error(sprintln(args)) + } +} + +// Errorf implements grpclog.LoggerV2. +func (l *Logger) Errorf(format string, args ...interface{}) { + l.delegate.Errorf(format, args...) +} + +// Fatal implements grpclog.LoggerV2. +func (l *Logger) Fatal(args ...interface{}) { + l.fatal.Print(args...) +} + +// Fatalln implements grpclog.LoggerV2. +func (l *Logger) Fatalln(args ...interface{}) { + l.fatal.Println(args...) +} + +// Fatalf implements grpclog.LoggerV2. +func (l *Logger) Fatalf(format string, args ...interface{}) { + l.fatal.Printf(format, args...) +} + +// V implements grpclog.LoggerV2. +func (l *Logger) V(level int) bool { + return l.levelEnabler.Enabled(_grpcToZapLevel[level]) +} + +func sprintln(args []interface{}) string { + s := fmt.Sprintln(args...) + // Drop the new line character added by Sprintln + return s[:len(s)-1] } diff --git a/zapgrpc/zapgrpc_test.go b/zapgrpc/zapgrpc_test.go index 036f3d764..a231d65ec 100644 --- a/zapgrpc/zapgrpc_test.go +++ b/zapgrpc/zapgrpc_test.go @@ -21,6 +21,7 @@ package zapgrpc import ( + "fmt" "testing" "go.uber.org/zap" @@ -33,44 +34,200 @@ import ( func TestLoggerInfoExpected(t *testing.T) { checkMessages(t, zapcore.DebugLevel, nil, zapcore.InfoLevel, []string{ "hello", - "world", + "s1s21 2 3s34s56", + "hello world", + "", "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", + "hello", + "s1s21 2 3s34s56", + "hello world", + "", + "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", }, func(logger *Logger) { + logger.Info("hello") + logger.Info("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Infof("%s world", "hello") + logger.Infoln() + logger.Infoln("foo") + logger.Infoln("foo", "bar") + logger.Infoln("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) logger.Print("hello") - logger.Printf("world") + logger.Print("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Printf("%s world", "hello") + logger.Println() logger.Println("foo") + logger.Println("foo", "bar") + logger.Println("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) }) } func TestLoggerDebugExpected(t *testing.T) { checkMessages(t, zapcore.DebugLevel, []Option{WithDebug()}, zapcore.DebugLevel, []string{ "hello", - "world", + "s1s21 2 3s34s56", + "hello world", + "", "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", }, func(logger *Logger) { logger.Print("hello") - logger.Printf("world") + logger.Print("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Printf("%s world", "hello") + logger.Println() logger.Println("foo") + logger.Println("foo", "bar") + logger.Println("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) }) } func TestLoggerDebugSuppressed(t *testing.T) { checkMessages(t, zapcore.InfoLevel, []Option{WithDebug()}, zapcore.DebugLevel, nil, func(logger *Logger) { logger.Print("hello") - logger.Printf("world") + logger.Printf("%s world", "hello") + logger.Println() logger.Println("foo") + logger.Println("foo", "bar") + }) +} + +func TestLoggerWarningExpected(t *testing.T) { + checkMessages(t, zapcore.DebugLevel, nil, zapcore.WarnLevel, []string{ + "hello", + "s1s21 2 3s34s56", + "hello world", + "", + "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", + }, func(logger *Logger) { + logger.Warning("hello") + logger.Warning("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Warningf("%s world", "hello") + logger.Warningln() + logger.Warningln("foo") + logger.Warningln("foo", "bar") + logger.Warningln("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + }) +} + +func TestLoggerErrorExpected(t *testing.T) { + checkMessages(t, zapcore.DebugLevel, nil, zapcore.ErrorLevel, []string{ + "hello", + "s1s21 2 3s34s56", + "hello world", + "", + "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", + }, func(logger *Logger) { + logger.Error("hello") + logger.Error("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Errorf("%s world", "hello") + logger.Errorln() + logger.Errorln("foo") + logger.Errorln("foo", "bar") + logger.Errorln("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) }) } func TestLoggerFatalExpected(t *testing.T) { checkMessages(t, zapcore.DebugLevel, nil, zapcore.FatalLevel, []string{ "hello", - "world", + "s1s21 2 3s34s56", + "hello world", + "", "foo", + "foo bar", + "s1 s2 1 2 3 s3 4 s5 6", }, func(logger *Logger) { logger.Fatal("hello") - logger.Fatalf("world") + logger.Fatal("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + logger.Fatalf("%s world", "hello") + logger.Fatalln() logger.Fatalln("foo") + logger.Fatalln("foo", "bar") + logger.Fatalln("s1", "s2", 1, 2, 3, "s3", 4, "s5", 6) + }) +} + +func TestLoggerV(t *testing.T) { + tests := []struct { + zapLevel zapcore.Level + grpcEnabled []int + grpcDisabled []int + }{ + { + zapLevel: zapcore.DebugLevel, + grpcEnabled: []int{grpcLvlInfo, grpcLvlWarn, grpcLvlError, grpcLvlFatal}, + grpcDisabled: []int{}, // everything is enabled, nothing is disabled + }, + { + zapLevel: zapcore.InfoLevel, + grpcEnabled: []int{grpcLvlInfo, grpcLvlWarn, grpcLvlError, grpcLvlFatal}, + grpcDisabled: []int{}, // everything is enabled, nothing is disabled + }, + { + zapLevel: zapcore.WarnLevel, + grpcEnabled: []int{grpcLvlWarn, grpcLvlError, grpcLvlFatal}, + grpcDisabled: []int{grpcLvlInfo}, + }, + { + zapLevel: zapcore.ErrorLevel, + grpcEnabled: []int{grpcLvlError, grpcLvlFatal}, + grpcDisabled: []int{grpcLvlInfo, grpcLvlWarn}, + }, + { + zapLevel: zapcore.DPanicLevel, + grpcEnabled: []int{grpcLvlFatal}, + grpcDisabled: []int{grpcLvlInfo, grpcLvlWarn, grpcLvlError}, + }, + { + zapLevel: zapcore.PanicLevel, + grpcEnabled: []int{grpcLvlFatal}, + grpcDisabled: []int{grpcLvlInfo, grpcLvlWarn, grpcLvlError}, + }, + { + zapLevel: zapcore.FatalLevel, + grpcEnabled: []int{grpcLvlFatal}, + grpcDisabled: []int{grpcLvlInfo, grpcLvlWarn, grpcLvlError}, + }, + } + for _, tst := range tests { + for _, grpcLvl := range tst.grpcEnabled { + t.Run(fmt.Sprintf("enabled %s %d", tst.zapLevel, grpcLvl), func(t *testing.T) { + checkLevel(t, tst.zapLevel, true, func(logger *Logger) bool { + return logger.V(grpcLvl) + }) + }) + } + for _, grpcLvl := range tst.grpcDisabled { + t.Run(fmt.Sprintf("disabled %s %d", tst.zapLevel, grpcLvl), func(t *testing.T) { + checkLevel(t, tst.zapLevel, false, func(logger *Logger) bool { + return logger.V(grpcLvl) + }) + }) + } + } +} + +func checkLevel( + t testing.TB, + enab zapcore.LevelEnabler, + expectedBool bool, + f func(*Logger) bool, +) { + withLogger(enab, nil, func(logger *Logger, observedLogs *observer.ObservedLogs) { + actualBool := f(logger) + if expectedBool { + require.True(t, actualBool) + } else { + require.False(t, actualBool) + } }) } @@ -104,12 +261,3 @@ func withLogger( core, observedLogs := observer.New(enab) f(NewLogger(zap.New(core), append(opts, withWarn())...), observedLogs) } - -// withWarn redirects the fatal level to the warn level, which makes testing -// easier. -func withWarn() Option { - return optionFunc(func(logger *Logger) { - logger.fatal = (*zap.SugaredLogger).Warn - logger.fatalf = (*zap.SugaredLogger).Warnf - }) -} diff --git a/zaptest/observer/observer.go b/zaptest/observer/observer.go index 78f5be45d..03866bd91 100644 --- a/zaptest/observer/observer.go +++ b/zaptest/observer/observer.go @@ -19,7 +19,7 @@ // THE SOFTWARE. // Package observer provides a zapcore.Core that keeps an in-memory, -// encoding-agnostic repesentation of log entries. It's useful for +// encoding-agnostic representation of log entries. It's useful for // applications that want to unit test their log output without tying their // tests to a particular output encoding. package observer // import "go.uber.org/zap/zaptest/observer" @@ -78,23 +78,30 @@ func (o *ObservedLogs) AllUntimed() []LoggedEntry { return ret } +// FilterLevelExact filters entries to those logged at exactly the given level. +func (o *ObservedLogs) FilterLevelExact(level zapcore.Level) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + return e.Level == level + }) +} + // FilterMessage filters entries to those that have the specified message. func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs { - return o.filter(func(e LoggedEntry) bool { + return o.Filter(func(e LoggedEntry) bool { return e.Message == msg }) } // FilterMessageSnippet filters entries to those that have a message containing the specified snippet. func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogs { - return o.filter(func(e LoggedEntry) bool { + return o.Filter(func(e LoggedEntry) bool { return strings.Contains(e.Message, snippet) }) } // FilterField filters entries to those that have the specified field. func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs { - return o.filter(func(e LoggedEntry) bool { + return o.Filter(func(e LoggedEntry) bool { for _, ctxField := range e.Context { if ctxField.Equals(field) { return true @@ -104,13 +111,27 @@ func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs { }) } -func (o *ObservedLogs) filter(match func(LoggedEntry) bool) *ObservedLogs { +// FilterFieldKey filters entries to those that have the specified key. +func (o *ObservedLogs) FilterFieldKey(key string) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + for _, ctxField := range e.Context { + if ctxField.Key == key { + return true + } + } + return false + }) +} + +// Filter returns a copy of this ObservedLogs containing only those entries +// for which the provided function returns true. +func (o *ObservedLogs) Filter(keep func(LoggedEntry) bool) *ObservedLogs { o.mu.RLock() defer o.mu.RUnlock() var filtered []LoggedEntry for _, entry := range o.logs { - if match(entry) { + if keep(entry) { filtered = append(filtered, entry) } } diff --git a/zaptest/observer/observer_test.go b/zaptest/observer/observer_test.go index e1a0da78c..9f179d02e 100644 --- a/zaptest/observer/observer_test.go +++ b/zaptest/observer/observer_test.go @@ -55,7 +55,7 @@ func TestObserver(t *testing.T) { assert.Equal(t, want, logs.AllUntimed(), "Unexpected contents from AllUntimed.") all := logs.All() - require.Equal(t, 1, len(all), "Unexpected numbed of LoggedEntries returned from All.") + require.Equal(t, 1, len(all), "Unexpected number of LoggedEntries returned from All.") assert.NotEqual(t, time.Time{}, all[0].Time, "Expected non-zero time on LoggedEntry.") // copy & zero time for stable assertions @@ -149,6 +149,22 @@ func TestFilters(t *testing.T) { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "any slice"}, Context: []zapcore.Field{zap.Any("slice", []string{"a"})}, }, + { + Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "msg 2"}, + Context: []zapcore.Field{zap.Int("b", 2), zap.Namespace("filterMe")}, + }, + { + Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "any slice"}, + Context: []zapcore.Field{zap.Any("filterMe", []string{"b"})}, + }, + { + Entry: zapcore.Entry{Level: zap.WarnLevel, Message: "danger will robinson"}, + Context: []zapcore.Field{zap.Int("b", 42)}, + }, + { + Entry: zapcore.Entry{Level: zap.ErrorLevel, Message: "warp core breach"}, + Context: []zapcore.Field{zap.Int("b", 42)}, + }, } logger, sink := New(zap.InfoLevel) @@ -206,6 +222,29 @@ func TestFilters(t *testing.T) { filtered: sink.FilterField(zap.Any("slice", []string{"a"})), want: logs[6:7], }, + { + msg: "filter field key", + filtered: sink.FilterFieldKey("filterMe"), + want: logs[7:9], + }, + { + msg: "filter by arbitrary function", + filtered: sink.Filter(func(e LoggedEntry) bool { + return len(e.Context) > 1 + }), + want: func() []LoggedEntry { + // Do not modify logs slice. + w := make([]LoggedEntry, 0, len(logs)) + w = append(w, logs[0:5]...) + w = append(w, logs[7]) + return w + }(), + }, + { + msg: "filter level", + filtered: sink.FilterLevelExact(zap.WarnLevel), + want: logs[9:10], + }, } for _, tt := range tests {