From 8abca32b6c1cb8d7c55150e1152319ee8c056afd Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:23:25 -0800 Subject: [PATCH] chore: set log to main (#23698) --- log/CHANGELOG.md | 54 +++++++++++++++------ log/README.md | 2 + log/bench_test.go | 3 -- log/go.mod | 15 ++++-- log/go.sum | 81 ++++++++++++++++--------------- log/level.go | 16 +++---- log/level_test.go | 90 +++++++++++++++++++++++++---------- log/logger.go | 86 +++++++++++++++++++++++++++++---- log/logger_test.go | 61 ++++++++++++++++++++++++ log/options.go | 18 +++++++ log/slog/logger.go | 50 ++++++++++++++++++++ log/slog/logger_test.go | 92 ++++++++++++++++++++++++++++++++++++ log/sonar-project.properties | 16 +++++++ log/testing.go | 27 +++++++---- log/writer.go | 5 +- log/writer_test.go | 14 ++++-- 16 files changed, 509 insertions(+), 121 deletions(-) create mode 100644 log/logger_test.go create mode 100644 log/slog/logger.go create mode 100644 log/slog/logger_test.go create mode 100644 log/sonar-project.properties diff --git a/log/CHANGELOG.md b/log/CHANGELOG.md index 08768555e7..533ee90be5 100644 --- a/log/CHANGELOG.md +++ b/log/CHANGELOG.md @@ -11,29 +11,55 @@ Mention whether you follow Semantic Versioning. Usage: -Change log entries are to be added to the Unreleased section under the -appropriate stanza (see below). Each entry should ideally include a tag and -the Github issue reference in the following format: +Change log entries are to be added to the Unreleased section from newest to oldest. +Each entry must include the Github issue reference in the following format: -* () [#] Changelog message. +* [#] Changelog message. -Types of changes (Stanzas): - -"Features" for new features. -"Improvements" for changes in existing functionality. -"Deprecated" for soon-to-be removed features. -"Bug Fixes" for any bug fixes. -"API Breaking" for breaking exported APIs used by developers building on SDK. -Ref: https://keepachangelog.com/en/1.0.0/ --> # Changelog ## [Unreleased] -### Features +## [v1.5.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.4.1) - 2024-11-07 -* [#15956](https://github.com/cosmos/cosmos-sdk/pull/15956) Introduce extra options to configure logger. +* [#22466](https://github.com/cosmos/cosmos-sdk/pull/22466) Disable coloring in testing logger. +* [#22233](https://github.com/cosmos/cosmos-sdk/pull/22233) Use sonic json library for faster json handling. +* [#22347](https://github.com/cosmos/cosmos-sdk/pull/22347) Add `cosmossdk.io/log/slog` to allow using a standard library log/slog-backed logger. This required to bump the minimum go version of `cosmossdk.io/log` to 1.21. + +## [v1.4.1](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.4.1) - 2024-08-16 + +* [#21326](https://github.com/cosmos/cosmos-sdk/pull/21326) Avoid context key collision. + +## [v1.4.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.4.0) - 2024-08-07 + +* [#21045](https://github.com/cosmos/cosmos-sdk/pull/21045) Add `WithContext` method implementations to make all returned loggers compatible with `cosmossdk.io/core/log.Logger` (v1) without a direct dependency. + +## [v1.3.1](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.3.1) - 2024-02-05 + +* [#19346](https://github.com/cosmos/cosmos-sdk/pull/19346) Upgrade zerolog to v1.32.0. +* [#19346](https://github.com/cosmos/cosmos-sdk/pull/19346) `#15956` now works thanks to the upgrade of `zerolog`. + +## [v1.3.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.3.0) - 2024-01-10 + +* [#18916](https://github.com/cosmos/cosmos-sdk/pull/18916) Introduce an option for setting hooks. +* [#18429](https://github.com/cosmos/cosmos-sdk/pull/18429) Support customization of log json marshal. +* [#18898](https://github.com/cosmos/cosmos-sdk/pull/18898) Add `WARN` level. + +## [v1.2.1](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.2.1) - 2023-08-25 + +* [#17532](https://github.com/cosmos/cosmos-sdk/pull/17532) Proper marshalling of `fmt.Stringer` (follow-up of [#17205](https://github.com/cosmos/cosmos-sdk/pull/17205)). + +## [v1.2.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.2.0) - 2023-07-31 + +* [#17194](https://github.com/cosmos/cosmos-sdk/pull/17194) Avoid repeating parse log level in `ParseLogLevel`. +* [#17205](https://github.com/cosmos/cosmos-sdk/pull/17205) Fix types that do not implement the `json.Marshaler` interface. +* [#15956](https://github.com/cosmos/cosmos-sdk/pull/15956) Introduce an option for enabling error stack trace. + +## [v1.1.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.1.0) - 2023-04-27 + +* [#15956](https://github.com/cosmos/cosmos-sdk/pull/15956) Introduce options to configure logger (enable/disable colored output, customize log timestamps). ## [v1.0.0](https://github.com/cosmos/cosmos-sdk/releases/tag/log/v1.0.0) - 2023-03-30 diff --git a/log/README.md b/log/README.md index c06f26e933..2dd94366a2 100644 --- a/log/README.md +++ b/log/README.md @@ -1,3 +1,5 @@ # Log The `cosmossdk.io/log` provides a zerolog logging implementation for the Cosmos SDK and Cosmos SDK modules. + +To use a logger wrapping an instance of the standard library's `log/slog` package, use `cosmossdk.io/log/slog`. diff --git a/log/bench_test.go b/log/bench_test.go index 0005e9c121..fdf5fca645 100644 --- a/log/bench_test.go +++ b/log/bench_test.go @@ -83,7 +83,6 @@ func BenchmarkLoggers(b *testing.B) { // so that real write time is negligible. b.Run("zerolog", func(b *testing.B) { for _, bc := range benchCases { - bc := bc b.Run(bc.name, func(b *testing.B) { zl := zerolog.New(io.Discard) logger := log.NewCustomLogger(zl) @@ -99,7 +98,6 @@ func BenchmarkLoggers(b *testing.B) { // also useful as a reference for how expensive zerolog is. b.Run("specialized nop logger", func(b *testing.B) { for _, bc := range nopCases { - bc := bc b.Run(bc.name, func(b *testing.B) { logger := log.NewNopLogger() @@ -115,7 +113,6 @@ func BenchmarkLoggers(b *testing.B) { // so we offer the specialized version in the exported API. b.Run("zerolog nop logger", func(b *testing.B) { for _, bc := range nopCases { - bc := bc b.Run(bc.name, func(b *testing.B) { logger := log.NewCustomLogger(zerolog.Nop()) diff --git a/log/go.mod b/log/go.mod index f34f4e6ad1..49adbfadaa 100644 --- a/log/go.mod +++ b/log/go.mod @@ -3,13 +3,18 @@ module cosmossdk.io/log go 1.23 require ( - github.com/rs/zerolog v1.29.1 - gotest.tools/v3 v3.4.0 + github.com/bytedance/sonic v1.12.8 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.33.0 ) require ( - github.com/google/go-cmp v0.5.5 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - golang.org/x/sys v0.8.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.13.0 // indirect + golang.org/x/sys v0.29.0 // indirect ) diff --git a/log/go.sum b/log/go.sum index 9eaba02a5e..a326c428bc 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,47 +1,52 @@ +github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= +github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coreos/go-systemd/v22 v22.5.0/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= -github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/log/level.go b/log/level.go index 9a4f094f2c..d8523f136a 100644 --- a/log/level.go +++ b/log/level.go @@ -35,7 +35,7 @@ func ParseLogLevel(levelStr string) (FilterFunc, error) { } // parse and validate the levels - filterMap := make(map[string]string) + filterMap := make(map[string]zerolog.Level) list := strings.Split(l, ",") for _, item := range list { moduleAndLevel := strings.Split(item, ":") @@ -50,18 +50,19 @@ func ParseLogLevel(levelStr string) (FilterFunc, error) { return nil, fmt.Errorf("duplicate module %s in log level list %s", module, list) } - if _, err := zerolog.ParseLevel(level); err != nil { + zllevel, err := zerolog.ParseLevel(level) + if err != nil { return nil, fmt.Errorf("invalid log level %s in log level list %s", level, list) } - filterMap[module] = level + filterMap[module] = zllevel } filterFunc := func(key, lvl string) bool { - level, ok := filterMap[key] + zllevel, ok := filterMap[key] if !ok { // no level filter for this key // check if there is a default level filter - level, ok = filterMap[defaultLogLevelKey] + zllevel, ok = filterMap[defaultLogLevelKey] if !ok { return false } @@ -72,11 +73,6 @@ func ParseLogLevel(levelStr string) (FilterFunc, error) { panic(err) } - zllevel, err := zerolog.ParseLevel(level) - if err != nil { - panic(err) - } - return zllvl < zllevel } diff --git a/log/level_test.go b/log/level_test.go index e893ca4242..9b7e2a5ac1 100644 --- a/log/level_test.go +++ b/log/level_test.go @@ -3,46 +3,88 @@ package log_test import ( "testing" - "gotest.tools/v3/assert" - "cosmossdk.io/log" ) func TestParseLogLevel(t *testing.T) { _, err := log.ParseLogLevel("") - assert.Error(t, err, "empty log level") + if err == nil { + t.Errorf("expected error for empty log level, got nil") + } level := "consensus:foo,mempool:debug,*:error" _, err = log.ParseLogLevel(level) - assert.Error(t, err, "invalid log level foo in log level list [consensus:foo mempool:debug *:error]") + if err == nil { + t.Errorf("expected error for invalid log level foo in log level list [consensus:foo mempool:debug *:error], got nil") + } level = "consensus:debug,mempool:debug,*:error" filter, err := log.ParseLogLevel(level) - assert.NilError(t, err) - assert.Assert(t, filter != nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if filter == nil { + t.Fatalf("expected non-nil filter, got nil") + } - assert.Assert(t, !filter("consensus", "debug")) - assert.Assert(t, !filter("consensus", "info")) - assert.Assert(t, !filter("consensus", "error")) - assert.Assert(t, !filter("mempool", "debug")) - assert.Assert(t, !filter("mempool", "info")) - assert.Assert(t, !filter("mempool", "error")) - assert.Assert(t, !filter("state", "error")) - assert.Assert(t, !filter("server", "panic")) + if filter("consensus", "debug") { + t.Errorf("expected filter to return false for consensus:debug") + } + if filter("consensus", "info") { + t.Errorf("expected filter to return false for consensus:info") + } + if filter("consensus", "error") { + t.Errorf("expected filter to return false for consensus:error") + } + if filter("mempool", "debug") { + t.Errorf("expected filter to return false for mempool:debug") + } + if filter("mempool", "info") { + t.Errorf("expected filter to return false for mempool:info") + } + if filter("mempool", "error") { + t.Errorf("expected filter to return false for mempool:error") + } + if filter("state", "error") { + t.Errorf("expected filter to return false for state:error") + } + if filter("server", "panic") { + t.Errorf("expected filter to return false for server:panic") + } - assert.Assert(t, filter("server", "debug")) - assert.Assert(t, filter("state", "debug")) - assert.Assert(t, filter("state", "info")) + if !filter("server", "debug") { + t.Errorf("expected filter to return true for server:debug") + } + if !filter("state", "debug") { + t.Errorf("expected filter to return true for state:debug") + } + if !filter("state", "info") { + t.Errorf("expected filter to return true for state:info") + } level = "error" filter, err = log.ParseLogLevel(level) - assert.NilError(t, err) - assert.Assert(t, filter != nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if filter == nil { + t.Fatalf("expected non-nil filter, got nil") + } - assert.Assert(t, !filter("state", "error")) - assert.Assert(t, !filter("consensus", "error")) + if filter("state", "error") { + t.Errorf("expected filter to return false for state:error") + } + if filter("consensus", "error") { + t.Errorf("expected filter to return false for consensus:error") + } - assert.Assert(t, filter("consensus", "debug")) - assert.Assert(t, filter("consensus", "info")) - assert.Assert(t, filter("state", "debug")) + if !filter("consensus", "debug") { + t.Errorf("expected filter to return true for consensus:debug") + } + if !filter("consensus", "info") { + t.Errorf("expected filter to return true for consensus:info") + } + if !filter("state", "debug") { + t.Errorf("expected filter to return true for state:debug") + } } diff --git a/log/logger.go b/log/logger.go index 46ae75b619..d8f5a314c0 100644 --- a/log/logger.go +++ b/log/logger.go @@ -1,25 +1,50 @@ package log import ( + "encoding" + "encoding/json" + "fmt" "io" + "github.com/bytedance/sonic" + "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" ) +func init() { + zerolog.InterfaceMarshalFunc = func(i any) ([]byte, error) { + switch v := i.(type) { + case json.Marshaler: + return sonic.Marshal(i) + case encoding.TextMarshaler: + return sonic.Marshal(i) + case fmt.Stringer: + return sonic.Marshal(v.String()) + default: + return sonic.Marshal(i) + } + } +} + // ModuleKey defines a module logging key. const ModuleKey = "module" // ContextKey is used to store the logger in the context. -var ContextKey struct{} +var ContextKey contextKey + +type contextKey struct{} // Logger is the Cosmos SDK logger interface. -// It maintains as much backward compatibility with the CometBFT logger as possible. -// All functionalities of the logger are available through the Impl() method. type Logger interface { // Info takes a message and a set of key/value pairs and logs with level INFO. // The key of the tuple must be a string. Info(msg string, keyVals ...any) + // Warn takes a message and a set of key/value pairs and logs with level WARN. + // The key of the tuple must be a string. + Warn(msg string, keyVals ...any) + // Error takes a message and a set of key/value pairs and logs with level ERR. // The key of the tuple must be a string. Error(msg string, keyVals ...any) @@ -37,6 +62,22 @@ type Logger interface { Impl() any } +// WithJSONMarshal configures zerolog global json encoding. +func WithJSONMarshal(marshaler func(v any) ([]byte, error)) { + zerolog.InterfaceMarshalFunc = func(i any) ([]byte, error) { + switch v := i.(type) { + case json.Marshaler: + return marshaler(i) + case encoding.TextMarshaler: + return marshaler(i) + case fmt.Stringer: + return marshaler(v.String()) + default: + return marshaler(i) + } + } +} + type zeroLogWrapper struct { *zerolog.Logger } @@ -69,6 +110,15 @@ func NewLogger(dst io.Writer, options ...Option) Logger { } logger := zerolog.New(output) + + if logCfg.StackTrace { + zerolog.ErrorStackMarshaler = func(err error) interface{} { + return pkgerrors.MarshalStack(errors.WithStack(err)) + } + + logger = logger.With().Stack().Logger() + } + if logCfg.TimeFormat != "" { logger = logger.With().Timestamp().Logger() } @@ -77,6 +127,8 @@ func NewLogger(dst io.Writer, options ...Option) Logger { logger = logger.Level(logCfg.Level) } + logger = logger.Hook(logCfg.Hooks...) + return zeroLogWrapper{&logger} } @@ -91,13 +143,19 @@ func (l zeroLogWrapper) Info(msg string, keyVals ...interface{}) { l.Logger.Info().Fields(keyVals).Msg(msg) } -// Error takes a message and a set of key/value pairs and logs with level DEBUG. +// Warn takes a message and a set of key/value pairs and logs with level WARN. +// The key of the tuple must be a string. +func (l zeroLogWrapper) Warn(msg string, keyVals ...interface{}) { + l.Logger.Warn().Fields(keyVals).Msg(msg) +} + +// Error takes a message and a set of key/value pairs and logs with level ERROR. // The key of the tuple must be a string. func (l zeroLogWrapper) Error(msg string, keyVals ...interface{}) { l.Logger.Error().Fields(keyVals).Msg(msg) } -// Debug takes a message and a set of key/value pairs and logs with level ERR. +// Debug takes a message and a set of key/value pairs and logs with level DEBUG. // The key of the tuple must be a string. func (l zeroLogWrapper) Debug(msg string, keyVals ...interface{}) { l.Logger.Debug().Fields(keyVals).Msg(msg) @@ -109,6 +167,12 @@ func (l zeroLogWrapper) With(keyVals ...interface{}) Logger { return zeroLogWrapper{&logger} } +// WithContext returns a new wrapped logger with additional context provided by a set. +func (l zeroLogWrapper) WithContext(keyVals ...interface{}) any { + logger := l.Logger.With().Fields(keyVals).Logger() + return zeroLogWrapper{&logger} +} + // Impl returns the underlying zerolog logger. // It can be used to used zerolog structured API directly instead of the wrapper. func (l zeroLogWrapper) Impl() interface{} { @@ -126,8 +190,10 @@ func NewNopLogger() Logger { // The custom implementation is about 3x faster. type nopLogger struct{} -func (nopLogger) Info(string, ...any) {} -func (nopLogger) Error(string, ...any) {} -func (nopLogger) Debug(string, ...any) {} -func (nopLogger) With(...any) Logger { return nopLogger{} } -func (nopLogger) Impl() any { return nopLogger{} } +func (nopLogger) Info(string, ...any) {} +func (nopLogger) Warn(string, ...any) {} +func (nopLogger) Error(string, ...any) {} +func (nopLogger) Debug(string, ...any) {} +func (nopLogger) With(...any) Logger { return nopLogger{} } +func (nopLogger) WithContext(...any) any { return nopLogger{} } +func (nopLogger) Impl() any { return nopLogger{} } diff --git a/log/logger_test.go b/log/logger_test.go new file mode 100644 index 0000000000..752026a25b --- /dev/null +++ b/log/logger_test.go @@ -0,0 +1,61 @@ +package log_test + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/rs/zerolog" + + "cosmossdk.io/log" +) + +func inner() error { + return errors.New("seems we have an error here") +} + +type _MockHook string + +func (h _MockHook) Run(e *zerolog.Event, l zerolog.Level, msg string) { + e.Bool(string(h), true) +} + +func TestLoggerOptionHooks(t *testing.T) { + buf := new(bytes.Buffer) + var ( + mockHook1 _MockHook = "mock_message1" + mockHook2 _MockHook = "mock_message2" + ) + logger := log.NewLogger(buf, log.HooksOption(mockHook1, mockHook2), log.ColorOption(false)) + logger.Info("hello world") + if !strings.Contains(buf.String(), "mock_message1=true") { + t.Fatalf("expected mock_message1=true, got: %s", buf.String()) + } + if !strings.Contains(buf.String(), "mock_message2=true") { + t.Fatalf("expected mock_message2=true, got: %s", buf.String()) + } + + buf.Reset() + logger = log.NewLogger(buf, log.HooksOption(), log.ColorOption(false)) + logger.Info("hello world") + if !strings.Contains(buf.String(), "hello world") { + t.Fatalf("expected hello world, got: %s", buf.String()) + } +} + +func TestLoggerOptionStackTrace(t *testing.T) { + buf := new(bytes.Buffer) + logger := log.NewLogger(buf, log.TraceOption(true), log.ColorOption(false)) + logger.Error("this log should be displayed", "error", inner()) + if strings.Count(buf.String(), "logger_test.go") != 1 { + t.Fatalf("stack trace not found, got: %s", buf.String()) + } + buf.Reset() + + logger = log.NewLogger(buf, log.TraceOption(false), log.ColorOption(false)) + logger.Error("this log should be displayed", "error", inner()) + if strings.Count(buf.String(), "logger_test.go") > 0 { + t.Fatalf("stack trace found, got: %s", buf.String()) + } +} diff --git a/log/options.go b/log/options.go index f939fd4863..28cfefee2e 100644 --- a/log/options.go +++ b/log/options.go @@ -12,7 +12,9 @@ var defaultConfig = Config{ Filter: nil, OutputJSON: false, Color: true, + StackTrace: false, TimeFormat: time.Kitchen, + Hooks: nil, } // Config defines configuration for the logger. @@ -21,7 +23,9 @@ type Config struct { Filter FilterFunc OutputJSON bool Color bool + StackTrace bool TimeFormat string + Hooks []zerolog.Hook } type Option func(*Config) @@ -78,3 +82,17 @@ func TimeFormatOption(format string) Option { cfg.TimeFormat = format } } + +// TraceOption add option to enable/disable print of stacktrace on error log +func TraceOption(val bool) Option { + return func(cfg *Config) { + cfg.StackTrace = val + } +} + +// HooksOption append hooks to the Logger hooks +func HooksOption(hooks ...zerolog.Hook) Option { + return func(cfg *Config) { + cfg.Hooks = append(cfg.Hooks, hooks...) + } +} diff --git a/log/slog/logger.go b/log/slog/logger.go new file mode 100644 index 0000000000..4b6c30ec4b --- /dev/null +++ b/log/slog/logger.go @@ -0,0 +1,50 @@ +// Package slog contains a Logger type that satisfies [cosmossdk.io/log.Logger], +// backed by a standard library [*log/slog.Logger]. +package slog + +import ( + "log/slog" + + "cosmossdk.io/log" +) + +var _ log.Logger = Logger{} + +// Logger satisfies [log.Logger] with logging backed by +// an instance of [*slog.Logger]. +type Logger struct { + log *slog.Logger +} + +// NewCustomLogger returns a Logger backed by an existing slog.Logger instance. +// All logging methods are called directly on the *slog.Logger; +// therefore it is the caller's responsibility to configure message filtering, +// level filtering, output format, and so on. +func NewCustomLogger(log *slog.Logger) Logger { + return Logger{log: log} +} + +func (l Logger) Info(msg string, keyVals ...any) { + l.log.Info(msg, keyVals...) +} + +func (l Logger) Warn(msg string, keyVals ...any) { + l.log.Warn(msg, keyVals...) +} + +func (l Logger) Error(msg string, keyVals ...any) { + l.log.Error(msg, keyVals...) +} + +func (l Logger) Debug(msg string, keyVals ...any) { + l.log.Debug(msg, keyVals...) +} + +func (l Logger) With(keyVals ...any) log.Logger { + return Logger{log: l.log.With(keyVals...)} +} + +// Impl returns l's underlying [*slog.Logger]. +func (l Logger) Impl() any { + return l.log +} diff --git a/log/slog/logger_test.go b/log/slog/logger_test.go new file mode 100644 index 0000000000..4a7a9d836e --- /dev/null +++ b/log/slog/logger_test.go @@ -0,0 +1,92 @@ +package slog_test + +import ( + "bytes" + "encoding/json" + stdslog "log/slog" + "testing" + + "cosmossdk.io/log/slog" +) + +func TestSlog(t *testing.T) { + var buf bytes.Buffer + h := stdslog.NewJSONHandler(&buf, &stdslog.HandlerOptions{ + Level: stdslog.LevelDebug, + }) + logger := slog.NewCustomLogger(stdslog.New(h)) + + type logLine struct { + Level string `json:"level"` + Msg string `json:"msg"` + Num int `json:"num"` + } + + var line logLine + + logger.Debug("Message one", "num", 1) + if err := json.Unmarshal(buf.Bytes(), &line); err != nil { + t.Fatal(err) + } + if want := (logLine{ + Level: stdslog.LevelDebug.String(), + Msg: "Message one", + Num: 1, + }); want != line { + t.Fatalf("unexpected log record: want %v, got %v", want, line) + } + + buf.Reset() + logger.Info("Message two", "num", 2) + if err := json.Unmarshal(buf.Bytes(), &line); err != nil { + t.Fatal(err) + } + if want := (logLine{ + Level: stdslog.LevelInfo.String(), + Msg: "Message two", + Num: 2, + }); want != line { + t.Fatalf("unexpected log record: want %v, got %v", want, line) + } + + buf.Reset() + logger.Warn("Message three", "num", 3) + if err := json.Unmarshal(buf.Bytes(), &line); err != nil { + t.Fatal(err) + } + if want := (logLine{ + Level: stdslog.LevelWarn.String(), + Msg: "Message three", + Num: 3, + }); want != line { + t.Fatalf("unexpected log record: want %v, got %v", want, line) + } + + buf.Reset() + logger.Error("Message four", "num", 4) + if err := json.Unmarshal(buf.Bytes(), &line); err != nil { + t.Fatal(err) + } + if want := (logLine{ + Level: stdslog.LevelError.String(), + Msg: "Message four", + Num: 4, + }); want != line { + t.Fatalf("unexpected log record: want %v, got %v", want, line) + } + + wLogger := logger.With("num", 5) + buf.Reset() + wLogger.Info("Using .With") + + if err := json.Unmarshal(buf.Bytes(), &line); err != nil { + t.Fatal(err) + } + if want := (logLine{ + Level: stdslog.LevelInfo.String(), + Msg: "Using .With", + Num: 5, + }); want != line { + t.Fatalf("unexpected log record: want %v, got %v", want, line) + } +} diff --git a/log/sonar-project.properties b/log/sonar-project.properties new file mode 100644 index 0000000000..ebb57d7d15 --- /dev/null +++ b/log/sonar-project.properties @@ -0,0 +1,16 @@ +sonar.projectKey=cosmos-sdk-log +sonar.organization=cosmos + +sonar.projectName=Cosmos SDK - Log +sonar.project.monorepo.enabled=true + +sonar.sources=. +sonar.exclusions=**/*_test.go,**/*.pb.go,**/*.pulsar.go,**/*.pb.gw.go +sonar.coverage.exclusions=**/*_test.go,**/testutil/**,**/*.pb.go,**/*.pb.gw.go,**/*.pulsar.go,test_helpers.go,docs/** +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.go.coverage.reportPaths=coverage.out + +sonar.sourceEncoding=UTF-8 +sonar.scm.provider=git +sonar.scm.forceReloadAll=true diff --git a/log/testing.go b/log/testing.go index f31f6e5182..0e82eced62 100644 --- a/log/testing.go +++ b/log/testing.go @@ -1,6 +1,10 @@ package log -import "github.com/rs/zerolog" +import ( + "time" + + "github.com/rs/zerolog" +) // TestingT is the interface required for logging in tests. // It is a subset of testing.T to avoid a direct dependency on the testing package. @@ -39,15 +43,18 @@ func NewTestLoggerError(t TestingT) Logger { } func newTestLogger(t TestingT, lvl zerolog.Level) Logger { - cw := zerolog.NewConsoleWriter() - cw.Out = zerolog.TestWriter{ - T: t, - // Normally one would use zerolog.ConsoleTestWriter - // to set the option on NewConsoleWriter, - // but the zerolog source for that is hardcoded to Frame=6. - // With Frame=6, all source locations are printed as "logger.go", - // but Frame=7 prints correct source locations. - Frame: 7, + cw := zerolog.ConsoleWriter{ + NoColor: true, + TimeFormat: time.Kitchen, + Out: zerolog.TestWriter{ + T: t, + // Normally one would use zerolog.ConsoleTestWriter + // to set the option on NewConsoleWriter, + // but the zerolog source for that is hardcoded to Frame=6. + // With Frame=6, all source locations are printed as "logger.go", + // but Frame=7 prints correct source locations. + Frame: 7, + }, } return NewCustomLogger(zerolog.New(cw).Level(lvl)) } diff --git a/log/writer.go b/log/writer.go index 9c6befef71..d6f0dc2381 100644 --- a/log/writer.go +++ b/log/writer.go @@ -1,9 +1,10 @@ package log import ( - "encoding/json" "fmt" "io" + + "github.com/bytedance/sonic" ) // NewFilterWriter returns a writer that filters out all key/value pairs that do not match the filter. @@ -28,7 +29,7 @@ func (fw *filterWriter) Write(p []byte) (n int, err error) { Module string `json:"module"` } - if err := json.Unmarshal(p, &event); err != nil { + if err := sonic.Unmarshal(p, &event); err != nil { return 0, fmt.Errorf("failed to unmarshal event: %w", err) } diff --git a/log/writer_test.go b/log/writer_test.go index 1c43030f8d..d69d6ccdd6 100644 --- a/log/writer_test.go +++ b/log/writer_test.go @@ -5,8 +5,6 @@ import ( "strings" "testing" - "gotest.tools/v3/assert" - "cosmossdk.io/log" ) @@ -15,13 +13,19 @@ func TestFilteredWriter(t *testing.T) { level := "consensus:debug,mempool:debug,*:error" filter, err := log.ParseLogLevel(level) - assert.NilError(t, err) + if err != nil { + t.Fatalf("failed to parse log level: %v", err) + } logger := log.NewLogger(buf, log.FilterOption(filter)) logger.Debug("this log line should be displayed", log.ModuleKey, "consensus") - assert.Check(t, strings.Contains(buf.String(), "this log line should be displayed")) + if !strings.Contains(buf.String(), "this log line should be displayed") { + t.Errorf("expected log line to be displayed, but it was not") + } buf.Reset() logger.Debug("this log line should be filtered", log.ModuleKey, "server") - assert.Check(t, buf.Len() == 0) + if buf.Len() != 0 { + t.Errorf("expected log line to be filtered, but it was not") + } }