diff --git a/benchmarks/apex_test.go b/benchmarks/apex_test.go new file mode 100644 index 0000000..47d51fb --- /dev/null +++ b/benchmarks/apex_test.go @@ -0,0 +1,57 @@ +// 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 benchmarks + +import ( + "io" + + "github.com/apex/log" + "github.com/apex/log/handlers/json" +) + +func newDisabledApexLog() *log.Logger { + return &log.Logger{ + Handler: json.New(io.Discard), + Level: log.ErrorLevel, + } +} + +func newApexLog() *log.Logger { + return &log.Logger{ + Handler: json.New(io.Discard), + Level: log.DebugLevel, + } +} + +func fakeApexFields() log.Fields { + return log.Fields{ + "int": _tenInts[0], + "ints": _tenInts, + "string": _tenStrings[0], + "strings": _tenStrings, + "time": _tenTimes[0], + "times": _tenTimes, + "user1": _oneUser, + "user2": _oneUser, + "users": _tenUsers, + "error": errExample, + } +} diff --git a/benchmarks/doc.go b/benchmarks/doc.go new file mode 100644 index 0000000..b79f79f --- /dev/null +++ b/benchmarks/doc.go @@ -0,0 +1,23 @@ +// 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 benchmarks contains only benchmarks comparing zap to other +// structured logging libraries. +package benchmarks diff --git a/benchmarks/go.mod b/benchmarks/go.mod new file mode 100644 index 0000000..b10e47d --- /dev/null +++ b/benchmarks/go.mod @@ -0,0 +1,29 @@ +module github.com/bep/log/benchmarks + +go 1.18 + +replace github.com/bep/log => ../ + +require ( + github.com/apex/log v1.9.0 + github.com/bep/log v0.0.0-00010101000000-000000000000 + github.com/go-kit/log v0.2.0 + github.com/rs/zerolog v1.26.0 + github.com/sirupsen/logrus v1.8.1 + go.uber.org/multierr v1.7.0 + go.uber.org/zap v1.19.1 + gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 +) + +require ( + github.com/benbjohnson/clock v1.2.0 // indirect + github.com/bep/clocks v0.5.0 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/goleak v1.1.12 // indirect + golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect +) diff --git a/benchmarks/go.sum b/benchmarks/go.sum new file mode 100644 index 0000000..def872f --- /dev/null +++ b/benchmarks/go.sum @@ -0,0 +1,150 @@ +github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= +github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= +github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +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/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.2.0 h1:9Re3G2TWxkE06LdMWMpcY6KV81GLXMGiYpPYUPkFAws= +github.com/benbjohnson/clock v1.2.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= +github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.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/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= +github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +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/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= +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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +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/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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/sync v0.0.0-20210220032951-036812b2e83c/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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 h1:iiHuQZCNgYPmFQxd3BBN/Nc5+dAwzZuq5y40s20oQw0= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1/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-20200605160147-a5ece683394c/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/kit_test.go b/benchmarks/kit_test.go new file mode 100644 index 0000000..c586166 --- /dev/null +++ b/benchmarks/kit_test.go @@ -0,0 +1,31 @@ +// 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 benchmarks + +import ( + "io" + + "github.com/go-kit/log" +) + +func newKitLog(fields ...interface{}) log.Logger { + return log.With(log.NewJSONLogger(io.Discard), fields...) +} diff --git a/benchmarks/log15_test.go b/benchmarks/log15_test.go new file mode 100644 index 0000000..6f6d106 --- /dev/null +++ b/benchmarks/log15_test.go @@ -0,0 +1,33 @@ +// 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 benchmarks + +import ( + "io" + + "gopkg.in/inconshreveable/log15.v2" +) + +func newLog15() log15.Logger { + logger := log15.New() + logger.SetHandler(log15.StreamHandler(io.Discard, log15.JsonFormat())) + return logger +} diff --git a/benchmarks/logn_test.go b/benchmarks/logn_test.go new file mode 100644 index 0000000..c435412 --- /dev/null +++ b/benchmarks/logn_test.go @@ -0,0 +1,63 @@ +// 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 benchmarks + +import ( + "io" + + "github.com/bep/log" + "github.com/bep/log/handlers/json" +) + +func newDisabledLognLog() log.LevelLogger { + logger := log.NewLogger(log.LoggerConfig{ + Handler: json.New(io.Discard), + Level: log.ErrorLevel, + }) + return logger.WithLevel(log.InfoLevel) + +} + +func newLognLog() log.LevelLogger { + logger := log.NewLogger(log.LoggerConfig{ + Handler: json.New(io.Discard), + Level: log.DebugLevel, + }) + + return logger.WithLevel(log.DebugLevel) +} + +func fakeLognFields() log.FieldsFunc { + return func() log.Fields { + return log.Fields{ + {"int", _tenInts[0]}, + {"ints", _tenInts}, + {"string", _tenStrings[0]}, + {"strings", _tenStrings}, + {"time", _tenTimes[0]}, + {"times", _tenTimes}, + {"user1", _oneUser}, + {"user2", _oneUser}, + {"users", _tenUsers}, + {"error", errExample}, + } + } +} diff --git a/benchmarks/logrus_test.go b/benchmarks/logrus_test.go new file mode 100644 index 0000000..850743b --- /dev/null +++ b/benchmarks/logrus_test.go @@ -0,0 +1,57 @@ +// 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 benchmarks + +import ( + "io" + + "github.com/sirupsen/logrus" +) + +func newDisabledLogrus() *logrus.Logger { + logger := newLogrus() + logger.Level = logrus.ErrorLevel + return logger +} + +func newLogrus() *logrus.Logger { + return &logrus.Logger{ + Out: io.Discard, + Formatter: new(logrus.JSONFormatter), + Hooks: make(logrus.LevelHooks), + Level: logrus.DebugLevel, + } +} + +func fakeLogrusFields() logrus.Fields { + return logrus.Fields{ + "int": _tenInts[0], + "ints": _tenInts, + "string": _tenStrings[0], + "strings": _tenStrings, + "time": _tenTimes[0], + "times": _tenTimes, + "user1": _oneUser, + "user2": _oneUser, + "users": _tenUsers, + "error": errExample, + } +} diff --git a/benchmarks/scenario_bench_test.go b/benchmarks/scenario_bench_test.go new file mode 100644 index 0000000..b9d3fdc --- /dev/null +++ b/benchmarks/scenario_bench_test.go @@ -0,0 +1,398 @@ +// 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 benchmarks + +import ( + "io" + "log" + "testing" + + logn "github.com/bep/log" +) + +func BenchmarkDisabledWithoutFields(b *testing.B) { + b.Logf("Logging at a disabled level without any structured context.") + b.Run("apex/log", func(b *testing.B) { + logger := newDisabledApexLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newDisabledLognLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.Log(message) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newDisabledLogrus() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := newDisabledZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msg(getMessage(0)) + } + }) + }) +} + +func BenchmarkDisabledAccumulatedContext(b *testing.B) { + b.Logf("Logging at a disabled level with some accumulated context.") + b.Run("apex/log", func(b *testing.B) { + logger := newDisabledApexLog().WithFields(fakeApexFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newDisabledLognLog().WithFields(fakeLognFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.Log(message) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newDisabledLogrus().WithFields(fakeLogrusFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := fakeZerologContext(newDisabledZerolog().With()).Logger() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msg(getMessage(0)) + } + }) + }) +} + +func BenchmarkDisabledAddingFields(b *testing.B) { + b.Logf("Logging at a disabled level, adding context at each log site.") + b.Run("apex/log", func(b *testing.B) { + logger := newDisabledApexLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.WithFields(fakeApexFields()).Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newDisabledLognLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.WithFields(fakeLognFields()).Log(message) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newDisabledLogrus() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.WithFields(fakeLogrusFields()).Info(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := newDisabledZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + fakeZerologFields(logger.Info()).Msg(getMessage(0)) + } + }) + }) +} + +func BenchmarkWithoutFields(b *testing.B) { + b.Logf("Logging without any structured context.") + b.Run("apex/log", func(b *testing.B) { + logger := newApexLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newLognLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.Log(message) + } + }) + }) + b.Run("go-kit/kit/log", func(b *testing.B) { + logger := newKitLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Log(getMessage(0), getMessage(1)) + } + }) + }) + b.Run("inconshreveable/log15", func(b *testing.B) { + logger := newLog15() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newLogrus() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("stdlib.Println", func(b *testing.B) { + logger := log.New(io.Discard, "", log.LstdFlags) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Println(getMessage(0)) + } + }) + }) + b.Run("stdlib.Printf", func(b *testing.B) { + logger := log.New(io.Discard, "", log.LstdFlags) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Printf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := newZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msg(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog.Formatting", func(b *testing.B) { + logger := newZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msgf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) + } + }) + }) + b.Run("rs/zerolog.Check", func(b *testing.B) { + logger := newZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if e := logger.Info(); e.Enabled() { + e.Msg(getMessage(0)) + } + } + }) + }) +} + +func BenchmarkAccumulatedContext(b *testing.B) { + b.Logf("Logging with some accumulated context.") + b.Run("apex/log", func(b *testing.B) { + logger := newApexLog().WithFields(fakeApexFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newLognLog().WithFields(fakeLognFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.Log(message) + } + }) + }) + b.Run("go-kit/kit/log", func(b *testing.B) { + logger := newKitLog(fakeSugarFields()...) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Log(getMessage(0), getMessage(1)) + } + }) + }) + b.Run("inconshreveable/log15", func(b *testing.B) { + logger := newLog15().New(fakeSugarFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newLogrus().WithFields(fakeLogrusFields()) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := fakeZerologContext(newZerolog().With()).Logger() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msg(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog.Check", func(b *testing.B) { + logger := fakeZerologContext(newZerolog().With()).Logger() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if e := logger.Info(); e.Enabled() { + e.Msg(getMessage(0)) + } + } + }) + }) + b.Run("rs/zerolog.Formatting", func(b *testing.B) { + logger := fakeZerologContext(newZerolog().With()).Logger() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info().Msgf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) + } + }) + }) +} + +func BenchmarkAddingFields(b *testing.B) { + b.Logf("Logging with additional context at each log site.") + b.Run("apex/log", func(b *testing.B) { + logger := newApexLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.WithFields(fakeApexFields()).Info(getMessage(0)) + } + }) + }) + b.Run("bep/log", func(b *testing.B) { + logger := newLognLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + message := logn.NewStringFunc(func() string { return getMessage(0) }) + logger.WithFields(fakeLognFields()).Log(message) + } + }) + }) + b.Run("go-kit/kit/log", func(b *testing.B) { + logger := newKitLog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Log(fakeSugarFields()...) + } + }) + }) + b.Run("inconshreveable/log15", func(b *testing.B) { + logger := newLog15() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Info(getMessage(0), fakeSugarFields()...) + } + }) + }) + b.Run("sirupsen/logrus", func(b *testing.B) { + logger := newLogrus() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.WithFields(fakeLogrusFields()).Info(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog", func(b *testing.B) { + logger := newZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + fakeZerologFields(logger.Info()).Msg(getMessage(0)) + } + }) + }) + b.Run("rs/zerolog.Check", func(b *testing.B) { + logger := newZerolog() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if e := logger.Info(); e.Enabled() { + fakeZerologFields(e).Msg(getMessage(0)) + } + } + }) + }) +} diff --git a/benchmarks/zap_test.go b/benchmarks/zap_test.go new file mode 100644 index 0000000..9b27d28 --- /dev/null +++ b/benchmarks/zap_test.go @@ -0,0 +1,150 @@ +// 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 benchmarks + +import ( + "errors" + "fmt" + "time" + + "go.uber.org/multierr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + errExample = errors.New("fail") + + _messages = fakeMessages(1000) + _tenInts = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} + _tenStrings = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} + _tenTimes = []time.Time{ + time.Unix(0, 0), + time.Unix(1, 0), + time.Unix(2, 0), + time.Unix(3, 0), + time.Unix(4, 0), + time.Unix(5, 0), + time.Unix(6, 0), + time.Unix(7, 0), + time.Unix(8, 0), + time.Unix(9, 0), + } + _oneUser = &user{ + Name: "Jane Doe", + Email: "jane@test.com", + CreatedAt: time.Date(1980, 1, 1, 12, 0, 0, 0, time.UTC), + } + _tenUsers = users{ + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + _oneUser, + } +) + +func fakeMessages(n int) []string { + messages := make([]string, n) + for i := range messages { + messages[i] = fmt.Sprintf("Test logging, but use a somewhat realistic message length. (#%v)", i) + } + return messages +} + +func getMessage(iter int) string { + return _messages[iter%1000] +} + +type users []*user + +func (uu users) MarshalLogArray(arr zapcore.ArrayEncoder) error { + var err error + for i := range uu { + err = multierr.Append(err, arr.AppendObject(uu[i])) + } + return err +} + +type user struct { + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` +} + +func (u *user) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("name", u.Name) + enc.AddString("email", u.Email) + enc.AddInt64("createdAt", u.CreatedAt.UnixNano()) + return nil +} + +func fakeFields() []zap.Field { + return []zap.Field{ + zap.Int("int", _tenInts[0]), + zap.Ints("ints", _tenInts), + zap.String("string", _tenStrings[0]), + zap.Strings("strings", _tenStrings), + zap.Time("time", _tenTimes[0]), + zap.Times("times", _tenTimes), + zap.Object("user1", _oneUser), + zap.Object("user2", _oneUser), + zap.Array("users", _tenUsers), + zap.Error(errExample), + } +} + +func fakeSugarFields() []interface{} { + return []interface{}{ + "int", _tenInts[0], + "ints", _tenInts, + "string", _tenStrings[0], + "strings", _tenStrings, + "time", _tenTimes[0], + "times", _tenTimes, + "user1", _oneUser, + "user2", _oneUser, + "users", _tenUsers, + "error", errExample, + } +} + +func fakeFmtArgs() []interface{} { + // Need to keep this a function instead of a package-global var so that we + // pay the cast-to-interface{} penalty on each call. + return []interface{}{ + _tenInts[0], + _tenInts, + _tenStrings[0], + _tenStrings, + _tenTimes[0], + _tenTimes, + _oneUser, + _oneUser, + _tenUsers, + errExample, + } +} diff --git a/benchmarks/zerolog_test.go b/benchmarks/zerolog_test.go new file mode 100644 index 0000000..02547e1 --- /dev/null +++ b/benchmarks/zerolog_test.go @@ -0,0 +1,75 @@ +// 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 benchmarks + +import ( + "io" + + "github.com/rs/zerolog" +) + +func newZerolog() zerolog.Logger { + return zerolog.New(io.Discard).With().Timestamp().Logger() +} + +func newDisabledZerolog() zerolog.Logger { + return newZerolog().Level(zerolog.Disabled) +} + +func (u *user) MarshalZerologObject(e *zerolog.Event) { + e.Str("name", u.Name). + Str("email", u.Email). + Int64("createdAt", u.CreatedAt.UnixNano()) +} + +func (uu users) MarshalZerologArray(a *zerolog.Array) { + for _, u := range uu { + a.Object(u) + } +} + +func fakeZerologFields(e *zerolog.Event) *zerolog.Event { + return e. + Int("int", _tenInts[0]). + Ints("ints", _tenInts). + Str("string", _tenStrings[0]). + Strs("strings", _tenStrings). + Time("time", _tenTimes[0]). + Times("times", _tenTimes). + Object("user1", _oneUser). + Object("user2", _oneUser). + Array("users", _tenUsers). + Err(errExample) +} + +func fakeZerologContext(c zerolog.Context) zerolog.Context { + return c. + Int("int", _tenInts[0]). + Ints("ints", _tenInts). + Str("string", _tenStrings[0]). + Strs("strings", _tenStrings). + Time("time", _tenTimes[0]). + Times("times", _tenTimes). + Object("user1", _oneUser). + Object("user2", _oneUser). + Array("users", _tenUsers). + Err(errExample) +} diff --git a/entry.go b/entry.go index f142c37..d78810f 100644 --- a/entry.go +++ b/entry.go @@ -1,7 +1,6 @@ package log import ( - "encoding/json" "fmt" "strings" "time" @@ -9,75 +8,48 @@ import ( // assert interface compliance. var ( - _ LevelLogger = (*EntryFields)(nil) _ LevelLogger = (*Entry)(nil) ) -// EntryFields represents a single log entry at a given log level. -type EntryFields struct { - Logger *logger `json:"-"` - Fields Fields `json:"-"` - Level Level `json:"level"` -} - -// Entry holds a Entry with a Message and a Timestamp. -// This is what is actually logged. +// Entry represents a single log entry at a given log level. type Entry struct { - *EntryFields + logger *logger + + Level Level `json:"level"` Timestamp time.Time `json:"timestamp"` + Fields Fields `json:"fields,omitempty"` Message string `json:"message"` } -// FieldsDistinct returns a list of fields with duplicate names removed, -// keeping the last. -func (e *EntryFields) FieldsDistinct() Fields { - return e.distinctFieldsLastByName() -} - -func (e Entry) MarshalJSON() ([]byte, error) { - fields := make(map[string]any) - for _, f := range e.FieldsDistinct() { - fields[f.Name] = f.Value - } - - type EntryAlias Entry - return json.Marshal(&struct { - Fields map[string]any `json:"fields"` - EntryAlias - }{ - Fields: fields, - EntryAlias: (EntryAlias)(e), - }) -} - // NewEntry returns a new entry for `log`. -func NewEntry(log *logger) *EntryFields { - return &EntryFields{ - Logger: log, +func NewEntry(log *logger) *Entry { + return &Entry{ + logger: log, } } -func (e EntryFields) WithLevel(level Level) *EntryFields { +func (e Entry) WithLevel(level Level) *Entry { e.Level = level return &e } -func (e EntryFields) WithFields(fielder Fielder) *EntryFields { +func (e *Entry) WithFields(fielder Fielder) *Entry { if e.isLevelDisabled() { - return &e + return e } - e.Fields = append(e.Fields, fielder.Fields()...) - return &e + x := *e + x.Fields = append(x.Fields, fielder.Fields()...) + return &x } -func (e *EntryFields) WithField(key string, value any) *EntryFields { +func (e *Entry) WithField(key string, value any) *Entry { if e.isLevelDisabled() { return e } return e.WithFields(Fields{{key, value}}) } -func (e *EntryFields) WithDuration(d time.Duration) *EntryFields { +func (e *Entry) WithDuration(d time.Duration) *Entry { if e.isLevelDisabled() { return e } @@ -88,7 +60,7 @@ func (e *EntryFields) WithDuration(d time.Duration) *EntryFields { // // The given error may implement .Fielder, if it does the method // will add all its `.Fields()` into the returned entry. -func (e *EntryFields) WithError(err error) *EntryFields { +func (e *Entry) WithError(err error) *Entry { if err == nil || e.isLevelDisabled() { return e } @@ -117,23 +89,42 @@ func (e *EntryFields) WithError(err error) *EntryFields { return ctx } -func (e *EntryFields) isLevelDisabled() bool { - return e.Level < e.Logger.Level +func (e *Entry) isLevelDisabled() bool { + return e.Level < e.logger.Level } // Log a message at the given level. -func (e *EntryFields) Log(s fmt.Stringer) { - e.Logger.log(e, s) +func (e *Entry) Log(s fmt.Stringer) { + e.logger.log(e, s) } -// distinctFieldsLastByName returns the fields with duplicate names removed, -// keeping the rightmost field (last) with a given name. -func (e *EntryFields) distinctFieldsLastByName() Fields { - fields := make(Fields, 0, len(e.Fields)) +// Clone returns a new Entry with the same fields. +func (e *Entry) Clone() *Entry { + x := *e + x.Fields = make(Fields, len(e.Fields)) + copy(x.Fields, e.Fields) + return &x +} + +func (e *Entry) reset() { + e.logger = nil + e.Level = 0 + e.Fields = e.Fields[:0] + e.Message = "" + e.Timestamp = time.Time{} +} + +// finalize populates dst with Level and Fields merged from e and Message and Timestamp set. +func (e *Entry) finalize(dst *Entry, msg string) { + dst.Message = msg + dst.Timestamp = e.logger.Clock.Now() + dst.Level = e.Level + + // There mau be fields logged with the same name, keep the latest. for i := len(e.Fields) - 1; i >= 0; i-- { f := e.Fields[i] var seen bool - for _, f2 := range fields { + for _, f2 := range dst.Fields { if f.Name == f2.Name { seen = true break @@ -141,24 +132,9 @@ func (e *EntryFields) distinctFieldsLastByName() Fields { } if !seen { // Insert first. - fields = append(fields, Field{}) - copy(fields[1:], fields[:]) - fields[0] = f + dst.Fields = append(dst.Fields, Field{}) + copy(dst.Fields[1:], dst.Fields[:]) + dst.Fields[0] = f } } - - if len(fields) == 0 { - return nil - } - - return fields -} - -// finalize returns a copy of the Entry with Fields merged. -func (e *EntryFields) finalize(msg string) *Entry { - return &Entry{ - EntryFields: e, - Message: msg, - Timestamp: e.Logger.Clock.Now(), - } } diff --git a/entry_test.go b/entry_test.go index 6a9e290..773f9e6 100644 --- a/entry_test.go +++ b/entry_test.go @@ -1,52 +1,60 @@ -package log +package log_test import ( "fmt" "testing" "time" + "github.com/bep/log" + "github.com/bep/log/handlers" + "github.com/bep/log/handlers/memory" qt "github.com/frankban/quicktest" ) func TestEntry_WithFields(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + h := memory.New() + a := log.NewLogger(log.LoggerConfig{Handler: h, Level: log.InfoLevel}).WithLevel(log.InfoLevel) - b := a.WithFields(Fields{{"foo", "bar"}}) + b := a.WithFields(log.Fields{{"foo", "bar"}}) - c := a.WithFields(Fields{{"foo", "hello"}, {"bar", "world"}}) - d := c.WithFields(Fields{{"baz", "jazz"}}) - qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "bar"}}) - qt.Assert(t, c.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}}) - qt.Assert(t, d.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}, {"baz", "jazz"}}) + c := a.WithFields(log.Fields{{"foo", "hello"}, {"bar", "world"}}) + d := c.WithFields(log.Fields{{"baz", "jazz"}}) + qt.Assert(t, b.Fields, qt.DeepEquals, log.Fields{{"foo", "bar"}}) + qt.Assert(t, c.Fields, qt.DeepEquals, log.Fields{{"foo", "hello"}, {"bar", "world"}}) + qt.Assert(t, d.Fields, qt.DeepEquals, log.Fields{{"foo", "hello"}, {"bar", "world"}, {"baz", "jazz"}}) + + c.Log(log.String("upload")) + e := h.Entries[0] - e := c.finalize("upload") qt.Assert(t, "upload", qt.Equals, e.Message) - qt.Assert(t, Fields{{"foo", "hello"}, {"bar", "world"}}, qt.DeepEquals, e.Fields) - qt.Assert(t, InfoLevel, qt.Equals, e.Level) + qt.Assert(t, log.Fields{{"foo", "hello"}, {"bar", "world"}}, qt.DeepEquals, e.Fields) + qt.Assert(t, log.InfoLevel, qt.Equals, e.Level) qt.Assert(t, time.Now().IsZero(), qt.IsFalse) } func TestEntry_WithField(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + h := memory.New() + a := log.NewLogger(log.LoggerConfig{Handler: h, Level: log.InfoLevel}).WithLevel(log.InfoLevel) b := a.WithField("foo", "baz").WithField("foo", "bar") - qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil) - qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "bar"}}) + b.Log(log.String("upload")) + qt.Assert(t, a.Fields, qt.IsNil) + qt.Assert(t, h.Entries[0].Fields, qt.DeepEquals, log.Fields{{"foo", "bar"}}) } func TestEntry_WithError(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + a := log.NewLogger(log.LoggerConfig{Handler: handlers.Discard, Level: log.InfoLevel}).WithLevel(log.InfoLevel) b := a.WithError(fmt.Errorf("boom")) - qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil) - qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"error", "boom"}}) + qt.Assert(t, a.Fields, qt.IsNil) + qt.Assert(t, b.Fields, qt.DeepEquals, log.Fields{{"error", "boom"}}) } func TestEntry_WithError_fields(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + a := log.NewLogger(log.LoggerConfig{Handler: handlers.Discard, Level: log.InfoLevel}).WithLevel(log.InfoLevel) b := a.WithError(errFields("boom")) - qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil) + qt.Assert(t, a.Fields, qt.IsNil) qt.Assert(t, - b.distinctFieldsLastByName(), qt.DeepEquals, Fields{ + b.Fields, qt.DeepEquals, log.Fields{ {"error", "boom"}, {"reason", "timeout"}, }) @@ -54,16 +62,16 @@ func TestEntry_WithError_fields(t *testing.T) { } func TestEntry_WithError_nil(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + a := log.NewLogger(log.LoggerConfig{Handler: handlers.Discard, Level: log.InfoLevel}).WithLevel(log.InfoLevel) b := a.WithError(nil) - qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil) - qt.Assert(t, b.distinctFieldsLastByName(), qt.IsNil) + qt.Assert(t, a.Fields, qt.IsNil) + qt.Assert(t, b.Fields, qt.IsNil) } func TestEntry_WithDuration(t *testing.T) { - a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel) + a := log.NewLogger(log.LoggerConfig{Handler: handlers.Discard, Level: log.InfoLevel}).WithLevel(log.InfoLevel) b := a.WithDuration(time.Second * 2) - qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"duration", int64(2000)}}) + qt.Assert(t, b.Fields, qt.DeepEquals, log.Fields{{"duration", int64(2000)}}) } type errFields string @@ -72,6 +80,6 @@ func (ef errFields) Error() string { return string(ef) } -func (ef errFields) Fields() Fields { - return Fields{{"reason", "timeout"}} +func (ef errFields) Fields() log.Fields { + return log.Fields{{"reason", "timeout"}} } diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..0222823 --- /dev/null +++ b/handler.go @@ -0,0 +1,13 @@ +package log + +// Handler is used to handle log events, outputting them to +// stdio or sending them to remote services. See the "handlers" +// directory for implementations. +// +// It is left up to Handlers to implement thread-safety. +type Handler interface { + // HandleLog is invoked for each log event. + // Note that i e is going to be used after the call to HandleLog returns, + // it must be cloned with e.Clone(). + HandleLog(e *Entry) error +} diff --git a/handlers/cli/cli.go b/handlers/cli/cli.go index 7d78d4c..8ed2e86 100644 --- a/handlers/cli/cli.go +++ b/handlers/cli/cli.go @@ -23,7 +23,6 @@ var Colors = [...]*color.Color{ log.InfoLevel: color.New(color.FgBlue), log.WarnLevel: color.New(color.FgYellow), log.ErrorLevel: color.New(color.FgRed), - log.FatalLevel: color.New(color.FgRed), } // Strings mapping. @@ -32,7 +31,6 @@ var Strings = [...]string{ log.InfoLevel: "•", log.WarnLevel: "•", log.ErrorLevel: "⨯", - log.FatalLevel: "⨯", } // Handler implementation. @@ -67,7 +65,7 @@ func (h *Handler) HandleLog(e *log.Entry) error { color.Fprintf(h.Writer, "%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message) - for _, field := range e.FieldsDistinct() { + for _, field := range e.Fields { if field.Name == "source" { continue } diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..31d1e05 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,8 @@ +package handlers + +import "github.com/bep/log" + +// Discard is a no-op handler that discards all log messages. +var Discard = log.HandlerFunc(func(e *log.Entry) error { + return nil +}) diff --git a/handlers/json/json.go b/handlers/json/json.go index 59a31d7..47256b9 100644 --- a/handlers/json/json.go +++ b/handlers/json/json.go @@ -2,33 +2,29 @@ package json import ( - j "encoding/json" + "encoding/json" "io" - "os" - "sync" "github.com/bep/log" ) -// Default handler outputting to stderr. -var Default = New(os.Stderr) - -// Handler implementation. type Handler struct { - *j.Encoder - mu sync.Mutex + w io.Writer } -// New handler. +// New Handler implementation for JSON logging. +// Eeach log Entry is written as a single JSON object, no more than one write to w. +// The writer w should be safe for concurrent use by multiple +// goroutines if the returned Handler will be used concurrently. func New(w io.Writer) *Handler { return &Handler{ - Encoder: j.NewEncoder(w), + w, } } // HandleLog implements log.Handler. func (h *Handler) HandleLog(e *log.Entry) error { - h.mu.Lock() - defer h.mu.Unlock() - return h.Encoder.Encode(e) + enc := json.NewEncoder(h.w) + enc.SetEscapeHTML(false) + return enc.Encode(e) } diff --git a/handlers/json/json_test.go b/handlers/json/json_test.go index 231474a..1dd24e1 100644 --- a/handlers/json/json_test.go +++ b/handlers/json/json_test.go @@ -27,7 +27,7 @@ func TestJSONHandler(t *testing.T) { info.Log(log.String("world")) info.WithLevel(log.ErrorLevel).Log(log.String("boom")) - expected := "{\"fields\":{\"id\":\"123\",\"user\":\"tj\"},\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"hello\"}\n{\"fields\":{},\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"world\"}\n{\"fields\":{},\"level\":\"error\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"boom\"}\n" + expected := "{\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"fields\":[{\"name\":\"user\",\"value\":\"tj\"},{\"name\":\"id\",\"value\":\"123\"}],\"message\":\"hello\"}\n{\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"world\"}\n{\"level\":\"error\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"boom\"}\n" qt.Assert(t, buf.String(), qt.Equals, expected) } diff --git a/handlers/memory/memory.go b/handlers/memory/memory.go index 1442ccb..b6233b9 100644 --- a/handlers/memory/memory.go +++ b/handlers/memory/memory.go @@ -23,6 +23,6 @@ func New() *Handler { func (h *Handler) HandleLog(e *log.Entry) error { h.mu.Lock() defer h.mu.Unlock() - h.Entries = append(h.Entries, e) + h.Entries = append(h.Entries, e.Clone()) return nil } diff --git a/handlers/text/text.go b/handlers/text/text.go index d279d0f..821c3a5 100644 --- a/handlers/text/text.go +++ b/handlers/text/text.go @@ -33,7 +33,6 @@ var Colors = [...]int{ log.InfoLevel: blue, log.WarnLevel: yellow, log.ErrorLevel: red, - log.FatalLevel: red, } // Strings mapping. @@ -42,7 +41,6 @@ var Strings = [...]string{ log.InfoLevel: "INFO", log.WarnLevel: "WARN", log.ErrorLevel: "ERROR", - log.FatalLevel: "FATAL", } // Handler implementation. @@ -69,7 +67,7 @@ func (h *Handler) HandleLog(e *log.Entry) error { ts := time.Since(start) / time.Second fmt.Fprintf(h.Writer, "\033[%dm%6s\033[0m[%04d] %-25s", color, level, ts, e.Message) - for _, f := range e.FieldsDistinct() { + for _, f := range e.Fields { fmt.Fprintf(h.Writer, " \033[%dm%s\033[0m=%v", color, f.Name, f.Value) } diff --git a/interface.go b/interface.go index fdd1315..cfb92a0 100644 --- a/interface.go +++ b/interface.go @@ -8,7 +8,7 @@ import ( // Logger is the main interface for the logger. type Logger interface { // WithLevel returns a new entry with `level` set. - WithLevel(Level) *EntryFields + WithLevel(Level) *Entry } // LevelLogger @@ -18,22 +18,22 @@ type LevelLogger interface { Log(s fmt.Stringer) // WithLevel returns a new entry with `level` set. - WithLevel(Level) *EntryFields + WithLevel(Level) *Entry // WithFields returns a new entry with the`fields` in fields set. // This is a noop if LevelLogger's level is less than Logger's. - WithFields(fields Fielder) *EntryFields + WithFields(fields Fielder) *Entry // WithLevel returns a new entry with the field f set with value v // This is a noop if LevelLogger's level is less than Logger's. - WithField(f string, v any) *EntryFields + WithField(f string, v any) *Entry // WithDuration returns a new entry with the "duration" field set // to the given duration in milliseconds. // This is a noop if LevelLogger's level is less than Logger's. - WithDuration(time.Duration) *EntryFields + WithDuration(time.Duration) *Entry // WithError returns a new entry with the "error" set to `err`. // This is a noop if err is nil or LevelLogger's level is less than Logger's. - WithError(error) *EntryFields + WithError(error) *Entry } diff --git a/levels.go b/levels.go index 2f51184..0b0bfe5 100644 --- a/levels.go +++ b/levels.go @@ -19,7 +19,6 @@ const ( InfoLevel WarnLevel ErrorLevel - FatalLevel ) var levelNames = [...]string{ @@ -27,7 +26,6 @@ var levelNames = [...]string{ InfoLevel: "info", WarnLevel: "warn", ErrorLevel: "error", - FatalLevel: "fatal", } var levelStrings = map[string]Level{ @@ -36,7 +34,6 @@ var levelStrings = map[string]Level{ "warn": WarnLevel, "warning": WarnLevel, "error": ErrorLevel, - "fatal": FatalLevel, } // String implementation. diff --git a/levels_test.go b/levels_test.go index 2d7e0ed..da6e736 100644 --- a/levels_test.go +++ b/levels_test.go @@ -18,7 +18,6 @@ func TestParseLevel(t *testing.T) { {"warn", WarnLevel, 2}, {"warning", WarnLevel, 3}, {"error", ErrorLevel, 4}, - {"fatal", FatalLevel, 5}, } for _, c := range cases { @@ -39,12 +38,10 @@ func TestParseLevel(t *testing.T) { func TestLevel_MarshalJSON(t *testing.T) { e := Entry{ Message: "hello", - EntryFields: &EntryFields{ - Level: InfoLevel, - }, + Level: InfoLevel, } - expect := `{"fields":{},"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` + expect := `{"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` b, err := json.Marshal(e) qt.Assert(t, err, qt.IsNil) @@ -52,7 +49,7 @@ func TestLevel_MarshalJSON(t *testing.T) { } func TestLevel_UnmarshalJSON(t *testing.T) { - s := `{"fields":{},"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` + s := `{"fields":[],"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` e := new(Entry) err := json.Unmarshal([]byte(s), e) diff --git a/logger.go b/logger.go index 18ee85d..cdb49c3 100644 --- a/logger.go +++ b/logger.go @@ -3,7 +3,6 @@ package log import ( "fmt" stdlog "log" - "os" "time" "github.com/bep/clocks" @@ -39,6 +38,16 @@ type Fielder interface { Fields() Fields } +func NewFieldsFunc(fn func() Fields) FieldsFunc { + return FieldsFunc(fn) +} + +type FieldsFunc func() Fields + +func (f FieldsFunc) Fields() Fields { + return f() +} + // Field holds a named value. type Field struct { Name string `json:"name"` @@ -63,20 +72,6 @@ func (f HandlerFunc) HandleLog(e *Entry) error { return f(e) } -// Handler is used to handle log events, outputting them to -// stdio or sending them to remote services. See the "handlers" -// directory for implementations. -// -// It is left up to Handlers to implement thread-safety. -type Handler interface { - HandleLog(*Entry) error -} - -// NoopHandler is a no-op handler that discards all log messages. -var NoopHandler = HandlerFunc(func(e *Entry) error { - return nil -}) - // LoggerConfig is the configuration used to create a logger. type LoggerConfig struct { // Level is the minimum level to log at. @@ -97,7 +92,7 @@ func NewLogger(cfg LoggerConfig) Logger { panic("handler cannot be nil") } - if cfg.Level <= 0 || cfg.Level > FatalLevel { + if cfg.Level <= 0 || cfg.Level > ErrorLevel { panic("log level is out of range") } @@ -129,12 +124,12 @@ type Clock interface { } // WithLevel returns a new entry with `level` set. -func (l *logger) WithLevel(level Level) *EntryFields { +func (l *logger) WithLevel(level Level) *Entry { return NewEntry(l).WithLevel(level) } // WithFields returns a new entry with `fields` set. -func (l *logger) WithFields(fields Fielder) *EntryFields { +func (l *logger) WithFields(fields Fielder) *Entry { return NewEntry(l).WithFields(fields.Fields()) } @@ -142,34 +137,33 @@ func (l *logger) WithFields(fields Fielder) *EntryFields { // // Note that the `key` should not have spaces in it - use camel // case or underscores -func (l *logger) WithField(key string, value any) *EntryFields { +func (l *logger) WithField(key string, value any) *Entry { return NewEntry(l).WithField(key, value) } // WithDuration returns a new entry with the "duration" field set // to the given duration in milliseconds. -func (l *logger) WithDuration(d time.Duration) *EntryFields { +func (l *logger) WithDuration(d time.Duration) *Entry { return NewEntry(l).WithDuration(d) } // WithError returns a new entry with the "error" set to `err`. -func (l *logger) WithError(err error) *EntryFields { +func (l *logger) WithError(err error) *Entry { return NewEntry(l).WithError(err) } -// log the message, invoking the handler. We clone the entry here -// to bypass the overhead in Entry methods when the level is not -// met. -func (l *logger) log(e *EntryFields, s fmt.Stringer) { +// log the message, invoking the handler. +func (l *logger) log(e *Entry, s fmt.Stringer) { if e.Level < l.Level { return } - if err := l.Handler.HandleLog(e.finalize(s.String())); err != nil { + finalized := objectPools.GetEntry() + defer objectPools.PutEntry(finalized) + e.finalize(finalized, s.String()) + + if err := l.Handler.HandleLog(finalized); err != nil { stdlog.Printf("error logging: %s", err) } - if e.Level == FatalLevel { - os.Exit(1) - } } diff --git a/logger_test.go b/logger_test.go index 47faeaa..d38d6f7 100644 --- a/logger_test.go +++ b/logger_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/bep/log" + "github.com/bep/log/handlers" "github.com/bep/log/handlers/memory" qt "github.com/frankban/quicktest" ) @@ -84,7 +85,7 @@ func TestLogger_HandlerFunc(t *testing.T) { } func BenchmarkLogger_small(b *testing.B) { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) for i := 0; i < b.N; i++ { @@ -93,7 +94,7 @@ func BenchmarkLogger_small(b *testing.B) { } func BenchmarkLogger_medium(b *testing.B) { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) for i := 0; i < b.N; i++ { @@ -106,7 +107,7 @@ func BenchmarkLogger_medium(b *testing.B) { } func BenchmarkLogger_large(b *testing.B) { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) err := fmt.Errorf("boom") @@ -129,6 +130,30 @@ func BenchmarkLogger_large(b *testing.B) { } } +func BenchmarkLogger_common_context(b *testing.B) { + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) + info := l.WithLevel(log.InfoLevel) + for i := 0; i < 3; i++ { + info = info.WithField(fmt.Sprintf("context%d", i), "value") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + info.Log(log.String("upload")) + } +} + +func BenchmarkLogger_common_context_many_fields(b *testing.B) { + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) + info := l.WithLevel(log.InfoLevel) + for i := 0; i < 42; i++ { + info = info.WithField(fmt.Sprintf("context%d", i), "value") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + info.Log(log.String("upload")) + } +} + func BenchmarkLogger_levels(b *testing.B) { doWork := func(l log.LevelLogger) { for i := 0; i < 10; i++ { @@ -142,7 +167,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level not met", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: handlers.Discard}) error := l.WithLevel(log.InfoLevel) doWork(error) } @@ -150,7 +175,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level not met, one field", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) info = info.WithField("file", "sloth.png") doWork(info) @@ -159,7 +184,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level not met, many fields", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.ErrorLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) info = info.WithField("file", "sloth.png") for i := 0; i < 32; i++ { @@ -171,7 +196,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level met", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) for j := 0; j < 10; j++ { doWork(info) @@ -181,7 +206,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level met, one field", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) info = info.WithField("file", "sloth.png") doWork(info) @@ -190,7 +215,7 @@ func BenchmarkLogger_levels(b *testing.B) { b.Run("level met, many fields", func(b *testing.B) { for i := 0; i < b.N; i++ { - l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: log.NoopHandler}) + l := log.NewLogger(log.LoggerConfig{Level: log.InfoLevel, Handler: handlers.Discard}) info := l.WithLevel(log.InfoLevel) info = info.WithField("file", "sloth.png") for i := 0; i < 32; i++ { diff --git a/objectpools.go b/objectpools.go new file mode 100644 index 0000000..31c015b --- /dev/null +++ b/objectpools.go @@ -0,0 +1,25 @@ +package log + +import "sync" + +var objectPools = &objectPoolsHolder{ + entryPool: &sync.Pool{ + New: func() any { + return &Entry{} + }, + }, +} + +type objectPoolsHolder struct { + // This is only used for the event copy passed to HandleLog. + entryPool *sync.Pool +} + +func (h *objectPoolsHolder) GetEntry() *Entry { + return h.entryPool.Get().(*Entry) +} + +func (h *objectPoolsHolder) PutEntry(e *Entry) { + e.reset() + h.entryPool.Put(e) +}