From 098dbe6ff4f59652181c8e0e3873fbfcf0e65ea3 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Tue, 19 Jul 2022 09:47:55 +0100 Subject: [PATCH] [chore] use our own logging implementation (#716) * first commit Signed-off-by: kim * replace logging with our own log library Signed-off-by: kim * fix imports Signed-off-by: kim * fix log imports Signed-off-by: kim * add license text Signed-off-by: kim * fix package import cycle between config and log package Signed-off-by: kim * fix empty kv.Fields{} being passed to WithFields() Signed-off-by: kim * fix uses of log.WithFields() with whitespace issues and empty slices Signed-off-by: kim * *linter related grumbling* Signed-off-by: kim * gofmt the codebase! also fix more log.WithFields() formatting issues Signed-off-by: kim * update testrig code to match new changes Signed-off-by: kim * fix error wrapping in non fmt.Errorf function Signed-off-by: kim * add benchmarking of log.Caller() vs non-cached Signed-off-by: kim * fix syslog tests, add standard build tags to test runner to ensure consistency Signed-off-by: kim * make syslog tests more robust Signed-off-by: kim * fix caller depth arithmatic (is that how you spell it?) Signed-off-by: kim * update to use unkeyed fields in kv.Field{} instances Signed-off-by: kim * update go-kv library Signed-off-by: kim * update libraries list Signed-off-by: kim * fuck you linter get nerfed Signed-off-by: kim Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com> --- .golangci.yml | 5 + README.md | 2 +- cmd/gotosocial/action/server/server.go | 6 +- cmd/gotosocial/action/testrig/testrig.go | 6 +- cmd/gotosocial/common.go | 15 +- cmd/gotosocial/main.go | 4 +- go.mod | 6 +- go.sum | 12 + internal/ap/interfaces.go | 3 +- internal/api/client/fileserver/servefile.go | 4 +- .../api/client/fileserver/servefile_test.go | 4 +- internal/api/client/media/mediacreate_test.go | 4 +- internal/api/client/media/mediaupdate_test.go | 4 +- internal/api/client/status/status.go | 5 +- internal/api/client/status/statusboostedby.go | 15 +- internal/api/client/streaming/stream.go | 15 +- internal/api/errorhandling.go | 13 +- internal/api/s2s/webfinger/webfingerget.go | 10 +- internal/api/security/signaturecheck.go | 8 +- internal/api/security/tokencheck.go | 29 +- internal/concurrency/workers.go | 14 +- internal/config/validate.go | 4 +- internal/db/bundb/account.go | 6 +- internal/db/bundb/admin.go | 15 +- internal/db/bundb/basic.go | 5 +- internal/db/bundb/bundb.go | 16 +- internal/db/bundb/hook.go | 24 +- internal/db/bundb/instance.go | 5 +- internal/db/bundb/mention.go | 4 +- ...20220612091800_duplicated_media_cleanup.go | 6 +- internal/db/bundb/migrations/main.go | 6 +- internal/db/bundb/notification.go | 4 +- internal/db/bundb/status.go | 10 +- internal/db/bundb/timeline.go | 8 +- internal/email/confirm.go | 4 +- internal/email/noopsender.go | 6 +- internal/email/util.go | 1 - internal/federation/authenticate.go | 51 +- internal/federation/dereferencing/account.go | 7 +- internal/federation/dereferencing/status.go | 27 +- internal/federation/dereferencing/thread.go | 33 +- internal/federation/federatingactor.go | 4 +- internal/federation/federatingdb/accept.go | 13 +- internal/federation/federatingdb/announce.go | 13 +- internal/federation/federatingdb/create.go | 23 +- internal/federation/federatingdb/delete.go | 13 +- internal/federation/federatingdb/exists.go | 12 +- internal/federation/federatingdb/followers.go | 12 +- internal/federation/federatingdb/following.go | 12 +- internal/federation/federatingdb/get.go | 12 +- internal/federation/federatingdb/owns.go | 12 +- internal/federation/federatingdb/reject.go | 13 +- internal/federation/federatingdb/undo.go | 11 +- internal/federation/federatingdb/update.go | 11 +- internal/federation/federatingdb/util.go | 17 +- internal/federation/federatingprotocol.go | 30 +- internal/log/caller.go | 89 ++ internal/log/caller_test.go | 54 + internal/log/entry.go | 117 +++ internal/log/init.go | 62 ++ internal/log/log.go | 312 ++++-- internal/log/log_test.go | 69 -- internal/log/pool.go | 49 + internal/log/syslog_test.go | 24 +- internal/log/trimhook.go | 53 - internal/log/writer.go | 37 + internal/media/manager.go | 18 +- internal/media/processingemoji.go | 4 +- internal/media/processingmedia.go | 30 +- internal/media/prunemeta.go | 10 +- internal/media/prunemeta_test.go | 1 + internal/media/pruneremote.go | 12 +- internal/media/pruneunusedlocal.go | 12 +- internal/media/util.go | 9 +- internal/oauth/clientstore_test.go | 2 - internal/oauth/server.go | 12 +- internal/oauth/tokenstore.go | 8 +- internal/oidc/handlecallback.go | 13 +- internal/processing/account/create.go | 9 +- internal/processing/account/delete.go | 15 +- internal/processing/account/update.go | 9 +- .../processing/admin/createdomainblock.go | 11 +- .../processing/admin/importdomainblocks.go | 2 - internal/processing/admin/mediaprune.go | 14 +- internal/processing/fromclientapi.go | 21 + internal/processing/fromfederator.go | 34 +- internal/processing/media/getemoji.go | 4 +- internal/processing/notification.go | 7 +- internal/processing/search.go | 21 +- internal/processing/status/util.go | 6 +- internal/processing/status/util_test.go | 11 +- internal/processing/statustimeline.go | 21 +- internal/processing/streaming/openstream.go | 13 +- internal/router/logger.go | 44 +- internal/router/router.go | 12 +- internal/router/session.go | 4 +- internal/storage/local.go | 5 + internal/storage/s3.go | 5 + internal/storage/storage.go | 4 +- internal/text/common.go | 5 +- internal/timeline/get.go | 31 +- internal/timeline/index.go | 19 +- internal/timeline/manager.go | 41 +- internal/timeline/prepare.go | 25 +- internal/timeline/remove.go | 24 +- internal/trans/import.go | 21 +- internal/transport/controller.go | 4 +- internal/transport/derefinstance.go | 19 +- internal/transport/transport.go | 13 +- internal/typeutils/astointernal.go | 5 +- internal/typeutils/internaltoas.go | 6 +- internal/typeutils/internaltofrontend.go | 31 +- internal/typeutils/wrap.go | 1 - internal/validate/account_test.go | 1 - internal/visibility/statusboostable.go | 14 +- internal/visibility/statushometimelineable.go | 11 +- .../visibility/statuspublictimelineable.go | 11 +- internal/visibility/statusvisible.go | 11 +- internal/web/assetscache.go | 4 +- scripts/build.sh | 2 +- scripts/test.sh | 4 +- testrig/db.go | 52 +- testrig/log.go | 16 +- testrig/transportcontroller.go | 12 +- vendor/codeberg.org/gruf/go-kv/LICENSE | 9 + vendor/codeberg.org/gruf/go-kv/README.md | 9 + vendor/codeberg.org/gruf/go-kv/benchmark.png | Bin 0 -> 97592 bytes vendor/codeberg.org/gruf/go-kv/field.go | 105 ++ vendor/codeberg.org/gruf/go-kv/field_fmt.go | 36 + .../codeberg.org/gruf/go-kv/field_format.go | 35 + .../codeberg.org/gruf/go-kv/format/README.md | 5 + .../gruf/go-kv/format/benchmark.png | Bin 0 -> 36505 bytes .../codeberg.org/gruf/go-kv/format/format.go | 922 ++++++++++++++++++ .../gruf/go-kv/format/formatter.go | 349 +++++++ vendor/codeberg.org/gruf/go-kv/format/util.go | 56 ++ vendor/codeberg.org/gruf/go-kv/util.go | 71 ++ vendor/codeberg.org/gruf/go-logger/v2/LICENSE | 9 + .../gruf/go-logger/v2/level/levels.go | 49 + .../sirupsen/logrus/hooks/syslog/README.md | 39 - .../sirupsen/logrus/hooks/syslog/syslog.go | 55 -- vendor/modules.txt | 8 +- 141 files changed, 3046 insertions(+), 997 deletions(-) create mode 100644 internal/log/caller.go create mode 100644 internal/log/caller_test.go create mode 100644 internal/log/entry.go create mode 100644 internal/log/init.go delete mode 100644 internal/log/log_test.go create mode 100644 internal/log/pool.go delete mode 100644 internal/log/trimhook.go create mode 100644 internal/log/writer.go create mode 100644 vendor/codeberg.org/gruf/go-kv/LICENSE create mode 100644 vendor/codeberg.org/gruf/go-kv/README.md create mode 100644 vendor/codeberg.org/gruf/go-kv/benchmark.png create mode 100644 vendor/codeberg.org/gruf/go-kv/field.go create mode 100644 vendor/codeberg.org/gruf/go-kv/field_fmt.go create mode 100644 vendor/codeberg.org/gruf/go-kv/field_format.go create mode 100644 vendor/codeberg.org/gruf/go-kv/format/README.md create mode 100644 vendor/codeberg.org/gruf/go-kv/format/benchmark.png create mode 100644 vendor/codeberg.org/gruf/go-kv/format/format.go create mode 100644 vendor/codeberg.org/gruf/go-kv/format/formatter.go create mode 100644 vendor/codeberg.org/gruf/go-kv/format/util.go create mode 100644 vendor/codeberg.org/gruf/go-kv/util.go create mode 100644 vendor/codeberg.org/gruf/go-logger/v2/LICENSE create mode 100644 vendor/codeberg.org/gruf/go-logger/v2/level/levels.go delete mode 100644 vendor/github.com/sirupsen/logrus/hooks/syslog/README.md delete mode 100644 vendor/github.com/sirupsen/logrus/hooks/syslog/syslog.go diff --git a/.golangci.yml b/.golangci.yml index a8a3b45e3..b117af924 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,3 +24,8 @@ linters: - nilerr - revive - wastedassign + +linters-settings: + govet: + disable: + - composites \ No newline at end of file diff --git a/README.md b/README.md index b5ca41aa7..c0107eaac 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ The following libraries and frameworks are used by GoToSocial, with gratitude - [go-playground/validator](https://github.com/go-playground/validator); struct validation. [MIT License](https://spdx.org/licenses/MIT.html) - [gorilla/websocket](https://github.com/gorilla/websocket); Websocket connectivity. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html). - [gruf/go-debug](https://codeberg.org/gruf/go-debug); profiling support in debug builds. [MIT License](https://spdx.org/licenses/MIT.html). +- [gruf/go-kv](https://codeberg.org/gruf/go-kv); key-value field formatting. [MIT License](https://spdx.org/licenses/MIT.html). - [gruf/go-mutexes](https://codeberg.org/gruf/go-mutexes); mutex map. [MIT License](https://spdx.org/licenses/MIT.html). - [gruf/go-runners](https://codeberg.org/gruf/go-runners); worker pool library. [MIT License](https://spdx.org/licenses/MIT.html). - [gruf/go-store](https://codeberg.org/gruf/go-store); local media store. [MIT License](https://spdx.org/licenses/MIT.html). @@ -235,7 +236,6 @@ The following libraries and frameworks are used by GoToSocial, with gratitude - [ReneKroon/ttlcache](https://github.com/ReneKroon/ttlcache); in-memory caching. [MIT License](https://spdx.org/licenses/MIT.html). - [robfig/cron](https://github.com/robfig/cron); cron job scheduling. [MIT License](https://spdx.org/licenses/MIT.html). - [russross/blackfriday](https://github.com/russross/blackfriday); markdown parsing for statuses. [Simplified BSD License](https://spdx.org/licenses/BSD-2-Clause.html). -- [sirupsen/logrus](https://github.com/sirupsen/logrus); logging. [MIT License](https://spdx.org/licenses/MIT.html). - [spf13/cobra](https://github.com/spf13/cobra); command-line tooling. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). - [spf13/pflag](https://github.com/spf13/pflag); command-line flag utilities. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). - [spf13/viper](https://github.com/spf13/viper); configuration management. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index 35dc029c5..ee327c0c5 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -25,7 +25,6 @@ import ( "os/signal" "syscall" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/client/account" @@ -59,6 +58,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" "github.com/superseriousbusiness/gotosocial/internal/httpclient" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -236,13 +236,13 @@ var Start action.GTSAction = func(ctx context.Context) error { sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) sig := <-sigs - logrus.Infof("received signal %s, shutting down", sig) + log.Infof("received signal %s, shutting down", sig) // close down all running services in order if err := gts.Stop(ctx); err != nil { return fmt.Errorf("error closing gotosocial service: %s", err) } - logrus.Info("done! exiting...") + log.Info("done! exiting...") return nil } diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index b5526dc32..739358f99 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -28,7 +28,6 @@ import ( "os/signal" "syscall" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/client/account" @@ -56,6 +55,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/storage" @@ -189,7 +189,7 @@ var Start action.GTSAction = func(ctx context.Context) error { sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) sig := <-sigs - logrus.Infof("received signal %s, shutting down", sig) + log.Infof("received signal %s, shutting down", sig) testrig.StandardDBTeardown(dbService) testrig.StandardStorageTeardown(storageBackend) @@ -199,6 +199,6 @@ var Start action.GTSAction = func(ctx context.Context) error { return fmt.Errorf("error closing gotosocial service: %s", err) } - logrus.Info("done! exiting...") + log.Info("done! exiting...") return nil } diff --git a/cmd/gotosocial/common.go b/cmd/gotosocial/common.go index 43e2f16ad..4ebdc5485 100644 --- a/cmd/gotosocial/common.go +++ b/cmd/gotosocial/common.go @@ -64,8 +64,19 @@ func preRun(a preRunArgs) error { // The idea here is to take a GTSAction and run it with the given // context, after initializing any last-minute things like loggers etc. func run(ctx context.Context, action action.GTSAction) error { - if err := log.Initialize(); err != nil { - return fmt.Errorf("error initializing log: %s", err) + // Set the global log level from configuration + if err := log.ParseLevel(config.GetLogLevel()); err != nil { + return fmt.Errorf("error parsing log level: %w", err) + } + + if config.GetSyslogEnabled() { + // Enable logging to syslog + if err := log.EnableSyslog( + config.GetSyslogProtocol(), + config.GetSyslogAddress(), + ); err != nil { + return fmt.Errorf("error enabling syslogging: %w", err) + } } return action(ctx) diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 1b815f6b7..6edede6d5 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -19,10 +19,10 @@ package main import ( + "log" "runtime/debug" "strings" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" _ "github.com/superseriousbusiness/gotosocial/docs" @@ -66,7 +66,7 @@ func main() { // run if err := rootCmd.Execute(); err != nil { - logrus.Fatalf("error executing command: %s", err) + log.Fatalf("error executing command: %s", err) } } diff --git a/go.mod b/go.mod index e01678a25..3d963019d 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module github.com/superseriousbusiness/gotosocial go 1.18 require ( + codeberg.org/gruf/go-atomics v1.1.0 codeberg.org/gruf/go-bytesize v0.2.1 codeberg.org/gruf/go-byteutil v1.0.2 codeberg.org/gruf/go-cache/v2 v2.1.1 codeberg.org/gruf/go-debug v1.2.0 codeberg.org/gruf/go-errors/v2 v2.0.2 + codeberg.org/gruf/go-kv v1.3.2 + codeberg.org/gruf/go-logger/v2 v2.0.6 codeberg.org/gruf/go-mutexes v1.1.2 codeberg.org/gruf/go-runners v1.2.1 codeberg.org/gruf/go-store v1.3.8 @@ -32,7 +35,6 @@ require ( github.com/oklog/ulid v1.3.1 github.com/robfig/cron/v3 v3.0.1 github.com/russross/blackfriday/v2 v2.1.0 - github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.11.0 github.com/stretchr/testify v1.7.1 @@ -55,7 +57,6 @@ require ( ) require ( - codeberg.org/gruf/go-atomics v1.1.0 // indirect codeberg.org/gruf/go-bitutil v1.0.1 // indirect codeberg.org/gruf/go-bytes v1.0.2 // indirect codeberg.org/gruf/go-fastcopy v1.1.1 // indirect @@ -113,6 +114,7 @@ require ( github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rs/xid v1.2.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index c6ab9451e..dda597dca 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ codeberg.org/gruf/go-atomics v1.1.0/go.mod h1:a/4/y/LgvjxjQVnpoy1VVkOSzLS1W9i1g4 codeberg.org/gruf/go-bitutil v1.0.0/go.mod h1:sb8IjlDnjVTz8zPK/8lmHesKxY0Yb3iqHWjUM/SkphA= codeberg.org/gruf/go-bitutil v1.0.1 h1:l8z9nOvCpHhicU2LZyJ6jLK03UNzCF6bxVCwu+VEenQ= codeberg.org/gruf/go-bitutil v1.0.1/go.mod h1:3ezHnADoiRJs9jgn65AEZ3HY7dsabAYLmmnIvseCGJI= +codeberg.org/gruf/go-buf v1.0.0/go.mod h1:9fuVcFZsqnuXyeNFr9DtrxR4c6MIRoDE37YV7B2egD0= codeberg.org/gruf/go-bytes v1.0.0/go.mod h1:1v/ibfaosfXSZtRdW2rWaVrDXMc9E3bsi/M9Ekx39cg= codeberg.org/gruf/go-bytes v1.0.2 h1:malqE42Ni+h1nnYWBUAJaDDtEzF4aeN4uPN8DfMNNvo= codeberg.org/gruf/go-bytes v1.0.2/go.mod h1:1v/ibfaosfXSZtRdW2rWaVrDXMc9E3bsi/M9Ekx39cg= @@ -51,6 +52,7 @@ codeberg.org/gruf/go-byteutil v1.0.2/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6 codeberg.org/gruf/go-cache v1.1.2/go.mod h1:/Dbc+xU72Op3hMn6x2PXF3NE9uIDFeS+sXPF00hN/7o= codeberg.org/gruf/go-cache/v2 v2.1.1 h1:Ate0URlO6dVTYGxcIST9sDKtwOqrqBmlm73zy3Bq1+k= codeberg.org/gruf/go-cache/v2 v2.1.1/go.mod h1:XstSofaENTH3PNBoMrBcowFJ339OcJfTwO0PCOp5lKQ= +codeberg.org/gruf/go-ctx v1.0.2/go.mod h1:iMyid3m2Rr1wSWL+snliBK5qE51b4pLY5CozVUrb/QU= codeberg.org/gruf/go-debug v1.2.0 h1:WBbTMnK1ArFKUmgv04aO2JiC/daTOB8zQGi521qb7OU= codeberg.org/gruf/go-debug v1.2.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg= codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4= @@ -63,6 +65,14 @@ codeberg.org/gruf/go-fastpath v1.0.3 h1:3Iftz9Z2suCEgTLkQMucew+2+4Oe46JPbAM2JEhn codeberg.org/gruf/go-fastpath v1.0.3/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI= codeberg.org/gruf/go-hashenc v1.0.2 h1:U3jH6zMXZiL96czD/qaJd8OR2h7LlBzGv/2WxnMHI/g= codeberg.org/gruf/go-hashenc v1.0.2/go.mod h1:eK+A8clLcEN/m1nftNsRId0kfYDQnETnuIfBGZ8Gvsg= +codeberg.org/gruf/go-kv v1.2.0/go.mod h1:z2+9sJc3cWuW9vUZI5tX0Uc9DNgmDdFihsThkShUk6w= +codeberg.org/gruf/go-kv v1.2.1/go.mod h1:nTmEd529nq8jV8jXwrr7HXR5nHRnKA9jPR0R8PX/EYw= +codeberg.org/gruf/go-kv v1.3.2 h1:FVWQMCZfFJlzYssOQDXs/sJ8kLhcOx4CPJmdzxfe41c= +codeberg.org/gruf/go-kv v1.3.2/go.mod h1:0I7qkI6eLpUtv940vrPasHFhMqlZojGlHf7iF2/Z86A= +codeberg.org/gruf/go-log v1.0.4/go.mod h1:XOkZfvx78Q214dWVKuaPFqkRWJQBipsdou+LrDRlwB8= +codeberg.org/gruf/go-logger/v2 v2.0.6 h1:uKpfwQufP76avJ326WUWI40T1qIh7UbxpPlA9UQ8zr8= +codeberg.org/gruf/go-logger/v2 v2.0.6/go.mod h1:9kg3GVDGzBXsrv1vALNRm/NJAkPvs5P000DLzladkb0= +codeberg.org/gruf/go-middleware v1.1.0/go.mod h1:6D5GVA+qdg/xd+FP+6RgX61mWaMlZZ2Jz6dowJs0rPI= codeberg.org/gruf/go-mutexes v1.1.2 h1:AMC1CFV6kMi+iBjR3yQv8yIagG3lWm68U6sQHYFHEf4= codeberg.org/gruf/go-mutexes v1.1.2/go.mod h1:1j/6/MBeBQUedAtAtysLLnBKogfOZAxdym0E3wlaBD8= codeberg.org/gruf/go-nowish v1.0.0/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s= @@ -76,6 +86,7 @@ codeberg.org/gruf/go-sched v1.0.1 h1:+EAXSVI4orY5lMNX2Vrke/UxF2qjmEy6gHcyySZg/3k codeberg.org/gruf/go-sched v1.0.1/go.mod h1:LFzosJL0yrCNtXg9Vq9iwr4q6ANuRirO2cVwKYH7CLs= codeberg.org/gruf/go-store v1.3.8 h1:7Hzzsa8gaOc6spuGWXJVUWRAyKiOR/m60/jNYrD8cT0= codeberg.org/gruf/go-store v1.3.8/go.mod h1:Fy5pXEHiIVFRWDx8DfILwXS1ulrj/jLdSK2C2oElz3I= +codeberg.org/gruf/go-ulid v1.0.0/go.mod h1:dVNsZJpVTge8+jfBTjAMpnscYSiCqk+g32ZgtorCAMs= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -140,6 +151,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.3/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 240d965ca..35ce937a1 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -261,8 +261,7 @@ type WithSensitive interface { } // WithConversation ... -type WithConversation interface { - // TODO +type WithConversation interface { // TODO } // WithContent represents an activity with ActivityStreamsContentProperty diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go index ac2fa7bc2..fffa15a1c 100644 --- a/internal/api/client/fileserver/servefile.go +++ b/internal/api/client/fileserver/servefile.go @@ -24,10 +24,10 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -93,7 +93,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { // if the content is a ReadCloser, close it when we're done if closer, ok := content.Content.(io.ReadCloser); ok { if err := closer.Close(); err != nil { - logrus.Errorf("ServeFile: error closing readcloser: %s", err) + log.Errorf("ServeFile: error closing readcloser: %s", err) } } }() diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index e918a9026..8e9cbe548 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -27,7 +27,6 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" "github.com/superseriousbusiness/gotosocial/internal/concurrency" @@ -35,6 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -96,7 +96,7 @@ func (suite *ServeFileTestSuite) SetupSuite() { func (suite *ServeFileTestSuite) TearDownSuite() { if err := suite.db.Stop(context.Background()); err != nil { - logrus.Panicf("error closing db connection: %s", err) + log.Panicf("error closing db connection: %s", err) } } diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 935fc2970..595edb73d 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -30,7 +30,6 @@ import ( "net/http/httptest" "testing" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -40,6 +39,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -100,7 +100,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() { func (suite *MediaCreateTestSuite) TearDownSuite() { if err := suite.db.Stop(context.Background()); err != nil { - logrus.Panicf("error closing db connection: %s", err) + log.Panicf("error closing db connection: %s", err) } } diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index deb93d83c..8081a5c15 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -29,7 +29,6 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -39,6 +38,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -99,7 +99,7 @@ func (suite *MediaUpdateTestSuite) SetupSuite() { func (suite *MediaUpdateTestSuite) TearDownSuite() { if err := suite.db.Stop(context.Background()); err != nil { - logrus.Panicf("error closing db connection: %s", err) + log.Panicf("error closing db connection: %s", err) } } diff --git a/internal/api/client/status/status.go b/internal/api/client/status/status.go index abb23f1ca..67f046abd 100644 --- a/internal/api/client/status/status.go +++ b/internal/api/client/status/status.go @@ -22,10 +22,9 @@ import ( "net/http" "strings" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/router" ) @@ -105,7 +104,7 @@ func (m *Module) Route(r router.Router) error { // muxHandler is a little workaround to overcome the limitations of Gin func (m *Module) muxHandler(c *gin.Context) { - logrus.Debug("entering mux handler") + log.Debug("entering mux handler") ru := c.Request.RequestURI if c.Request.Method == http.MethodGet { diff --git a/internal/api/client/status/statusboostedby.go b/internal/api/client/status/statusboostedby.go index 382b38f7b..fbe447bf4 100644 --- a/internal/api/client/status/statusboostedby.go +++ b/internal/api/client/status/statusboostedby.go @@ -21,9 +21,10 @@ package status import ( "net/http" + "codeberg.org/gruf/go-kv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -64,12 +65,12 @@ import ( // '404': // description: not found func (m *Module) StatusBoostedByGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusBoostedByGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) + l := log.WithFields(kv.Fields{ + + {"request_uri", c.Request.RequestURI}, + {"user_agent", c.Request.UserAgent()}, + {"origin_ip", c.ClientIP()}, + }...) l.Debugf("entering function") authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go index 2c18e4e41..7e2922acf 100644 --- a/internal/api/client/streaming/stream.go +++ b/internal/api/client/streaming/stream.go @@ -5,9 +5,10 @@ import ( "net/http" "time" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -144,12 +145,12 @@ func (m *Module) StreamGETHandler(c *gin.Context) { return } - l := logrus.WithFields(logrus.Fields{ - "account": account.Username, - "path": BasePath, - "streamID": stream.ID, - "streamType": streamType, - }) + l := log.WithFields(kv.Fields{ + {"account", account.Username}, + {"path", BasePath}, + {"streamID", stream.ID}, + {"streamType", streamType}, + }...) wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { diff --git a/internal/api/errorhandling.go b/internal/api/errorhandling.go index 32699fe36..346841f3f 100644 --- a/internal/api/errorhandling.go +++ b/internal/api/errorhandling.go @@ -22,11 +22,12 @@ import ( "context" "net/http" + "codeberg.org/gruf/go-kv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // TODO: add more templated html pages here for different error types @@ -106,11 +107,11 @@ func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet fun // to pass any detailed errors (that might contain sensitive information) into the // errWithCode.Error() field, since the client will see this. Use your noggin! func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) { - l := logrus.WithFields(logrus.Fields{ - "path": c.Request.URL.Path, - "error": errWithCode.Error(), - "help": errWithCode.Safe(), - }) + l := log.WithFields(kv.Fields{ + {"path", c.Request.URL.Path}, + {"error", errWithCode.Error()}, + {"help", errWithCode.Safe()}, + }...) statusCode := errWithCode.Code() diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go index 6d4764ce5..68ea760bd 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -23,11 +23,12 @@ import ( "fmt" "net/http" + "codeberg.org/gruf/go-kv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -55,10 +56,9 @@ import ( // schema: // "$ref": "#/definitions/wellKnownResponse" func (m *Module) WebfingerGETRequest(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "WebfingerGETRequest", - "user-agent": c.Request.UserAgent(), - }) + l := log.WithFields(kv.Fields{ + {K: "user-agent", V: c.Request.UserAgent()}, + }...) resourceQuery, set := c.GetQuery("resource") if !set || resourceQuery == "" { diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go index 1dd6b5f79..1c117cd1b 100644 --- a/internal/api/security/signaturecheck.go +++ b/internal/api/security/signaturecheck.go @@ -4,8 +4,8 @@ import ( "net/http" "net/url" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/gin-gonic/gin" "github.com/go-fed/httpsig" @@ -15,8 +15,6 @@ import ( // that signed the request is permitted to access the server. If it is permitted, the handler will set the key // verifier and the signature in the gin context for use down the line. func (m *Module) SignatureCheck(c *gin.Context) { - l := logrus.WithField("func", "DomainBlockChecker") - // create the verifier from the request // if the request is signed, it will have a signature header verifier, err := httpsig.NewVerifier(c.Request) @@ -32,12 +30,12 @@ func (m *Module) SignatureCheck(c *gin.Context) { // if the domain is blocked we want to bail as early as possible blocked, err := m.db.IsURIBlocked(c.Request.Context(), requestingPublicKeyID) if err != nil { - l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) + log.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) c.AbortWithStatus(http.StatusInternalServerError) return } if blocked { - l.Infof("domain %s is blocked", requestingPublicKeyID.Host) + log.Infof("domain %s is blocked", requestingPublicKeyID.Host) c.AbortWithStatus(http.StatusForbidden) return } diff --git a/internal/api/security/tokencheck.go b/internal/api/security/tokencheck.go index e366af2ea..0e9e7bde2 100644 --- a/internal/api/security/tokencheck.go +++ b/internal/api/security/tokencheck.go @@ -20,9 +20,9 @@ package security import ( "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -32,7 +32,6 @@ import ( // If user or account can't be found, then the handler won't *fail*, in case the server wants to allow // public requests that don't have a Bearer token set (eg., for public instance information and so on). func (m *Module) TokenCheck(c *gin.Context) { - l := logrus.WithField("func", "OauthTokenMiddleware") ctx := c.Request.Context() defer c.Next() @@ -43,38 +42,38 @@ func (m *Module) TokenCheck(c *gin.Context) { ti, err := m.server.ValidationBearerToken(c.Copy().Request) if err != nil { - l.Infof("token was passed in Authorization header but we could not validate it: %s", err) + log.Infof("token was passed in Authorization header but we could not validate it: %s", err) return } c.Set(oauth.SessionAuthorizedToken, ti) // check for user-level token if userID := ti.GetUserID(); userID != "" { - l.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope()) + log.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope()) // fetch user for this token user := >smodel.User{} if err := m.db.GetByID(ctx, userID, user); err != nil { if err != db.ErrNoEntries { - l.Errorf("database error looking for user with id %s: %s", userID, err) + log.Errorf("database error looking for user with id %s: %s", userID, err) return } - l.Warnf("no user found for userID %s", userID) + log.Warnf("no user found for userID %s", userID) return } if user.ConfirmedAt.IsZero() { - l.Warnf("authenticated user %s has never confirmed thier email address", userID) + log.Warnf("authenticated user %s has never confirmed thier email address", userID) return } if !user.Approved { - l.Warnf("authenticated user %s's account was never approved by an admin", userID) + log.Warnf("authenticated user %s's account was never approved by an admin", userID) return } if user.Disabled { - l.Warnf("authenticated user %s's account was disabled'", userID) + log.Warnf("authenticated user %s's account was disabled'", userID) return } @@ -84,15 +83,15 @@ func (m *Module) TokenCheck(c *gin.Context) { acct, err := m.db.GetAccountByID(ctx, user.AccountID) if err != nil { if err != db.ErrNoEntries { - l.Errorf("database error looking for account with id %s: %s", user.AccountID, err) + log.Errorf("database error looking for account with id %s: %s", user.AccountID, err) return } - l.Warnf("no account found for userID %s", userID) + log.Warnf("no account found for userID %s", userID) return } if !acct.SuspendedAt.IsZero() { - l.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID) + log.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID) return } @@ -101,16 +100,16 @@ func (m *Module) TokenCheck(c *gin.Context) { // check for application token if clientID := ti.GetClientID(); clientID != "" { - l.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope()) + log.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope()) // fetch app for this token app := >smodel.Application{} if err := m.db.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil { if err != db.ErrNoEntries { - l.Errorf("database error looking for application with clientID %s: %s", clientID, err) + log.Errorf("database error looking for application with clientID %s: %s", clientID, err) return } - l.Warnf("no app found for client %s", clientID) + log.Warnf("no app found for client %s", clientID) return } c.Set(oauth.SessionAuthorizedApplication, app) diff --git a/internal/concurrency/workers.go b/internal/concurrency/workers.go index 65ce9e3ab..d9669fcb3 100644 --- a/internal/concurrency/workers.go +++ b/internal/concurrency/workers.go @@ -27,7 +27,7 @@ import ( "runtime" "codeberg.org/gruf/go-runners" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // WorkerPool represents a proccessor for MsgType objects, using a worker pool to allocate resources. @@ -63,7 +63,7 @@ func NewWorkerPool[MsgType any](workers int, queueRatio int) *WorkerPool[MsgType } // Log new worker creation with type prefix - logrus.Infof("%s created with workers=%d queue=%d", + log.Infof("%s created with workers=%d queue=%d", w.prefix, workers, workers*queueRatio, @@ -74,7 +74,7 @@ func NewWorkerPool[MsgType any](workers int, queueRatio int) *WorkerPool[MsgType // Start will attempt to start the underlying worker pool, or return error. func (w *WorkerPool[MsgType]) Start() error { - logrus.Infof("%s starting", w.prefix) + log.Infof("%s starting", w.prefix) // Check processor was set if w.process == nil { @@ -91,7 +91,7 @@ func (w *WorkerPool[MsgType]) Start() error { // Stop will attempt to stop the underlying worker pool, or return error. func (w *WorkerPool[MsgType]) Stop() error { - logrus.Infof("%s stopping", w.prefix) + log.Infof("%s stopping", w.prefix) // Attempt to stop pool if !w.workers.Stop() { @@ -104,19 +104,19 @@ func (w *WorkerPool[MsgType]) Stop() error { // SetProcessor will set the Worker's processor function, which is called for each queued message. func (w *WorkerPool[MsgType]) SetProcessor(fn func(context.Context, MsgType) error) { if w.process != nil { - logrus.Panicf("%s Worker.process is already set", w.prefix) + log.Fatalf("%s Worker.process is already set", w.prefix) } w.process = fn } // Queue will queue provided message to be processed with there's a free worker. func (w *WorkerPool[MsgType]) Queue(msg MsgType) { - logrus.Tracef("%s queueing message (workers=%d queue=%d): %+v", + log.Tracef("%s queueing message (workers=%d queue=%d): %+v", w.prefix, w.workers.Workers(), w.workers.Queue(), msg, ) w.workers.Enqueue(func(ctx context.Context) { if err := w.process(ctx, msg); err != nil { - logrus.Errorf("%s %v", w.prefix, err) + log.Errorf("%s %v", w.prefix, err) } }) } diff --git a/internal/config/validate.go b/internal/config/validate.go index b2bdda960..b68cb2c62 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/miekg/dns" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Validate validates global config settings which don't have defaults, to make sure they are set sensibly. @@ -55,7 +55,7 @@ func Validate() error { // no problem break case "http": - logrus.Warnf("%s was set to 'http'; this should *only* be used for debugging and tests!", ProtocolFlag()) + log.Warnf("%s was set to 'http'; this should *only* be used for debugging and tests!", ProtocolFlag()) case "": errs = append(errs, fmt.Errorf("%s must be set", ProtocolFlag())) default: diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 2c97a6646..aaeb646cf 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -25,11 +25,11 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) @@ -287,7 +287,7 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li Where("? != '{}'", bun.Ident("attachments")). Where("? != '[]'", bun.Ident("attachments")) default: - logrus.Panic("db dialect was neither pg nor sqlite") + log.Panic("db dialect was neither pg nor sqlite") return q } }) @@ -383,7 +383,7 @@ func (a *accountDB) statusesFromIDs(ctx context.Context, statusIDs []string) ([] // Fetch from status from database by ID status, err := a.status.GetStatusByID(ctx, id) if err != nil { - logrus.Errorf("statusesFromIDs: error getting status %q: %v", id, err) + log.Errorf("statusesFromIDs: error getting status %q: %v", id, err) continue } diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index 8b9c7c9a3..1bda82d56 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -29,13 +29,12 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" "golang.org/x/crypto/bcrypt" ) @@ -87,7 +86,7 @@ func (a *adminDB) IsEmailAvailable(ctx context.Context, email string) (bool, db. func (a *adminDB) NewSignup(ctx context.Context, username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, db.Error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - logrus.Errorf("error creating new rsa key: %s", err) + log.Errorf("error creating new rsa key: %s", err) return nil, err } @@ -190,13 +189,13 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error { return err } if exists { - logrus.Infof("instance account %s already exists", username) + log.Infof("instance account %s already exists", username) return nil } key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - logrus.Errorf("error creating new rsa key: %s", err) + log.Errorf("error creating new rsa key: %s", err) return err } @@ -231,7 +230,7 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error { return a.conn.ProcessError(err) } - logrus.Infof("instance account %s CREATED with id %s", username, acct.ID) + log.Infof("instance account %s CREATED with id %s", username, acct.ID) return nil } @@ -250,7 +249,7 @@ func (a *adminDB) CreateInstanceInstance(ctx context.Context) db.Error { return err } if exists { - logrus.Infof("instance entry already exists") + log.Infof("instance entry already exists") return nil } @@ -275,6 +274,6 @@ func (a *adminDB) CreateInstanceInstance(ctx context.Context) db.Error { return a.conn.ProcessError(err) } - logrus.Infof("created instance instance %s with id %s", host, i.ID) + log.Infof("created instance instance %s with id %s", host, i.ID) return nil } diff --git a/internal/db/bundb/basic.go b/internal/db/bundb/basic.go index 1e03fab72..b31a73dd7 100644 --- a/internal/db/bundb/basic.go +++ b/internal/db/bundb/basic.go @@ -22,10 +22,9 @@ import ( "context" "errors" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -165,6 +164,6 @@ func (b *basicDB) IsHealthy(ctx context.Context) db.Error { } func (b *basicDB) Stop(ctx context.Context) db.Error { - logrus.Info("closing db connection") + log.Info("closing db connection") return b.conn.Close() } diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index d92318afd..784a7d443 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -33,13 +33,13 @@ import ( "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/stdlib" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/dialect/sqlitedialect" @@ -89,8 +89,6 @@ type bunDBService struct { } func doMigration(ctx context.Context, db *bun.DB) error { - l := logrus.WithField("func", "doMigration") - migrator := migrate.NewMigrator(db, migrations.Migrations) if err := migrator.Init(ctx); err != nil { @@ -106,11 +104,11 @@ func doMigration(ctx context.Context, db *bun.DB) error { } if group.ID == 0 { - l.Info("there are no new migrations to run") + log.Info("there are no new migrations to run") return nil } - l.Infof("MIGRATED DATABASE TO %s", group) + log.Infof("MIGRATED DATABASE TO %s", group) return nil } @@ -243,7 +241,7 @@ func sqliteConn(ctx context.Context) (*DBConn, error) { tweakConnectionValues(sqldb) if dbAddress == "file::memory:?cache=shared" { - logrus.Warn("sqlite in-memory database should only be used for debugging") + log.Warn("sqlite in-memory database should only be used for debugging") // don't close connections on disconnect -- otherwise // the SQLite database will be deleted when there // are no active connections @@ -260,7 +258,7 @@ func sqliteConn(ctx context.Context) (*DBConn, error) { return nil, fmt.Errorf("sqlite ping: %s", err) } - logrus.Info("connected to SQLITE database") + log.Info("connected to SQLITE database") return conn, nil } @@ -281,7 +279,7 @@ func pgConn(ctx context.Context) (*DBConn, error) { return nil, fmt.Errorf("postgres ping: %s", err) } - logrus.Info("connected to POSTGRES database") + log.Info("connected to POSTGRES database") return conn, nil } @@ -436,7 +434,7 @@ func (ps *bunDBService) EmojiStringsToEmojis(ctx context.Context, emojis []strin if err != nil { if err == sql.ErrNoRows { // no result found for this username/domain so just don't include it as an emoji and carry on about our business - logrus.Debugf("no emoji found with shortcode %s, skipping it", e) + log.Debugf("no emoji found with shortcode %s, skipping it", e) continue } // a serious error has happened so bail diff --git a/internal/db/bundb/hook.go b/internal/db/bundb/hook.go index 6f9935272..23f334048 100644 --- a/internal/db/bundb/hook.go +++ b/internal/db/bundb/hook.go @@ -22,7 +22,9 @@ import ( "context" "time" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -38,21 +40,17 @@ func (queryHook) AfterQuery(_ context.Context, event *bun.QueryEvent) { // Get the DB query duration dur := time.Since(event.StartTime) - log := func(lvl logrus.Level, msg string) { - logrus.WithFields(logrus.Fields{ - "duration": dur, - "operation": event.Operation(), - "query": event.Query, - }).Log(lvl, msg) - } - switch { // Warn on slow database queries case dur > time.Second: - log(logrus.WarnLevel, "SLOW DATABASE QUERY") + log.WithFields(kv.Fields{ + {"duration", dur}, + {"query", event.Query}, + }...).Warn("SLOW DATABASE QUERY") - // On trace, we log query information - case logrus.GetLevel() == logrus.TraceLevel: - log(logrus.TraceLevel, "database query") + // On trace, we log query information, + // manually crafting so DB query not escaped. + case log.Level() >= level.TRACE: + log.Printf("level=TRACE duration=%s query=%s", dur, event.Query) } } diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go index c8a9c5776..fb6454e2f 100644 --- a/internal/db/bundb/instance.go +++ b/internal/db/bundb/instance.go @@ -21,11 +21,10 @@ package bundb import ( "context" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -118,7 +117,7 @@ func (i *instanceDB) GetInstancePeers(ctx context.Context, includeSuspended bool } func (i *instanceDB) GetInstanceAccounts(ctx context.Context, domain string, maxID string, limit int) ([]*gtsmodel.Account, db.Error) { - logrus.Debug("GetAccountsForInstance") + log.Debug("GetAccountsForInstance") accounts := []*gtsmodel.Account{} diff --git a/internal/db/bundb/mention.go b/internal/db/bundb/mention.go index 067f0d676..e2c83ef3f 100644 --- a/internal/db/bundb/mention.go +++ b/internal/db/bundb/mention.go @@ -22,9 +22,9 @@ import ( "context" "codeberg.org/gruf/go-cache/v2" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -72,7 +72,7 @@ func (m *mentionDB) GetMentions(ctx context.Context, ids []string) ([]*gtsmodel. // Attempt fetch from DB mention, err := m.GetMention(ctx, id) if err != nil { - logrus.Errorf("GetMentions: error getting mention %q: %v", id, err) + log.Errorf("GetMentions: error getting mention %q: %v", id, err) continue } diff --git a/internal/db/bundb/migrations/20220612091800_duplicated_media_cleanup.go b/internal/db/bundb/migrations/20220612091800_duplicated_media_cleanup.go index 1a3ae07aa..4c4ada594 100644 --- a/internal/db/bundb/migrations/20220612091800_duplicated_media_cleanup.go +++ b/internal/db/bundb/migrations/20220612091800_duplicated_media_cleanup.go @@ -26,14 +26,14 @@ import ( "codeberg.org/gruf/go-store/kv" "codeberg.org/gruf/go-store/storage" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) func init() { - deleteAttachment := func(ctx context.Context, l *logrus.Entry, a *gtsmodel.MediaAttachment, s *kv.KVStore, tx bun.Tx) { + deleteAttachment := func(ctx context.Context, l log.Entry, a *gtsmodel.MediaAttachment, s *kv.KVStore, tx bun.Tx) { if err := s.Delete(a.File.Path); err != nil && err != storage.ErrNotFound { l.Errorf("error removing file %s: %s", a.File.Path, err) } else { @@ -57,7 +57,7 @@ func init() { } up := func(ctx context.Context, db *bun.DB) error { - l := logrus.WithField("migration", "20220612091800_duplicated_media_cleanup") + l := log.WithField("migration", "20220612091800_duplicated_media_cleanup") if config.GetStorageBackend() != "local" { // this migration only affects versions which only supported local storage diff --git a/internal/db/bundb/migrations/main.go b/internal/db/bundb/migrations/main.go index 318f9be3f..af43856fe 100644 --- a/internal/db/bundb/migrations/main.go +++ b/internal/db/bundb/migrations/main.go @@ -22,7 +22,5 @@ import ( "github.com/uptrace/bun/migrate" ) -var ( - // Migrations provides migration logic for bun - Migrations = migrate.NewMigrations() -) +// Migrations provides migration logic for bun +var Migrations = migrate.NewMigrations() diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index f5ea099de..5e825d096 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -22,9 +22,9 @@ import ( "context" "codeberg.org/gruf/go-cache/v2" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) type notificationDB struct { @@ -98,7 +98,7 @@ func (n *notificationDB) GetNotifications(ctx context.Context, accountID string, // Attempt fetch from DB notif, err := n.GetNotification(ctx, id) if err != nil { - logrus.Errorf("GetNotifications: error getting notification %q: %v", id, err) + log.Errorf("GetNotifications: error getting notification %q: %v", id, err) continue } diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 74a24ebaa..378ee1a7a 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -24,10 +24,10 @@ import ( "database/sql" "time" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -208,7 +208,7 @@ func (s *statusDB) GetStatusChildren(ctx context.Context, status *gtsmodel.Statu // only append children, not the overall parent status entry, ok := e.Value.(*gtsmodel.Status) if !ok { - logrus.Panic("GetStatusChildren: found status could not be asserted to *gtsmodel.Status") + log.Panic("GetStatusChildren: found status could not be asserted to *gtsmodel.Status") } if entry.ID != status.ID { @@ -233,7 +233,7 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, if err := q.Scan(ctx, &childIDs); err != nil { if err != sql.ErrNoRows { - logrus.Errorf("statusChildren: error getting children for %q: %v", status.ID, err) + log.Errorf("statusChildren: error getting children for %q: %v", status.ID, err) } return } @@ -242,7 +242,7 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, // Fetch child with ID from database child, err := s.GetStatusByID(ctx, id) if err != nil { - logrus.Errorf("statusChildren: error getting child status %q: %v", id, err) + log.Errorf("statusChildren: error getting child status %q: %v", id, err) continue } @@ -250,7 +250,7 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, for e := foundStatuses.Front(); e != nil; e = e.Next() { entry, ok := e.Value.(*gtsmodel.Status) if !ok { - logrus.Panic("statusChildren: found status could not be asserted to *gtsmodel.Status") + log.Panic("statusChildren: found status could not be asserted to *gtsmodel.Status") } if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index 3c0d6d7e4..d2b3cf07e 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -21,9 +21,9 @@ package bundb import ( "context" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" "golang.org/x/exp/slices" ) @@ -96,7 +96,7 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI // Fetch status from db for ID status, err := t.status.GetStatusByID(ctx, id) if err != nil { - logrus.Errorf("GetHomeTimeline: error fetching status %q: %v", id, err) + log.Errorf("GetHomeTimeline: error fetching status %q: %v", id, err) continue } @@ -156,7 +156,7 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, accountID string, ma // Fetch status from db for ID status, err := t.status.GetStatusByID(ctx, id) if err != nil { - logrus.Errorf("GetPublicTimeline: error fetching status %q: %v", id, err) + log.Errorf("GetPublicTimeline: error fetching status %q: %v", id, err) continue } @@ -216,7 +216,7 @@ func (t *timelineDB) GetFavedTimeline(ctx context.Context, accountID string, max // Fetch status from db for corresponding favourite status, err := t.status.GetStatusByID(ctx, fave.StatusID) if err != nil { - logrus.Errorf("GetFavedTimeline: error fetching status for fave %q: %v", fave.ID, err) + log.Errorf("GetFavedTimeline: error fetching status for fave %q: %v", fave.ID, err) continue } diff --git a/internal/email/confirm.go b/internal/email/confirm.go index b49004056..7cc32f330 100644 --- a/internal/email/confirm.go +++ b/internal/email/confirm.go @@ -22,8 +22,8 @@ import ( "bytes" "net/smtp" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" ) const ( @@ -42,7 +42,7 @@ func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error { if err != nil { return err } - logrus.WithField("func", "SendConfirmEmail").Trace(s.hostAddress + "\n" + config.GetSMTPUsername() + ":password" + "\n" + s.from + "\n" + toAddress + "\n\n" + string(msg) + "\n") + log.Trace(s.hostAddress + "\n" + config.GetSMTPUsername() + ":password" + "\n" + s.from + "\n" + toAddress + "\n\n" + string(msg) + "\n") return smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg) } diff --git a/internal/email/noopsender.go b/internal/email/noopsender.go index 9aab1ca41..4f4aa2014 100644 --- a/internal/email/noopsender.go +++ b/internal/email/noopsender.go @@ -22,8 +22,8 @@ import ( "bytes" "text/template" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // NewNoopSender returns a no-op email sender that will just execute the given sendCallback @@ -61,7 +61,7 @@ func (s *noopSender) SendConfirmEmail(toAddress string, data ConfirmData) error return err } - logrus.Tracef("NOT SENDING confirmation email to %s with contents: %s", toAddress, msg) + log.Tracef("NOT SENDING confirmation email to %s with contents: %s", toAddress, msg) if s.sendCallback != nil { s.sendCallback(toAddress, string(msg)) @@ -81,7 +81,7 @@ func (s *noopSender) SendResetEmail(toAddress string, data ResetData) error { return err } - logrus.Tracef("NOT SENDING reset email to %s with contents: %s", toAddress, msg) + log.Tracef("NOT SENDING reset email to %s with contents: %s", toAddress, msg) if s.sendCallback != nil { s.sendCallback(toAddress, string(msg)) diff --git a/internal/email/util.go b/internal/email/util.go index c543db755..80534d6ae 100644 --- a/internal/email/util.go +++ b/internal/email/util.go @@ -45,7 +45,6 @@ func loadTemplates(templateBaseDir string) (*template.Template, error) { // https://pkg.go.dev/net/smtp#SendMail // and it did seem to work. func assembleMessage(mailSubject string, mailBody string, mailTo string, mailFrom string) ([]byte, error) { - if strings.Contains(mailSubject, "\r") || strings.Contains(mailSubject, "\n") { return nil, errors.New("email subject must not contain newline characters") } diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go index 5ca2f9806..cb693892e 100644 --- a/internal/federation/authenticate.go +++ b/internal/federation/authenticate.go @@ -28,8 +28,6 @@ import ( "net/url" "strings" - "github.com/sirupsen/logrus" - "github.com/go-fed/httpsig" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" @@ -39,6 +37,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) /* @@ -116,8 +115,6 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca // Also note that this function *does not* dereference the remote account that the signature key is associated with. // Other functions should use the returned URL to dereference the remote account, if required. func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*url.URL, gtserror.WithCode) { - l := logrus.WithField("func", "AuthenticateFederatedRequest") - var publicKey interface{} var pkOwnerURI *url.URL var err error @@ -127,7 +124,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU if vi == nil { err := errors.New("http request wasn't signed or http signature was invalid") errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -135,7 +132,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU if !ok { err := errors.New("http request wasn't signed or http signature was invalid") errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -144,7 +141,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU if si == nil { err := errors.New("http request wasn't signed or http signature was invalid") errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -152,7 +149,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU if !ok { err := errors.New("http request wasn't signed or http signature was invalid") errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -160,7 +157,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU requestingPublicKeyID, err := url.Parse(verifier.KeyId()) if err != nil { errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key URL %s", verifier.KeyId())) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -170,39 +167,39 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU if host := config.GetHost(); strings.EqualFold(requestingHost, host) { // LOCAL ACCOUNT REQUEST // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing - l.Tracef("proceeding without dereference for local public key %s", requestingPublicKeyID) + log.Tracef("proceeding without dereference for local public key %s", requestingPublicKeyID) if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil { errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("couldn't get account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } publicKey = requestingLocalAccount.PublicKey pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) if err != nil { errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingLocalAccount.URI)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } } else if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil { // REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY // this is a remote account and we already have the public key for it so use that - l.Tracef("proceeding without dereference for cached public key %s", requestingPublicKeyID) + log.Tracef("proceeding without dereference for cached public key %s", requestingPublicKeyID) publicKey = requestingRemoteAccount.PublicKey pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI) if err != nil { errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingRemoteAccount.URI)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } } else { // REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY // the request is remote and we don't have the public key yet, // so we need to authenticate the request properly by dereferencing the remote key - l.Tracef("proceeding with dereference for uncached public key %s", requestingPublicKeyID) + log.Tracef("proceeding with dereference for uncached public key %s", requestingPublicKeyID) transport, err := f.transportController.NewTransportForUsername(ctx, requestedUsername) if err != nil { errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("error creating transport for %s: %s", requestedUsername, err)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -210,7 +207,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU b, err := transport.Dereference(ctx, requestingPublicKeyID) if err != nil { errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -218,7 +215,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID) if err != nil { errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -226,7 +223,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value")) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -235,14 +232,14 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { errWithCode := gtserror.NewErrorUnauthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) if err != nil { errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -250,7 +247,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value")) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } pkOwnerURI = pkOwnerProp.GetIRI() @@ -259,7 +256,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // after all that, public key should be defined if publicKey == nil { errWithCode := gtserror.NewErrorInternalError(errors.New("returned public key was empty")) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } @@ -271,16 +268,16 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU } for _, algo := range algos { - l.Tracef("trying algo: %s", algo) + log.Tracef("trying algo: %s", algo) err := verifier.Verify(publicKey, algo) if err == nil { - l.Tracef("authentication for %s PASSED with algorithm %s", pkOwnerURI, algo) + log.Tracef("authentication for %s PASSED with algorithm %s", pkOwnerURI, algo) return pkOwnerURI, nil } - l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) + log.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) } errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature)) - l.Debug(errWithCode) + log.Debug(errWithCode) return nil, errWithCode } diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index c479906d7..a0e2b87ae 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -29,13 +29,13 @@ import ( "sync" "time" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/transport" ) @@ -79,7 +79,6 @@ type GetRemoteAccountParams struct { // GetRemoteAccount completely dereferences a remote account, converts it to a GtS model account, // puts or updates it in the database (if necessary), and returns it to a caller. func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountParams) (remoteAccount *gtsmodel.Account, err error) { - /* In this function we want to retrieve a gtsmodel representation of a remote account, with its proper accountDomain set, while making as few calls to remote instances as possible to save time and bandwidth. @@ -454,7 +453,7 @@ func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsm go func() { dlCtx, done := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute)) if err := lockAndLoad(dlCtx, d.dereferencingAvatarsLock, processingMedia, d.dereferencingAvatars, targetAccount.ID); err != nil { - logrus.Errorf("fetchRemoteAccountMedia: error during async lock and load of avatar: %s", err) + log.Errorf("fetchRemoteAccountMedia: error during async lock and load of avatar: %s", err) } done() }() @@ -512,7 +511,7 @@ func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsm go func() { dlCtx, done := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute)) if err := lockAndLoad(dlCtx, d.dereferencingHeadersLock, processingMedia, d.dereferencingHeaders, targetAccount.ID); err != nil { - logrus.Errorf("fetchRemoteAccountMedia: error during async lock and load of header: %s", err) + log.Errorf("fetchRemoteAccountMedia: error during async lock and load of header: %s", err) } done() }() diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index 8cb110f24..e6e03646c 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -26,12 +26,13 @@ import ( "net/url" "strings" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" ) @@ -224,10 +225,10 @@ func (d *deref) dereferenceStatusable(ctx context.Context, username string, remo // and attach them to the status. The status itself will not be added to the database yet, // that's up the caller to do. func (d *deref) populateStatusFields(ctx context.Context, status *gtsmodel.Status, requestingUsername string, includeParent bool) error { - l := logrus.WithFields(logrus.Fields{ - "func": "dereferenceStatusFields", - "status": fmt.Sprintf("%+v", status), - }) + l := log.WithFields(kv.Fields{ + + {"status", status}, + }...) l.Debug("entering function") statusIRI, err := url.Parse(status.URI) @@ -288,20 +289,20 @@ func (d *deref) populateStatusMentions(ctx context.Context, status *gtsmodel.Sta for _, m := range status.Mentions { if m.ID != "" { // we've already populated this mention, since it has an ID - logrus.Debug("populateStatusMentions: mention already populated") + log.Debug("populateStatusMentions: mention already populated") mentionIDs = append(mentionIDs, m.ID) newMentions = append(newMentions, m) continue } if m.TargetAccountURI == "" { - logrus.Debug("populateStatusMentions: target URI not set on mention") + log.Debug("populateStatusMentions: target URI not set on mention") continue } targetAccountURI, err := url.Parse(m.TargetAccountURI) if err != nil { - logrus.Debugf("populateStatusMentions: error parsing mentioned account uri %s: %s", m.TargetAccountURI, err) + log.Debugf("populateStatusMentions: error parsing mentioned account uri %s: %s", m.TargetAccountURI, err) continue } @@ -312,7 +313,7 @@ func (d *deref) populateStatusMentions(ctx context.Context, status *gtsmodel.Sta if a, err := d.db.GetAccountByURI(ctx, targetAccountURI.String()); err != nil { errs = append(errs, err.Error()) } else { - logrus.Debugf("populateStatusMentions: got target account %s with id %s through GetAccountByURI", targetAccountURI, a.ID) + log.Debugf("populateStatusMentions: got target account %s with id %s through GetAccountByURI", targetAccountURI, a.ID) targetAccount = a } @@ -325,13 +326,13 @@ func (d *deref) populateStatusMentions(ctx context.Context, status *gtsmodel.Sta }); err != nil { errs = append(errs, err.Error()) } else { - logrus.Debugf("populateStatusMentions: got target account %s with id %s through GetRemoteAccount", targetAccountURI, a.ID) + log.Debugf("populateStatusMentions: got target account %s with id %s through GetRemoteAccount", targetAccountURI, a.ID) targetAccount = a } } if targetAccount == nil { - logrus.Debugf("populateStatusMentions: couldn't get target account %s: %s", m.TargetAccountURI, strings.Join(errs, " : ")) + log.Debugf("populateStatusMentions: couldn't get target account %s: %s", m.TargetAccountURI, strings.Join(errs, " : ")) continue } @@ -392,13 +393,13 @@ func (d *deref) populateStatusAttachments(ctx context.Context, status *gtsmodel. Blurhash: &a.Blurhash, }) if err != nil { - logrus.Errorf("populateStatusAttachments: couldn't get remote media %s: %s", a.RemoteURL, err) + log.Errorf("populateStatusAttachments: couldn't get remote media %s: %s", a.RemoteURL, err) continue } attachment, err := processingMedia.LoadAttachment(ctx) if err != nil { - logrus.Errorf("populateStatusAttachments: couldn't load remote attachment %s: %s", a.RemoteURL, err) + log.Errorf("populateStatusAttachments: couldn't load remote attachment %s: %s", a.RemoteURL, err) continue } diff --git a/internal/federation/dereferencing/thread.go b/internal/federation/dereferencing/thread.go index a299c1220..7d7431110 100644 --- a/internal/federation/dereferencing/thread.go +++ b/internal/federation/dereferencing/thread.go @@ -23,9 +23,10 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -36,11 +37,11 @@ import ( // presented by remote instances as part of their replies collections, and will likely involve making several calls to // multiple different hosts. func (d *deref) DereferenceThread(ctx context.Context, username string, statusIRI *url.URL) error { - l := logrus.WithFields(logrus.Fields{ - "func": "DereferenceThread", - "username": username, - "statusIRI": statusIRI.String(), - }) + l := log.WithFields(kv.Fields{ + + {"username", username}, + {"statusIRI", statusIRI}, + }...) l.Trace("entering DereferenceThread") // if it's our status we already have everything stashed so we can bail early @@ -70,11 +71,11 @@ func (d *deref) DereferenceThread(ctx context.Context, username string, statusIR // iterateAncestors has the goal of reaching the oldest ancestor of a given status, and stashing all statuses along the way. func (d *deref) iterateAncestors(ctx context.Context, username string, statusIRI url.URL) error { - l := logrus.WithFields(logrus.Fields{ - "func": "iterateAncestors", - "username": username, - "statusIRI": statusIRI.String(), - }) + l := log.WithFields(kv.Fields{ + + {"username", username}, + {"statusIRI", statusIRI}, + }...) l.Trace("entering iterateAncestors") // if it's our status we don't need to dereference anything so we can immediately move up the chain @@ -123,11 +124,11 @@ func (d *deref) iterateAncestors(ctx context.Context, username string, statusIRI } func (d *deref) iterateDescendants(ctx context.Context, username string, statusIRI url.URL, statusable ap.Statusable) error { - l := logrus.WithFields(logrus.Fields{ - "func": "iterateDescendants", - "username": username, - "statusIRI": statusIRI.String(), - }) + l := log.WithFields(kv.Fields{ + + {"username", username}, + {"statusIRI", statusIRI}, + }...) l.Trace("entering iterateDescendants") // if it's our status we already have descendants stashed so we can bail early diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go index 1d6f4b937..b80e9ff23 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -23,9 +23,9 @@ import ( "net/http" "net/url" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // federatingActor implements the go-fed federating protocol interface @@ -56,7 +56,7 @@ func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub // method will guaranteed work for non-custom Actors. For custom actors, // care should be used to not call this method if only C2S is supported. func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) { - logrus.Infof("federating actor: send activity %s via outbox %s", t.GetTypeName(), outbox) + log.Infof("federating actor: send activity %s via outbox %s", t.GetTypeName(), outbox) return f.actor.Send(c, outbox, t) } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index f22db38a5..977cf2bba 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -23,28 +23,23 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/uris" ) func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Accept", - }, - ) - - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(accept) if err != nil { return err } - l = l.WithField("accept", i) + l := log.WithField("accept", i) l.Debug("entering Accept") } diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index b70fa1913..47e873ca3 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -22,25 +22,20 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Announce", - }, - ) - - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(announce) if err != nil { return err } - l = l.WithField("announce", i) + l := log.WithField("announce", i) l.Debug("entering Announce") } diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 625d75603..a6e55f2ad 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -24,12 +24,14 @@ import ( "fmt" "strings" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -46,18 +48,12 @@ import ( // Under certain conditions and network activities, Create may be called // multiple times for the same ActivityStreams object. func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Create", - }, - ) - - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(asType) if err != nil { return err } - l = l.WithField("create", i) + l := log.WithField("create", i) l.Debug("entering Create") } @@ -169,11 +165,10 @@ func (f *federatingDB) activityCreate(ctx context.Context, asType vocab.Type, re // createNote handles a Create activity with a Note type. func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStreamsNote, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error { - l := logrus.WithFields(logrus.Fields{ - "func": "createNote", - "receivingAccount": receivingAccount.URI, - "requestingAccount": requestingAccount.URI, - }) + l := log.WithFields(kv.Fields{ + {"receivingAccount", receivingAccount.URI}, + {"requestingAccount", requestingAccount.URI}, + }...) // Check if we have a forward. // In other words, was the note posted to our inbox by at least one actor who actually created the note, or are they just forwarding it? diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go index bd0184f76..8c3457fae 100644 --- a/internal/federation/federatingdb/delete.go +++ b/internal/federation/federatingdb/delete.go @@ -23,9 +23,10 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -36,12 +37,10 @@ import ( // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Delete", - "id": id, - }, - ) + l := log.WithFields(kv.Fields{ + + {"id", id}, + }...) l.Debug("entering Delete") receivingAccount, _ := extractFromCtx(ctx) diff --git a/internal/federation/federatingdb/exists.go b/internal/federation/federatingdb/exists.go index fe220fc71..65b2a55b3 100644 --- a/internal/federation/federatingdb/exists.go +++ b/internal/federation/federatingdb/exists.go @@ -22,7 +22,8 @@ import ( "context" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Exists returns true if the database has an entry for the specified @@ -32,12 +33,9 @@ import ( // // Implementation note: this just straight up isn't implemented, and doesn't *really* need to be either. func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "Exists", - "id": id, - }, - ) + l := log.WithFields(kv.Fields{ + {"id", id}, + }...) l.Debug("entering Exists") return false, nil } diff --git a/internal/federation/federatingdb/followers.go b/internal/federation/federatingdb/followers.go index 96b824c66..9a80d5f0b 100644 --- a/internal/federation/federatingdb/followers.go +++ b/internal/federation/federatingdb/followers.go @@ -5,9 +5,10 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Followers obtains the Followers Collection for an actor with the @@ -17,12 +18,9 @@ import ( // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "Followers", - "id": actorIRI, - }, - ) + l := log.WithFields(kv.Fields{ + {"id", actorIRI}, + }...) l.Debug("entering Followers") acct, err := f.getAccountForIRI(ctx, actorIRI) diff --git a/internal/federation/federatingdb/following.go b/internal/federation/federatingdb/following.go index 62b148d5b..a244cf7ac 100644 --- a/internal/federation/federatingdb/following.go +++ b/internal/federation/federatingdb/following.go @@ -23,9 +23,10 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Following obtains the Following Collection for an actor with the @@ -35,12 +36,9 @@ import ( // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "Following", - "id": actorIRI, - }, - ) + l := log.WithFields(kv.Fields{ + {"id", actorIRI}, + }...) l.Debug("entering Following") acct, err := f.getAccountForIRI(ctx, actorIRI) diff --git a/internal/federation/federatingdb/get.go b/internal/federation/federatingdb/get.go index 9fdc08dd9..5467c886d 100644 --- a/internal/federation/federatingdb/get.go +++ b/internal/federation/federatingdb/get.go @@ -23,8 +23,9 @@ import ( "errors" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -32,12 +33,9 @@ import ( // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Get(ctx context.Context, id *url.URL) (value vocab.Type, err error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "Get", - "id": id, - }, - ) + l := log.WithFields(kv.Fields{ + {"id", id}, + }...) l.Debug("entering Get") if uris.IsUserPath(id) { diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go index aaa58348f..51d11f018 100644 --- a/internal/federation/federatingdb/owns.go +++ b/internal/federation/federatingdb/owns.go @@ -23,10 +23,11 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -34,12 +35,9 @@ import ( // the database has an entry for the IRI. // The library makes this call only after acquiring a lock first. func (f *federatingDB) Owns(ctx context.Context, id *url.URL) (bool, error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "Owns", - "id": id, - }, - ) + l := log.WithFields(kv.Fields{ + {"id", id}, + }...) l.Debug("entering Owns") // if the id host isn't this instance host, we don't own this IRI diff --git a/internal/federation/federatingdb/reject.go b/internal/federation/federatingdb/reject.go index 9cb81c267..b77c3ce3a 100644 --- a/internal/federation/federatingdb/reject.go +++ b/internal/federation/federatingdb/reject.go @@ -23,27 +23,22 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Reject", - }, - ) - - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(reject) if err != nil { return err } - l = l.WithField("reject", i) + l := log.WithField("reject", i) l.Debug("entering Reject") } diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index 92f24f315..4cb3d0fa8 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -23,21 +23,18 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Undo", - }, - ) + l := log.Entry{} - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(undo) if err != nil { return err diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go index 09d5c8fd8..599544e34 100644 --- a/internal/federation/federatingdb/update.go +++ b/internal/federation/federatingdb/update.go @@ -23,11 +23,12 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -41,13 +42,9 @@ import ( // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { - l := logrus.WithFields( - logrus.Fields{ - "func": "Update", - }, - ) + l := log.Entry{} - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(asType) if err != nil { return err diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index 7f27cc759..9fc35faa4 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -25,7 +25,7 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" @@ -33,6 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -63,18 +64,12 @@ func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor voc // The go-fed library will handle setting the 'id' property on the // activity or object provided with the value returned. func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL, err error) { - l := logrus.WithFields( - logrus.Fields{ - "func": "NewID", - }, - ) - - if logrus.GetLevel() >= logrus.DebugLevel { + if log.Level() >= level.DEBUG { i, err := marshalItem(t) if err != nil { return nil, err } - l = l.WithField("newID", i) + l := log.WithField("newID", i) l.Debug("entering NewID") } @@ -312,7 +307,7 @@ func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *g var ok bool receivingAccount, ok = receivingAccountI.(*gtsmodel.Account) if !ok { - logrus.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", ap.ContextReceivingAccount) + log.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", ap.ContextReceivingAccount) } } @@ -321,7 +316,7 @@ func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *g var ok bool requestingAccount, ok = requestingAcctI.(*gtsmodel.Account) if !ok { - logrus.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", ap.ContextRequestingAccount) + log.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", ap.ContextRequestingAccount) } } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 8944987c5..8242ca4b1 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -25,7 +25,7 @@ import ( "net/http" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" @@ -33,6 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -137,11 +138,10 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques // authenticated must be true and error nil. The request will continue // to be processed. func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "AuthenticatePostInbox", - "useragent": r.UserAgent(), - "url": r.URL.String(), - }) + l := log.WithFields(kv.Fields{ + {"useragent", r.UserAgent()}, + {"url", r.URL.String()}, + }...) l.Trace("received request to authenticate") if !uris.IsInboxPath(r.URL) { @@ -226,10 +226,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr // blocked must be false and error nil. The request will continue // to be processed. func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "Blocked", - }) - l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs) + log.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs) // check domain blocks first for the given actor IRIs blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs) @@ -244,7 +241,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er otherInvolvedIRIsI := ctx.Value(ap.ContextOtherInvolvedIRIs) otherInvolvedIRIs, ok := otherInvolvedIRIsI.([]*url.URL) if !ok { - l.Errorf("other involved IRIs not set on request context") + log.Error("other involved IRIs not set on request context") return false, errors.New("other involved IRIs not set on request context, so couldn't determine blocks") } blocked, err = f.db.AreURIsBlocked(ctx, otherInvolvedIRIs) @@ -259,13 +256,13 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er receivingAccountI := ctx.Value(ap.ContextReceivingAccount) receivingAccount, ok := receivingAccountI.(*gtsmodel.Account) if !ok { - l.Errorf("receiving account not set on request context") + log.Error("receiving account not set on request context") return false, errors.New("receiving account not set on request context, so couldn't determine blocks") } requestingAccountI := ctx.Value(ap.ContextRequestingAccount) requestingAccount, ok := requestingAccountI.(*gtsmodel.Account) if !ok { - l.Errorf("requesting account not set on request context") + log.Error("requesting account not set on request context") return false, errors.New("requesting account not set on request context, so couldn't determine blocks") } // the receiver shouldn't block the sender @@ -371,10 +368,9 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa // type and extension, so the unhandled ones are passed to // DefaultCallback. func (f *federator) DefaultCallback(ctx context.Context, activity pub.Activity) error { - l := logrus.WithFields(logrus.Fields{ - "func": "DefaultCallback", - "aptype": activity.GetTypeName(), - }) + l := log.WithFields(kv.Fields{ + {"aptype", activity.GetTypeName()}, + }...) l.Debugf("received unhandle-able activity type so ignoring it") return nil } diff --git a/internal/log/caller.go b/internal/log/caller.go new file mode 100644 index 000000000..f7062f06a --- /dev/null +++ b/internal/log/caller.go @@ -0,0 +1,89 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package log + +import ( + "runtime" + "strings" + "sync" +) + +var ( + // fnCache is a cache of PCs to their calculated function names. + fnCache = map[uintptr]string{} + + // strCache is a cache of strings to the originally allocated version + // of that string contents. so we don't have hundreds of the same instances + // of string floating around in memory. + strCache = map[string]string{} + + // cacheMu protects fnCache and strCache. + cacheMu sync.Mutex +) + +// Caller fetches the calling function name, skipping 'depth'. Results are cached per PC. +func Caller(depth int) string { + var rpc [1]uintptr + + // Fetch pcs of callers + n := runtime.Callers(depth, rpc[:]) + + if n > 0 { + // Look for value in cache + cacheMu.Lock() + fn, ok := fnCache[rpc[0]] + cacheMu.Unlock() + + if ok { + return fn + } + + // Fetch frame info for caller pc + frame, _ := runtime.CallersFrames(rpc[:]).Next() + + if frame.PC != 0 { + name := frame.Function + + // Drop all but the package name and function name, no mod path + if idx := strings.LastIndex(name, "/"); idx >= 0 { + name = name[idx+1:] + } + + // Drop any generic type parameter markers + if idx := strings.Index(name, "[...]"); idx >= 0 { + name = name[:idx] + name[idx+5:] + } + + // Cache this func name + cacheMu.Lock() + fn, ok := strCache[name] + if !ok { + // Cache ptr to this allocated str + strCache[name] = name + fn = name + } + fnCache[rpc[0]] = fn + cacheMu.Unlock() + + return fn + } + } + + return "???" +} diff --git a/internal/log/caller_test.go b/internal/log/caller_test.go new file mode 100644 index 000000000..59bf342d1 --- /dev/null +++ b/internal/log/caller_test.go @@ -0,0 +1,54 @@ +package log_test + +import ( + "runtime" + "strings" + "testing" + + "codeberg.org/gruf/go-atomics" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +// noopt exists to prevent certain optimisations during benching. +var noopt = atomics.NewString() + +func BenchmarkCaller(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + name := log.Caller(2) + noopt.Store(name) + } + }) +} + +func BenchmarkCallerNoCache(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var rpc [1]uintptr + + // Fetch pcs of callers + n := runtime.Callers(2, rpc[:]) + + if n > 0 { + // Fetch frame info for caller pc + frame, _ := runtime.CallersFrames(rpc[:]).Next() + + if frame.PC != 0 { + name := frame.Function + + // Drop all but the package name and function name, no mod path + if idx := strings.LastIndex(name, "/"); idx >= 0 { + name = name[idx+1:] + } + + // Drop any generic type parameter markers + if idx := strings.Index(name, "[...]"); idx >= 0 { + name = name[:idx] + name[idx+5:] + } + + noopt.Store(name) + } + } + } + }) +} diff --git a/internal/log/entry.go b/internal/log/entry.go new file mode 100644 index 000000000..2e6f62e96 --- /dev/null +++ b/internal/log/entry.go @@ -0,0 +1,117 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package log + +import ( + "fmt" + "syscall" + + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" +) + +type Entry struct { + fields []kv.Field +} + +func (e Entry) WithField(key string, value interface{}) Entry { + e.fields = append(e.fields, kv.Field{K: key, V: value}) + return e +} + +func (e Entry) WithFields(fields ...kv.Field) Entry { + e.fields = append(e.fields, fields...) + return e +} + +func (e Entry) Trace(a ...interface{}) { + logf(level.TRACE, e.fields, args(len(a)), a...) +} + +func (e Entry) Tracef(s string, a ...interface{}) { + logf(level.TRACE, e.fields, s, a...) +} + +func (e Entry) Debug(a ...interface{}) { + logf(level.DEBUG, e.fields, args(len(a)), a...) +} + +func (e Entry) Debugf(s string, a ...interface{}) { + logf(level.DEBUG, e.fields, s, a...) +} + +func (e Entry) Info(a ...interface{}) { + logf(level.INFO, e.fields, args(len(a)), a...) +} + +func (e Entry) Infof(s string, a ...interface{}) { + logf(level.INFO, e.fields, s, a...) +} + +func (e Entry) Warn(a ...interface{}) { + logf(level.WARN, e.fields, args(len(a)), a...) +} + +func (e Entry) Warnf(s string, a ...interface{}) { + logf(level.WARN, e.fields, s, a...) +} + +func (e Entry) Error(a ...interface{}) { + logf(level.ERROR, e.fields, args(len(a)), a...) +} + +func (e Entry) Errorf(s string, a ...interface{}) { + logf(level.ERROR, e.fields, s, a...) +} + +func (e Entry) Fatal(a ...interface{}) { + defer syscall.Exit(1) + logf(level.FATAL, e.fields, args(len(a)), a...) +} + +func (e Entry) Fatalf(s string, a ...interface{}) { + defer syscall.Exit(1) + logf(level.FATAL, e.fields, s, a...) +} + +func (e Entry) Panic(a ...interface{}) { + defer panic(fmt.Sprint(a...)) + logf(level.PANIC, e.fields, args(len(a)), a...) +} + +func (e Entry) Panicf(s string, a ...interface{}) { + defer panic(fmt.Sprintf(s, a...)) + logf(level.PANIC, e.fields, s, a...) +} + +func (e Entry) Log(lvl level.LEVEL, a ...interface{}) { + logf(lvl, e.fields, args(len(a)), a...) +} + +func (e Entry) Logf(lvl level.LEVEL, s string, a ...interface{}) { + logf(lvl, e.fields, s, a...) +} + +func (e Entry) Print(a ...interface{}) { + printf(e.fields, args(len(a)), a...) +} + +func (e Entry) Printf(s string, a ...interface{}) { + printf(e.fields, s, a...) +} diff --git a/internal/log/init.go b/internal/log/init.go new file mode 100644 index 000000000..9ec790ba2 --- /dev/null +++ b/internal/log/init.go @@ -0,0 +1,62 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package log + +import ( + "fmt" + "log/syslog" + "strings" + + "codeberg.org/gruf/go-logger/v2/level" +) + +// ParseLevel will parse the log level from given string and set to appropriate level. +func ParseLevel(str string) error { + switch strings.ToLower(str) { + case "trace": + SetLevel(level.TRACE) + case "debug": + SetLevel(level.DEBUG) + case "", "info": + SetLevel(level.INFO) + case "warn": + SetLevel(level.WARN) + case "error": + SetLevel(level.ERROR) + case "fatal": + SetLevel(level.FATAL) + default: + return fmt.Errorf("unknown log level: %q", str) + } + return nil +} + +// EnableSyslog will enabling logging to the syslog at given address. +func EnableSyslog(proto, addr string) error { + // Dial a connection to the syslog daemon + writer, err := syslog.Dial(proto, addr, 0, "gotosocial") + if err != nil { + return err + } + + // Set the syslog writer + sysout = writer + + return nil +} diff --git a/internal/log/log.go b/internal/log/log.go index 67dd03606..a9c7340ed 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -19,95 +19,259 @@ package log import ( - "bytes" + "fmt" "io" "log/syslog" "os" + "strings" + "syscall" + "time" - "github.com/sirupsen/logrus" - lSyslog "github.com/sirupsen/logrus/hooks/syslog" - "github.com/superseriousbusiness/gotosocial/internal/config" + "codeberg.org/gruf/go-atomics" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" ) -// Initialize initializes the global Logrus logger, reading the desired -// log level from the viper store, or using a default if the level -// has not been set in viper. -// -// It also sets the output to log.SplitErrOutputs(...) -// so you get error logs on stderr and normal logs on stdout. -// -// If syslog settings are also in viper, then Syslog will be initialized as well. -func Initialize() error { - out := SplitErrOutputs(os.Stdout, os.Stderr) - logrus.SetOutput(out) +var ( + // loglvl is the currently set logging level. + loglvl atomics.Uint32 - // check if a desired log level has been set - if lvl := config.GetLogLevel(); lvl != "" { - level, err := logrus.ParseLevel(lvl) - if err != nil { - return err - } - logrus.SetLevel(level) + // lvlstrs is the lookup table of log levels to strings. + lvlstrs = level.Default() - if level == logrus.TraceLevel { - logrus.SetReportCaller(true) - } - } + // Preprepared stdout/stderr log writers. + stdout = &safewriter{w: os.Stdout} + stderr = &safewriter{w: os.Stderr} - // set our custom formatter options - logrus.SetFormatter(&logrus.TextFormatter{ - DisableColors: true, - FullTimestamp: true, + // Syslog output, only set if enabled. + sysout *syslog.Writer +) - // By default, quoting is enabled to help differentiate key-value - // fields in log lines. But when debug (or higher, e.g. trace) logging - // is enabled, we disable this. This allows easier copy-pasting of - // entry fields without worrying about escaped quotes. - DisableQuote: logrus.GetLevel() >= logrus.DebugLevel, - }) - - // check if syslog has been enabled, and configure it if so - if config.GetSyslogEnabled() { - protocol := config.GetSyslogProtocol() - address := config.GetSyslogAddress() - - hook, err := lSyslog.NewSyslogHook(protocol, address, syslog.LOG_INFO, "") - if err != nil { - return err - } - - logrus.AddHook(&trimHook{hook}) - } - - return nil +// Level returns the currently set log level. +func Level() level.LEVEL { + return level.LEVEL(loglvl.Load()) } -// SplitErrOutputs returns an OutputSplitFunc that splits output to either one of -// two given outputs depending on whether the level is "error","fatal","panic". -func SplitErrOutputs(out, err io.Writer) OutputSplitFunc { - return func(lvl []byte) io.Writer { - switch string(lvl) /* convert to str for compare is no-alloc */ { - case "error", "fatal", "panic": - return err - default: - return out - } +// SetLevel sets the max logging level. +func SetLevel(lvl level.LEVEL) { + loglvl.Store(uint32(lvl)) +} + +func WithField(key string, value interface{}) Entry { + return Entry{fields: []kv.Field{{K: key, V: value}}} +} + +func WithFields(fields ...kv.Field) Entry { + return Entry{fields: fields} +} + +func Trace(a ...interface{}) { + logf(level.TRACE, nil, args(len(a)), a...) +} + +func Tracef(s string, a ...interface{}) { + logf(level.TRACE, nil, s, a...) +} + +func Debug(a ...interface{}) { + logf(level.DEBUG, nil, args(len(a)), a...) +} + +func Debugf(s string, a ...interface{}) { + logf(level.DEBUG, nil, s, a...) +} + +func Info(a ...interface{}) { + logf(level.INFO, nil, args(len(a)), a...) +} + +func Infof(s string, a ...interface{}) { + logf(level.INFO, nil, s, a...) +} + +func Warn(a ...interface{}) { + logf(level.WARN, nil, args(len(a)), a...) +} + +func Warnf(s string, a ...interface{}) { + logf(level.WARN, nil, s, a...) +} + +func Error(a ...interface{}) { + logf(level.ERROR, nil, args(len(a)), a...) +} + +func Errorf(s string, a ...interface{}) { + logf(level.ERROR, nil, s, a...) +} + +func Fatal(a ...interface{}) { + defer syscall.Exit(1) + logf(level.FATAL, nil, args(len(a)), a...) +} + +func Fatalf(s string, a ...interface{}) { + defer syscall.Exit(1) + logf(level.FATAL, nil, s, a...) +} + +func Panic(a ...interface{}) { + defer panic(fmt.Sprint(a...)) + logf(level.PANIC, nil, args(len(a)), a...) +} + +func Panicf(s string, a ...interface{}) { + defer panic(fmt.Sprintf(s, a...)) + logf(level.PANIC, nil, s, a...) +} + +// Log will log formatted args as 'msg' field to the log at given level. +func Log(lvl level.LEVEL, a ...interface{}) { + logf(lvl, nil, args(len(a)), a...) +} + +// Logf will log format string as 'msg' field to the log at given level. +func Logf(lvl level.LEVEL, s string, a ...interface{}) { + logf(lvl, nil, s, a...) +} + +// Print will log formatted args to the stdout log output. +func Print(a ...interface{}) { + printf(nil, args(len(a)), a...) +} + +// Print will log format string to the stdout log output. +func Printf(s string, a ...interface{}) { + printf(nil, s, a...) +} + +func printf(fields []kv.Field, s string, a ...interface{}) { + // Acquire buffer + buf := getBuf() + + // Append formatted timestamp + now := time.Now().Format("02/01/2006 15:04:05.000") + buf.B = append(buf.B, `timestamp="`...) + buf.B = append(buf.B, now...) + buf.B = append(buf.B, `" `...) + + // Append formatted caller func + buf.B = append(buf.B, `func=`...) + buf.B = append(buf.B, Caller(4)...) + buf.B = append(buf.B, ' ') + + if len(fields) > 0 { + // Append formatted fields + kv.Fields(fields).AppendFormat(buf, false) + buf.B = append(buf.B, ' ') + } + + // Append formatted args + fmt.Fprintf(buf, s, a...) + + // Append a final newline + buf.B = append(buf.B, '\n') + + // Write to log and release + _, _ = stdout.Write(buf.B) + putBuf(buf) +} + +func logf(lvl level.LEVEL, fields []kv.Field, s string, a ...interface{}) { + var out io.Writer + + // Check if enabled. + if lvl > Level() { + return + } + + // Split errors to stderr, + // all else goes to stdout. + if lvl <= level.ERROR { + out = stderr + } else { + out = stdout + } + + // Acquire buffer + buf := getBuf() + + // Append formatted timestamp + now := time.Now().Format("02/01/2006 15:04:05.000") + buf.B = append(buf.B, `timestamp="`...) + buf.B = append(buf.B, now...) + buf.B = append(buf.B, `" `...) + + // Append formatted caller func + buf.B = append(buf.B, `func=`...) + buf.B = append(buf.B, Caller(4)...) + buf.B = append(buf.B, ' ') + + // Append formatted level string + buf.B = append(buf.B, `level=`...) + buf.B = append(buf.B, lvlstrs[lvl]...) + buf.B = append(buf.B, ' ') + + // Append formatted fields with msg + kv.Fields(append(fields, kv.Field{ + "msg", fmt.Sprintf(s, a...), + })).AppendFormat(buf, false) + + // Append a final newline + buf.B = append(buf.B, '\n') + + if sysout != nil { + // Write log entry to syslog + logsys(lvl, buf.String()) + } + + // Write to log and release + _, _ = out.Write(buf.B) + putBuf(buf) +} + +// logsys will log given msg at given severity to the syslog. +func logsys(lvl level.LEVEL, msg string) { + // Truncate message if > 1700 chars + if len(msg) > 1700 { + msg = msg[:1697] + "..." + } + + // Log at appropriate syslog severity + switch lvl { + case level.TRACE: + case level.DEBUG: + case level.INFO: + _ = sysout.Info(msg) + case level.WARN: + _ = sysout.Warning(msg) + case level.ERROR: + _ = sysout.Err(msg) + case level.FATAL: + _ = sysout.Crit(msg) } } -// OutputSplitFunc implements the io.Writer interface for use with Logrus, and simply -// splits logs between stdout and stderr depending on their severity. -type OutputSplitFunc func(lvl []byte) io.Writer +// args returns an args format string of format '%v' * count. +func args(count int) string { + const args = `%v%v%v%v%v%v%v%v%v%v` + + `%v%v%v%v%v%v%v%v%v%v` + + `%v%v%v%v%v%v%v%v%v%v` + + `%v%v%v%v%v%v%v%v%v%v` -var levelBytes = []byte("level=") - -func (fn OutputSplitFunc) Write(b []byte) (int, error) { - var lvl []byte - if i := bytes.Index(b, levelBytes); i >= 0 { - blvl := b[i+len(levelBytes):] - if i := bytes.IndexByte(blvl, ' '); i >= 0 { - lvl = blvl[:i] - } + // Use predetermined args str + if count < len(args) { + return args[:count*2] } - return fn(lvl).Write(b) + + // Allocate buffer of needed len + var buf strings.Builder + buf.Grow(count * 2) + + // Manually build an args str + for i := 0; i < count; i++ { + buf.WriteString(`%v`) + } + + return buf.String() } diff --git a/internal/log/log_test.go b/internal/log/log_test.go deleted file mode 100644 index 01aae73ae..000000000 --- a/internal/log/log_test.go +++ /dev/null @@ -1,69 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package log_test - -import ( - "bytes" - "testing" - - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -func TestOutputSplitFunc(t *testing.T) { - var outbuf, errbuf bytes.Buffer - - out := log.SplitErrOutputs(&outbuf, &errbuf) - - log := logrus.New() - log.SetOutput(out) - log.SetLevel(logrus.TraceLevel) - - for _, lvl := range logrus.AllLevels { - func() { - defer func() { recover() }() - log.Log(lvl, "hello world") - }() - - t.Logf("outbuf=%q errbuf=%q", outbuf.String(), errbuf.String()) - - switch lvl { - case logrus.PanicLevel: - if outbuf.Len() > 0 || errbuf.Len() == 0 { - t.Error("expected panic to log to OutputSplitter.Err") - } - case logrus.FatalLevel: - if outbuf.Len() > 0 || errbuf.Len() == 0 { - t.Error("expected fatal to log to OutputSplitter.Err") - } - case logrus.ErrorLevel: - if outbuf.Len() > 0 || errbuf.Len() == 0 { - t.Error("expected error to log to OutputSplitter.Err") - } - default: - if outbuf.Len() == 0 || errbuf.Len() > 0 { - t.Errorf("expected %s to log to OutputSplitter.Out", lvl) - } - } - - // Reset buffers - outbuf.Reset() - errbuf.Reset() - } -} diff --git a/internal/log/pool.go b/internal/log/pool.go new file mode 100644 index 000000000..cb2edfbea --- /dev/null +++ b/internal/log/pool.go @@ -0,0 +1,49 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package log + +import ( + "sync" + + "codeberg.org/gruf/go-byteutil" +) + +// bufPool provides a memory pool of log buffers. +var bufPool = sync.Pool{ + New: func() any { + return &byteutil.Buffer{ + B: make([]byte, 0, 512), + } + }, +} + +// getBuf acquires a buffer from memory pool. +func getBuf() *byteutil.Buffer { + buf, _ := bufPool.Get().(*byteutil.Buffer) + return buf +} + +// putBuf places (after resetting) buffer back in memory pool, dropping if capacity too large. +func putBuf(buf *byteutil.Buffer) { + if buf.Cap() > int(^uint16(0)) { + return // drop large buffer + } + buf.Reset() + bufPool.Put(buf) +} diff --git a/internal/log/syslog_test.go b/internal/log/syslog_test.go index d58dfdcba..aafa93419 100644 --- a/internal/log/syslog_test.go +++ b/internal/log/syslog_test.go @@ -19,15 +19,16 @@ package log_test import ( + "fmt" "os" "path" "regexp" "testing" "github.com/google/uuid" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/testrig" "gopkg.in/mcuadros/go-syslog.v2" "gopkg.in/mcuadros/go-syslog.v2/format" @@ -65,17 +66,21 @@ func (suite *SyslogTestSuite) TearDownTest() { } func (suite *SyslogTestSuite) TestSyslog() { - logrus.Warn("this is a test of the emergency broadcast system!") + log.Info("this is a test of the emergency broadcast system!") entry := <-suite.syslogChannel - suite.Regexp(regexp.MustCompile(`time=.* msg=this is a test of the emergency broadcast system! func=.*`), entry["content"]) + suite.Regexp(regexp.MustCompile(`timestamp=.* func=.* level=INFO msg="this is a test of the emergency broadcast system!"`), entry["content"]) } func (suite *SyslogTestSuite) TestSyslogLongMessage() { - logrus.Warn(longMessage) + log.Warn(longMessage) + + funcName := log.Caller(2) + prefix := fmt.Sprintf(`timestamp="02/01/2006 15:04:05.000" func=%s level=WARN msg="`, funcName) entry := <-suite.syslogChannel - suite.Regexp(regexp.MustCompile(`time=.* msg=condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus viverra vitae congue eu consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim sodales ut eu sem integer vitae justo eget magna fermentum iaculis eu non diam phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet enim tortor at auctor urna nunc id cursus metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper eget duis at tellus at urna condimentum mattis pellentesque id nibh tortor id aliquet lectus proin nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas sed tempus urna et pharetra pharetra massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas integer eget aliquet nibh praesent tristique magna sit amet purus gravida quis blandit turpis cursus in hac habitasse platea dictumst quisque sagittis purus sit amet volutpat consequat mauris nunc congue nisi vitae suscipit tellus mauris a diam maecenas sed enim ut sem viverra aliquet eget sit amet tellus cras adipiscing enim eu turpis egestas pretium aenean pharetra magna ac placerat vestibulum lectus mauris ultrices eros in cursus turpis massa tincidunt dui ut ornare lectus sit a\.\.\. func=.*`), entry["content"]) + regex := fmt.Sprintf(`timestamp=.* func=.* level=WARN msg="%s\.\.\.`, longMessage[:1700-len(prefix)-3]) + suite.Regexp(regexp.MustCompile(regex), entry["content"]) } func (suite *SyslogTestSuite) TestSyslogLongMessageUnixgram() { @@ -99,10 +104,15 @@ func (suite *SyslogTestSuite) TestSyslogLongMessageUnixgram() { testrig.InitTestLog() - logrus.Warn(longMessage) + log.Warn(longMessage) + + funcName := log.Caller(2) + prefix := fmt.Sprintf(`timestamp="02/01/2006 15:04:05.000" func=%s level=WARN msg="`, funcName) entry := <-syslogChannel - suite.Regexp(regexp.MustCompile(`time=.* msg=condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus viverra vitae congue eu consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim sodales ut eu sem integer vitae justo eget magna fermentum iaculis eu non diam phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet enim tortor at auctor urna nunc id cursus metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper eget duis at tellus at urna condimentum mattis pellentesque id nibh tortor id aliquet lectus proin nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas sed tempus urna et pharetra pharetra massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas integer eget aliquet nibh praesent tristique magna sit amet purus gravida quis blandit turpis cursus in hac habitasse platea dictumst quisque sagittis purus sit amet volutpat consequat mauris nunc congue nisi vitae suscipit tellus mauris a diam maecenas sed enim ut sem viverra aliquet eget sit amet tellus cras adipiscing enim eu turpis egestas pretium aenean pharetra magna ac placerat vestibulum lectus mauris ultrices eros in cursus turpis massa tincidunt dui ut ornare lectus sit a\.\.\. func=.*`), entry["content"]) + regex := fmt.Sprintf(`timestamp=.* func=.* level=WARN msg="%s\.\.\.`, longMessage[:1700-len(prefix)-3]) + + suite.Regexp(regexp.MustCompile(regex), entry["content"]) if err := syslogServer.Kill(); err != nil { panic(err) diff --git a/internal/log/trimhook.go b/internal/log/trimhook.go deleted file mode 100644 index c9faccb90..000000000 --- a/internal/log/trimhook.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package log - -import ( - "github.com/sirupsen/logrus" -) - -// trimHook is a wrapper round a logrus hook that trims the *entry.Message -// to no more than 1700 characters before sending it through to the wrapped hook, -// to avoid spamming syslog with messages that are too long for it. -type trimHook struct { - wrappedHook logrus.Hook -} - -func (t *trimHook) Fire(e *logrus.Entry) error { - // only copy/truncate if we need to - if len(e.Message) < 1700 { - return t.wrappedHook.Fire(e) - } - - // it's too long, truncate + fire a copy of the entry so we don't meddle with the original - return t.wrappedHook.Fire(&logrus.Entry{ - Logger: e.Logger, - Data: e.Data, - Time: e.Time, - Level: e.Level, - Caller: e.Caller, - Message: e.Message[:1696] + "...", // truncate - Buffer: e.Buffer, - Context: e.Context, - }) -} - -func (t *trimHook) Levels() []logrus.Level { - return t.wrappedHook.Levels() -} diff --git a/internal/log/writer.go b/internal/log/writer.go new file mode 100644 index 000000000..4356d9b7d --- /dev/null +++ b/internal/log/writer.go @@ -0,0 +1,37 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package log + +import ( + "io" + "sync" +) + +// safewriter wraps a writer to provide mutex safety on write. +type safewriter struct { + w io.Writer + m sync.Mutex +} + +func (w *safewriter) Write(b []byte) (int, error) { + w.m.Lock() + n, err := w.w.Write(b) + w.m.Unlock() + return n, err +} diff --git a/internal/media/manager.go b/internal/media/manager.go index b14288324..828aa033b 100644 --- a/internal/media/manager.go +++ b/internal/media/manager.go @@ -24,10 +24,10 @@ import ( "time" "github.com/robfig/cron/v3" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -210,10 +210,10 @@ func scheduleCleanupJobs(m *manager) error { begin := time.Now() pruned, err := m.PruneAllMeta(pruneCtx) if err != nil { - logrus.Errorf("media manager: error pruning meta: %s", err) + log.Errorf("media manager: error pruning meta: %s", err) return } - logrus.Infof("media manager: pruned %d meta entries in %s", pruned, time.Since(begin)) + log.Infof("media manager: pruned %d meta entries in %s", pruned, time.Since(begin)) }); err != nil { pruneCancel() return fmt.Errorf("error starting media manager meta cleanup job: %s", err) @@ -223,10 +223,10 @@ func scheduleCleanupJobs(m *manager) error { begin := time.Now() pruned, err := m.PruneUnusedLocalAttachments(pruneCtx) if err != nil { - logrus.Errorf("media manager: error pruning unused local attachments: %s", err) + log.Errorf("media manager: error pruning unused local attachments: %s", err) return } - logrus.Infof("media manager: pruned %d unused local attachments in %s", pruned, time.Since(begin)) + log.Infof("media manager: pruned %d unused local attachments in %s", pruned, time.Since(begin)) }); err != nil { pruneCancel() return fmt.Errorf("error starting media manager unused local attachments cleanup job: %s", err) @@ -238,10 +238,10 @@ func scheduleCleanupJobs(m *manager) error { begin := time.Now() pruned, err := m.PruneAllRemote(pruneCtx, mediaRemoteCacheDays) if err != nil { - logrus.Errorf("media manager: error pruning remote cache: %s", err) + log.Errorf("media manager: error pruning remote cache: %s", err) return } - logrus.Infof("media manager: pruned %d remote cache entries in %s", pruned, time.Since(begin)) + log.Infof("media manager: pruned %d remote cache entries in %s", pruned, time.Since(begin)) }); err != nil { pruneCancel() return fmt.Errorf("error starting media manager remote cache cleanup job: %s", err) @@ -254,9 +254,9 @@ func scheduleCleanupJobs(m *manager) error { select { case <-cronCtx.Done(): - logrus.Infof("media manager: cron finished jobs and stopped gracefully") + log.Infof("media manager: cron finished jobs and stopped gracefully") case <-time.After(1 * time.Minute): - logrus.Infof("media manager: cron didn't stop after 60 seconds, will force close jobs") + log.Infof("media manager: cron didn't stop after 60 seconds, will force close jobs") break } diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go index ffac56052..b28dc7f02 100644 --- a/internal/media/processingemoji.go +++ b/internal/media/processingemoji.go @@ -28,9 +28,9 @@ import ( "sync/atomic" "time" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -174,7 +174,7 @@ func (p *ProcessingEmoji) store(ctx context.Context) error { defer func() { if rc, ok := reader.(io.ReadCloser); ok { if err := rc.Close(); err != nil { - logrus.Errorf("store: error closing readcloser: %s", err) + log.Errorf("store: error closing readcloser: %s", err) } } }() diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go index 17fddddb7..885e97417 100644 --- a/internal/media/processingmedia.go +++ b/internal/media/processingmedia.go @@ -28,11 +28,11 @@ import ( "sync/atomic" "time" - "github.com/sirupsen/logrus" terminator "github.com/superseriousbusiness/exif-terminator" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -80,10 +80,10 @@ func (p *ProcessingMedia) AttachmentID() string { // LoadAttachment blocks until the thumbnail and fullsize content // has been processed, and then returns the completed attachment. func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) { - logrus.Tracef("LoadAttachment: getting lock for attachment %s", p.attachment.URL) + log.Tracef("LoadAttachment: getting lock for attachment %s", p.attachment.URL) p.mu.Lock() defer p.mu.Unlock() - logrus.Tracef("LoadAttachment: got lock for attachment %s", p.attachment.URL) + log.Tracef("LoadAttachment: got lock for attachment %s", p.attachment.URL) if err := p.store(ctx); err != nil { return nil, err @@ -113,7 +113,7 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt p.insertedInDB = true } - logrus.Tracef("LoadAttachment: finished, returning attachment %s", p.attachment.URL) + log.Tracef("LoadAttachment: finished, returning attachment %s", p.attachment.URL) return p.attachment, nil } @@ -137,7 +137,7 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { } // stream the original file out of storage - logrus.Tracef("loadThumb: fetching attachment from storage %s", p.attachment.URL) + log.Tracef("loadThumb: fetching attachment from storage %s", p.attachment.URL) stored, err := p.storage.GetStream(ctx, p.attachment.File.Path) if err != nil { p.err = fmt.Errorf("loadThumb: error fetching file from storage: %s", err) @@ -147,14 +147,14 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { // whatever happens, close the stream when we're done defer func() { - logrus.Tracef("loadThumb: closing stored stream %s", p.attachment.URL) + log.Tracef("loadThumb: closing stored stream %s", p.attachment.URL) if err := stored.Close(); err != nil { - logrus.Errorf("loadThumb: error closing stored full size: %s", err) + log.Errorf("loadThumb: error closing stored full size: %s", err) } }() // stream the file from storage straight into the derive thumbnail function - logrus.Tracef("loadThumb: calling deriveThumbnail %s", p.attachment.URL) + log.Tracef("loadThumb: calling deriveThumbnail %s", p.attachment.URL) thumb, err := deriveThumbnail(stored, p.attachment.File.ContentType, createBlurhash) if err != nil { p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err) @@ -163,7 +163,7 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { } // put the thumbnail in storage - logrus.Tracef("loadThumb: storing new thumbnail %s", p.attachment.URL) + log.Tracef("loadThumb: storing new thumbnail %s", p.attachment.URL) if err := p.storage.Put(ctx, p.attachment.Thumbnail.Path, thumb.small); err != nil { p.err = fmt.Errorf("loadThumb: error storing thumbnail: %s", err) atomic.StoreInt32(&p.thumbState, int32(errored)) @@ -184,7 +184,7 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { // we're done processing the thumbnail! atomic.StoreInt32(&p.thumbState, int32(complete)) - logrus.Tracef("loadThumb: finished processing thumbnail for attachment %s", p.attachment.URL) + log.Tracef("loadThumb: finished processing thumbnail for attachment %s", p.attachment.URL) fallthrough case complete: return nil @@ -245,7 +245,7 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) error { // we're done processing the full-size image atomic.StoreInt32(&p.fullSizeState, int32(complete)) - logrus.Tracef("loadFullSize: finished processing full size image for attachment %s", p.attachment.URL) + log.Tracef("loadFullSize: finished processing full size image for attachment %s", p.attachment.URL) fallthrough case complete: return nil @@ -270,13 +270,13 @@ func (p *ProcessingMedia) store(ctx context.Context) error { if err != nil { return fmt.Errorf("store: error executing data function: %s", err) } - logrus.Tracef("store: reading %d bytes from data function for media %s", fileSize, p.attachment.URL) + log.Tracef("store: reading %d bytes from data function for media %s", fileSize, p.attachment.URL) // defer closing the reader when we're done with it defer func() { if rc, ok := reader.(io.ReadCloser); ok { if err := rc.Close(); err != nil { - logrus.Errorf("store: error closing readcloser: %s", err) + log.Errorf("store: error closing readcloser: %s", err) } } }() @@ -330,7 +330,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error { defer func() { if rc, ok := clean.(io.ReadCloser); ok { if err := rc.Close(); err != nil { - logrus.Errorf("store: error closing clean readcloser: %s", err) + log.Errorf("store: error closing clean readcloser: %s", err) } } }() @@ -353,7 +353,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error { return p.postData(ctx) } - logrus.Tracef("store: finished storing initial data for attachment %s", p.attachment.URL) + log.Tracef("store: finished storing initial data for attachment %s", p.attachment.URL) return nil } diff --git a/internal/media/prunemeta.go b/internal/media/prunemeta.go index 33391beaf..7b2b14f98 100644 --- a/internal/media/prunemeta.go +++ b/internal/media/prunemeta.go @@ -22,9 +22,9 @@ import ( "context" "codeberg.org/gruf/go-store/storage" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (m *manager) PruneAllMeta(ctx context.Context) (int, error) { @@ -37,7 +37,7 @@ func (m *manager) PruneAllMeta(ctx context.Context) (int, error) { for attachments, err = m.db.GetAvatarsAndHeaders(ctx, maxID, selectPruneLimit); err == nil && len(attachments) != 0; attachments, err = m.db.GetAvatarsAndHeaders(ctx, maxID, selectPruneLimit) { // use the id of the last attachment in the slice as the next 'maxID' value l := len(attachments) - logrus.Tracef("PruneAllMeta: got %d attachments with maxID < %s", l, maxID) + log.Tracef("PruneAllMeta: got %d attachments with maxID < %s", l, maxID) maxID = attachments[l-1].ID // prune each attachment that meets one of the following criteria: @@ -61,14 +61,14 @@ func (m *manager) PruneAllMeta(ctx context.Context) (int, error) { return totalPruned, err } - logrus.Infof("PruneAllMeta: finished pruning avatars + headers: pruned %d entries", totalPruned) + log.Infof("PruneAllMeta: finished pruning avatars + headers: pruned %d entries", totalPruned) return totalPruned, nil } func (m *manager) pruneOneAvatarOrHeader(ctx context.Context, attachment *gtsmodel.MediaAttachment) error { if attachment.File.Path != "" { // delete the full size attachment from storage - logrus.Tracef("pruneOneAvatarOrHeader: deleting %s", attachment.File.Path) + log.Tracef("pruneOneAvatarOrHeader: deleting %s", attachment.File.Path) if err := m.storage.Delete(ctx, attachment.File.Path); err != nil && err != storage.ErrNotFound { return err } @@ -76,7 +76,7 @@ func (m *manager) pruneOneAvatarOrHeader(ctx context.Context, attachment *gtsmod if attachment.Thumbnail.Path != "" { // delete the thumbnail from storage - logrus.Tracef("pruneOneAvatarOrHeader: deleting %s", attachment.Thumbnail.Path) + log.Tracef("pruneOneAvatarOrHeader: deleting %s", attachment.Thumbnail.Path) if err := m.storage.Delete(ctx, attachment.Thumbnail.Path); err != nil && err != storage.ErrNotFound { return err } diff --git a/internal/media/prunemeta_test.go b/internal/media/prunemeta_test.go index 8b250e7a5..b02156587 100644 --- a/internal/media/prunemeta_test.go +++ b/internal/media/prunemeta_test.go @@ -85,6 +85,7 @@ func (suite *PruneMetaTestSuite) TestPruneMetaTwice() { suite.NoError(err) suite.Equal(0, totalPruned) } + func (suite *PruneMetaTestSuite) TestPruneMetaMultipleAccounts() { ctx := context.Background() diff --git a/internal/media/pruneremote.go b/internal/media/pruneremote.go index 6cad7fbf8..5c3335511 100644 --- a/internal/media/pruneremote.go +++ b/internal/media/pruneremote.go @@ -23,9 +23,9 @@ import ( "fmt" "codeberg.org/gruf/go-store/storage" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (m *manager) PruneAllRemote(ctx context.Context, olderThanDays int) (int, error) { @@ -35,14 +35,14 @@ func (m *manager) PruneAllRemote(ctx context.Context, olderThanDays int) (int, e if err != nil { return totalPruned, fmt.Errorf("PruneAllRemote: error parsing olderThanDays %d: %s", olderThanDays, err) } - logrus.Infof("PruneAllRemote: pruning media older than %s", olderThan) + log.Infof("PruneAllRemote: pruning media older than %s", olderThan) // select 20 attachments at a time and prune them for attachments, err := m.db.GetRemoteOlderThan(ctx, olderThan, selectPruneLimit); err == nil && len(attachments) != 0; attachments, err = m.db.GetRemoteOlderThan(ctx, olderThan, selectPruneLimit) { // use the age of the oldest attachment (the last one in the slice) as the next 'older than' value l := len(attachments) - logrus.Tracef("PruneAllRemote: got %d attachments older than %s", l, olderThan) + log.Tracef("PruneAllRemote: got %d attachments older than %s", l, olderThan) olderThan = attachments[l-1].CreatedAt // prune each attachment @@ -59,14 +59,14 @@ func (m *manager) PruneAllRemote(ctx context.Context, olderThanDays int) (int, e return totalPruned, err } - logrus.Infof("PruneAllRemote: finished pruning remote media: pruned %d entries", totalPruned) + log.Infof("PruneAllRemote: finished pruning remote media: pruned %d entries", totalPruned) return totalPruned, nil } func (m *manager) pruneOneRemote(ctx context.Context, attachment *gtsmodel.MediaAttachment) error { if attachment.File.Path != "" { // delete the full size attachment from storage - logrus.Tracef("pruneOneRemote: deleting %s", attachment.File.Path) + log.Tracef("pruneOneRemote: deleting %s", attachment.File.Path) if err := m.storage.Delete(ctx, attachment.File.Path); err != nil && err != storage.ErrNotFound { return err } @@ -75,7 +75,7 @@ func (m *manager) pruneOneRemote(ctx context.Context, attachment *gtsmodel.Media if attachment.Thumbnail.Path != "" { // delete the thumbnail from storage - logrus.Tracef("pruneOneRemote: deleting %s", attachment.Thumbnail.Path) + log.Tracef("pruneOneRemote: deleting %s", attachment.Thumbnail.Path) if err := m.storage.Delete(ctx, attachment.Thumbnail.Path); err != nil && err != storage.ErrNotFound { return err } diff --git a/internal/media/pruneunusedlocal.go b/internal/media/pruneunusedlocal.go index 789583938..ea777428b 100644 --- a/internal/media/pruneunusedlocal.go +++ b/internal/media/pruneunusedlocal.go @@ -23,9 +23,9 @@ import ( "fmt" "codeberg.org/gruf/go-store/storage" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (m *manager) PruneUnusedLocalAttachments(ctx context.Context) (int, error) { @@ -38,14 +38,14 @@ func (m *manager) PruneUnusedLocalAttachments(ctx context.Context) (int, error) if err != nil { return totalPruned, fmt.Errorf("PruneUnusedLocalAttachments: error parsing olderThanDays %d: %s", UnusedLocalAttachmentCacheDays, err) } - logrus.Infof("PruneUnusedLocalAttachments: pruning unused local attachments older than %s", olderThan) + log.Infof("PruneUnusedLocalAttachments: pruning unused local attachments older than %s", olderThan) // select 20 attachments at a time and prune them for attachments, err = m.db.GetLocalUnattachedOlderThan(ctx, olderThan, maxID, selectPruneLimit); err == nil && len(attachments) != 0; attachments, err = m.db.GetLocalUnattachedOlderThan(ctx, olderThan, maxID, selectPruneLimit) { // use the id of the last attachment in the slice as the next 'maxID' value l := len(attachments) maxID = attachments[l-1].ID - logrus.Tracef("PruneUnusedLocalAttachments: got %d unused local attachments older than %s with maxID < %s", l, olderThan, maxID) + log.Tracef("PruneUnusedLocalAttachments: got %d unused local attachments older than %s with maxID < %s", l, olderThan, maxID) for _, attachment := range attachments { if err := m.pruneOneLocal(ctx, attachment); err != nil { @@ -60,14 +60,14 @@ func (m *manager) PruneUnusedLocalAttachments(ctx context.Context) (int, error) return totalPruned, err } - logrus.Infof("PruneUnusedLocalAttachments: finished pruning: pruned %d entries", totalPruned) + log.Infof("PruneUnusedLocalAttachments: finished pruning: pruned %d entries", totalPruned) return totalPruned, nil } func (m *manager) pruneOneLocal(ctx context.Context, attachment *gtsmodel.MediaAttachment) error { if attachment.File.Path != "" { // delete the full size attachment from storage - logrus.Tracef("pruneOneLocal: deleting %s", attachment.File.Path) + log.Tracef("pruneOneLocal: deleting %s", attachment.File.Path) if err := m.storage.Delete(ctx, attachment.File.Path); err != nil && err != storage.ErrNotFound { return err } @@ -75,7 +75,7 @@ func (m *manager) pruneOneLocal(ctx context.Context, attachment *gtsmodel.MediaA if attachment.Thumbnail.Path != "" { // delete the thumbnail from storage - logrus.Tracef("pruneOneLocal: deleting %s", attachment.Thumbnail.Path) + log.Tracef("pruneOneLocal: deleting %s", attachment.Thumbnail.Path) if err := m.storage.Delete(ctx, attachment.Thumbnail.Path); err != nil && err != storage.ErrNotFound { return err } diff --git a/internal/media/util.go b/internal/media/util.go index 9d62619f5..aea2ad990 100644 --- a/internal/media/util.go +++ b/internal/media/util.go @@ -24,7 +24,7 @@ import ( "time" "github.com/h2non/filetype" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // AllSupportedMIMETypes just returns all media @@ -117,17 +117,16 @@ func ParseMediaSize(s string) (Size, error) { } // logrusWrapper is just a util for passing the logrus logger into the cron logging system. -type logrusWrapper struct { -} +type logrusWrapper struct{} // Info logs routine messages about cron's operation. func (l *logrusWrapper) Info(msg string, keysAndValues ...interface{}) { - logrus.Info("media manager cron logger: ", msg, keysAndValues) + log.Info("media manager cron logger: ", msg, keysAndValues) } // Error logs an error condition. func (l *logrusWrapper) Error(err error, msg string, keysAndValues ...interface{}) { - logrus.Error("media manager cron logger: ", err, msg, keysAndValues) + log.Error("media manager cron logger: ", err, msg, keysAndValues) } func parseOlderThan(olderThanDays int) (time.Time, error) { diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index f9c6dc4fe..1dba43c2c 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -37,8 +37,6 @@ type PgClientStoreTestSuite struct { testClientUserID string } -const () - // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout func (suite *PgClientStoreTestSuite) SetupSuite() { suite.testClientID = "01FCVB74EW6YBYAEY7QG9CQQF6" diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 4dcc41ceb..d520b19ec 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -23,9 +23,9 @@ import ( "fmt" "net/http" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/errors" "github.com/superseriousbusiness/oauth2/v4/manage" @@ -95,12 +95,12 @@ func New(ctx context.Context, database db.Basic) Server { srv := server.NewServer(sc, manager) srv.SetInternalErrorHandler(func(err error) *errors.Response { - logrus.Errorf("internal oauth error: %s", err) + log.Errorf("internal oauth error: %s", err) return nil }) srv.SetResponseErrorHandler(func(re *errors.Response) { - logrus.Errorf("internal response error: %s", re.Error) + log.Errorf("internal response error: %s", re.Error) }) srv.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (string, error) { @@ -155,7 +155,6 @@ func (s *s) ValidationBearerToken(r *http.Request) (oauth2.TokenInfo, error) { // The ti parameter refers to an existing Application token that was used to make the upstream // request. This token needs to be validated and exist in database in order to create a new token. func (s *s) GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, clientSecret string, userID string) (oauth2.TokenInfo, error) { - authToken, err := s.server.Manager.GenerateAuthToken(ctx, oauth2.Code, &oauth2.TokenGenerateRequest{ ClientID: ti.GetClientID(), ClientSecret: clientSecret, @@ -169,7 +168,7 @@ func (s *s) GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, cl if authToken == nil { return nil, errors.New("generated auth token was empty") } - logrus.Tracef("obtained auth token: %+v", authToken) + log.Tracef("obtained auth token: %+v", authToken) accessToken, err := s.server.Manager.GenerateAccessToken(ctx, oauth2.AuthorizationCode, &oauth2.TokenGenerateRequest{ ClientID: authToken.GetClientID(), @@ -178,14 +177,13 @@ func (s *s) GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, cl Scope: authToken.GetScope(), Code: authToken.GetCode(), }) - if err != nil { return nil, fmt.Errorf("error generating user-level access token: %s", err) } if accessToken == nil { return nil, errors.New("generated user-level access token was empty") } - logrus.Tracef("obtained user-level access token: %+v", accessToken) + log.Tracef("obtained user-level access token: %+v", accessToken) return accessToken, nil } diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 5250e22b7..25946c212 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -24,10 +24,10 @@ import ( "fmt" "time" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/models" ) @@ -53,12 +53,12 @@ func newTokenStore(ctx context.Context, db db.Basic) oauth2.TokenStore { for { select { case <-ctx.Done(): - logrus.Info("breaking cleanloop") + log.Info("breaking cleanloop") break cleanloop case <-time.After(1 * time.Minute): - logrus.Trace("sweeping out old oauth entries broom broom") + log.Trace("sweeping out old oauth entries broom broom") if err := ts.sweep(ctx); err != nil { - logrus.Errorf("error while sweeping oauth entries: %s", err) + log.Errorf("error while sweeping oauth entries: %s", err) } } } diff --git a/internal/oidc/handlecallback.go b/internal/oidc/handlecallback.go index 588fb227b..ede4f51f2 100644 --- a/internal/oidc/handlecallback.go +++ b/internal/oidc/handlecallback.go @@ -23,34 +23,33 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) { - l := logrus.WithField("func", "HandleCallback") if code == "" { err := errors.New("code was empty string") return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - l.Debug("exchanging code for oauth2token") + log.Debug("exchanging code for oauth2token") oauth2Token, err := i.oauth2Config.Exchange(ctx, code) if err != nil { err := fmt.Errorf("error exchanging code for oauth2token: %s", err) return nil, gtserror.NewErrorInternalError(err) } - l.Debug("extracting id_token") + log.Debug("extracting id_token") rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { err := errors.New("no id_token in oauth2token") return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - l.Debugf("raw id token: %s", rawIDToken) + log.Debugf("raw id token: %s", rawIDToken) // Parse and verify ID Token payload. - l.Debug("verifying id_token") + log.Debug("verifying id_token") idTokenVerifier := i.provider.Verifier(i.oidcConf) idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) if err != nil { @@ -58,7 +57,7 @@ func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, gtserro return nil, gtserror.NewErrorUnauthorized(err, err.Error()) } - l.Debug("extracting claims from id_token") + log.Debug("extracting claims from id_token") claims := &Claims{} if err := idToken.Claims(claims); err != nil { err := fmt.Errorf("could not parse claims from idToken: %s", err) diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go index 44d6bbea5..e6cb44ce8 100644 --- a/internal/processing/account/create.go +++ b/internal/processing/account/create.go @@ -22,21 +22,18 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/oauth2/v4" ) func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { - l := logrus.WithField("func", "accountCreate") - emailAvailable, err := p.db.IsEmailAvailable(ctx, form.Email) if err != nil { return nil, gtserror.NewErrorBadRequest(err) @@ -62,13 +59,13 @@ func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInf reason = "" } - l.Trace("creating new username and account") + log.Trace("creating new username and account") user, err := p.db.NewSignup(ctx, form.Username, text.SanitizePlaintext(reason), approvalRequired, form.Email, form.Password, form.IP, form.Locale, application.ID, false, false) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new signup in the database: %s", err)) } - l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID) + log.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID) accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, applicationToken, application.ClientSecret, user.ID) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)) diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 67192cb8f..7b382f17c 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -23,12 +23,13 @@ import ( "errors" "time" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "golang.org/x/crypto/bcrypt" ) @@ -55,14 +56,16 @@ import ( // 17. Delete account's timeline // 18. Delete account itself func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { - fields := logrus.Fields{ - "func": "Delete", - "username": account.Username, + fields := kv.Fields{ + + {"username", account.Username}, } if account.Domain != "" { - fields["domain"] = account.Domain + fields = append(fields, kv.Field{ + "domain", account.Domain, + }) } - l := logrus.WithFields(fields) + l := log.WithFields(fields...) l.Debug("beginning account delete process") diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index d1085845b..78791dcce 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -24,13 +24,12 @@ import ( "io" "mime/multipart" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" @@ -39,8 +38,6 @@ import ( ) func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { - l := logrus.WithField("func", "AccountUpdate") - if form.Discoverable != nil { account.Discoverable = *form.Discoverable } @@ -81,7 +78,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form } account.AvatarMediaAttachmentID = avatarInfo.ID account.AvatarMediaAttachment = avatarInfo - l.Tracef("new avatar info for account %s is %+v", account.ID, avatarInfo) + log.Tracef("new avatar info for account %s is %+v", account.ID, avatarInfo) } if form.Header != nil && form.Header.Size != 0 { @@ -91,7 +88,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form } account.HeaderMediaAttachmentID = headerInfo.ID account.HeaderMediaAttachment = headerInfo - l.Tracef("new header info for account %s is %+v", account.ID, headerInfo) + log.Tracef("new header info for account %s is %+v", account.ID, headerInfo) } if form.Locked != nil { diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go index 1c641950c..b42445380 100644 --- a/internal/processing/admin/createdomainblock.go +++ b/internal/processing/admin/createdomainblock.go @@ -24,13 +24,14 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" ) @@ -90,10 +91,10 @@ func (p *processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Acc // 2. Delete the instance account for that instance if it exists. // 3. Select all accounts from this instance and pass them through the delete functionality of the processor. func (p *processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) { - l := logrus.WithFields(logrus.Fields{ - "func": "domainBlockProcessSideEffects", - "domain": block.Domain, - }) + l := log.WithFields(kv.Fields{ + + {"domain", block.Domain}, + }...) l.Debug("processing domain block side effects") diff --git a/internal/processing/admin/importdomainblocks.go b/internal/processing/admin/importdomainblocks.go index b78589b8a..57235a5b8 100644 --- a/internal/processing/admin/importdomainblocks.go +++ b/internal/processing/admin/importdomainblocks.go @@ -34,7 +34,6 @@ import ( // DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file. func (p *processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) { - f, err := domains.Open() if err != nil { return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err)) @@ -56,7 +55,6 @@ func (p *processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Ac blocks := []*apimodel.DomainBlock{} for _, d := range d { block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "") - if err != nil { return nil, err } diff --git a/internal/processing/admin/mediaprune.go b/internal/processing/admin/mediaprune.go index 1c3398b78..40652c3ca 100644 --- a/internal/processing/admin/mediaprune.go +++ b/internal/processing/admin/mediaprune.go @@ -22,8 +22,8 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (p *processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode { @@ -35,27 +35,27 @@ func (p *processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gt go func() { pruned, err := p.mediaManager.PruneAllRemote(ctx, mediaRemoteCacheDays) if err != nil { - logrus.Errorf("MediaPrune: error pruning remote cache: %s", err) + log.Errorf("MediaPrune: error pruning remote cache: %s", err) } else { - logrus.Infof("MediaPrune: pruned %d remote cache entries", pruned) + log.Infof("MediaPrune: pruned %d remote cache entries", pruned) } }() go func() { pruned, err := p.mediaManager.PruneUnusedLocalAttachments(ctx) if err != nil { - logrus.Errorf("MediaPrune: error pruning unused local cache: %s", err) + log.Errorf("MediaPrune: error pruning unused local cache: %s", err) } else { - logrus.Infof("MediaPrune: pruned %d unused local cache entries", pruned) + log.Infof("MediaPrune: pruned %d unused local cache entries", pruned) } }() go func() { pruned, err := p.mediaManager.PruneAllMeta(ctx) if err != nil { - logrus.Errorf("MediaPrune: error pruning meta: %s", err) + log.Errorf("MediaPrune: error pruning meta: %s", err) } else { - logrus.Infof("MediaPrune: pruned %d meta entries", pruned) + log.Infof("MediaPrune: pruned %d meta entries", pruned) } }() diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 24465a059..0f684f200 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -24,15 +24,36 @@ import ( "fmt" "net/url" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + // Allocate new log fields slice + fields := make([]kv.Field, 3, 4) + fields[0] = kv.Field{"activityType", clientMsg.APActivityType} + fields[1] = kv.Field{"objectType", clientMsg.APObjectType} + fields[2] = kv.Field{"fromAccount", clientMsg.OriginAccount.Username} + + if clientMsg.GTSModel != nil && + log.Level() >= level.DEBUG { + // Append converted model to log + fields = append(fields, kv.Field{ + "model", clientMsg.GTSModel, + }) + } + + // Log this federated message + l := log.WithFields(fields...) + l.Info("processing from client") + switch clientMsg.APActivityType { case ap.ActivityCreate: // CREATE diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index e39a6b4e8..132b33f4c 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -24,11 +24,13 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -36,12 +38,30 @@ import ( // and directs the message into the appropriate side effect handler function, or simply does nothing if there's // no handler function defined for the combination of Activity and Object. func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { - l := logrus.WithFields(logrus.Fields{ - "func": "processFromFederator", - "APActivityType": federatorMsg.APActivityType, - "APObjectType": federatorMsg.APObjectType, - }) - l.Trace("processing message from federator") + // Allocate new log fields slice + fields := make([]kv.Field, 3, 5) + fields[0] = kv.Field{"activityType", federatorMsg.APActivityType} + fields[1] = kv.Field{"objectType", federatorMsg.APObjectType} + fields[2] = kv.Field{"toAccount", federatorMsg.ReceivingAccount.Username} + + if federatorMsg.APIri != nil { + // An IRI was supplied, append to log + fields = append(fields, kv.Field{ + "iri", federatorMsg.APIri, + }) + } + + if federatorMsg.GTSModel != nil && + log.Level() >= level.DEBUG { + // Append converted model to log + fields = append(fields, kv.Field{ + "model", federatorMsg.GTSModel, + }) + } + + // Log this federated message + l := log.WithFields(fields...) + l.Info("processing from federator") switch federatorMsg.APActivityType { case ap.ActivityCreate: diff --git a/internal/processing/media/getemoji.go b/internal/processing/media/getemoji.go index e84e5b570..ee33c25eb 100644 --- a/internal/processing/media/getemoji.go +++ b/internal/processing/media/getemoji.go @@ -22,10 +22,10 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (p *processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) { @@ -40,7 +40,7 @@ func (p *processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gts for _, gtsEmoji := range emojis { apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, gtsEmoji) if err != nil { - logrus.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) + log.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) continue } apiEmojis = append(apiEmojis, &apiEmoji) diff --git a/internal/processing/notification.go b/internal/processing/notification.go index ee0e4ae34..9b99141a6 100644 --- a/internal/processing/notification.go +++ b/internal/processing/notification.go @@ -21,18 +21,15 @@ package processing import ( "context" - "github.com/sirupsen/logrus" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/util" ) func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) { - l := logrus.WithField("func", "NotificationsGet") - notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, limit, maxID, sinceID) if err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -46,7 +43,7 @@ func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, li for _, n := range notifs { apiNotif, err := p.tc.NotificationToAPINotification(ctx, n) if err != nil { - l.Debugf("got an error converting a notification to api, will skip it: %s", err) + log.Debugf("got an error converting a notification to api, will skip it: %s", err) continue } timelineables = append(timelineables, apiNotif) diff --git a/internal/processing/search.go b/internal/processing/search.go index ee64526e5..d25bee2ae 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -25,22 +25,23 @@ import ( "net/url" "strings" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" ) func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) { - l := logrus.WithFields(logrus.Fields{ - "func": "SearchGet", - "query": search.Query, - }) + l := log.WithFields(kv.Fields{ + + {"query", search.Query}, + }...) // tidy up the query and make sure it wasn't just spaces query := strings.TrimSpace(search.Query) @@ -133,11 +134,11 @@ func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *a } func (p *processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Status, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "searchStatusByURI", - "uri": uri.String(), - "resolve": resolve, - }) + l := log.WithFields(kv.Fields{ + + {"uri", uri.String()}, + {"resolve", resolve}, + }...) if maybeStatus, err := p.db.GetStatusByURI(ctx, uri.String()); err == nil { return maybeStatus, nil diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 79c416f98..52214e95f 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -23,11 +23,11 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -204,12 +204,12 @@ func (p *processor) ProcessMentions(ctx context.Context, form *apimodel.Advanced for _, mentionedAccountName := range mentionedAccountNames { gtsMention, err := p.parseMention(ctx, mentionedAccountName, accountID, status.ID) if err != nil { - logrus.Errorf("ProcessMentions: error parsing mention %s from status: %s", mentionedAccountName, err) + log.Errorf("ProcessMentions: error parsing mention %s from status: %s", mentionedAccountName, err) continue } if err := p.db.Put(ctx, gtsMention); err != nil { - logrus.Errorf("ProcessMentions: error putting mention in db: %s", err) + log.Errorf("ProcessMentions: error putting mention in db: %s", err) } mentions = append(mentions, gtsMention) diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go index 80c2da897..f1b826bd0 100644 --- a/internal/processing/status/util_test.go +++ b/internal/processing/status/util_test.go @@ -34,8 +34,11 @@ const statusText1 = `Another test @foss_satan@fossbros-anonymous.io #Hashtag Text` -const statusText1ExpectedFull = "

Another test @foss_satan

#Hashtag

Text

" -const statusText1ExpectedPartial = "

Another test @foss_satan

#Hashtag

Text

" + +const ( + statusText1ExpectedFull = "

Another test @foss_satan

#Hashtag

Text

" + statusText1ExpectedPartial = "

Another test @foss_satan

#Hashtag

Text

" +) const statusText2 = `Another test @foss_satan@fossbros-anonymous.io @@ -97,7 +100,6 @@ func (suite *UtilTestSuite) TestProcessMentions1() { } func (suite *UtilTestSuite) TestProcessContentFull1() { - /* TEST PREPARATION */ @@ -146,7 +148,6 @@ func (suite *UtilTestSuite) TestProcessContentFull1() { } func (suite *UtilTestSuite) TestProcessContentPartial1() { - /* TEST PREPARATION */ @@ -238,7 +239,6 @@ func (suite *UtilTestSuite) TestProcessMentions2() { } func (suite *UtilTestSuite) TestProcessContentFull2() { - /* TEST PREPARATION */ @@ -288,7 +288,6 @@ func (suite *UtilTestSuite) TestProcessContentFull2() { } func (suite *UtilTestSuite) TestProcessContentPartial2() { - /* TEST PREPARATION */ diff --git a/internal/processing/statustimeline.go b/internal/processing/statustimeline.go index 081709302..b7313818c 100644 --- a/internal/processing/statustimeline.go +++ b/internal/processing/statustimeline.go @@ -23,12 +23,11 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -73,7 +72,7 @@ func StatusFilterFunction(database db.DB, filter visibility.Filter) timeline.Fil timelineable, err := filter.StatusHometimelineable(ctx, status, requestingAccount) if err != nil { - logrus.Warnf("error checking hometimelineability of status %s for account %s: %s", status.ID, timelineAccountID, err) + log.Warnf("error checking hometimelineability of status %s for account %s: %s", status.ID, timelineAccountID, err) } return timelineable, nil // we don't return the error here because we want to just skip this item if something goes wrong @@ -235,14 +234,12 @@ func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma } func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { - l := logrus.WithField("func", "filterPublicStatuses") - apiStatuses := []*apimodel.Status{} for _, s := range statuses { targetAccount := >smodel.Account{} if err := p.db.GetByID(ctx, s.AccountID, targetAccount); err != nil { if err == db.ErrNoEntries { - l.Debugf("filterPublicStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) + log.Debugf("filterPublicStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) continue } return nil, gtserror.NewErrorInternalError(fmt.Errorf("filterPublicStatuses: error getting status author: %s", err)) @@ -250,7 +247,7 @@ func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth timelineable, err := p.filter.StatusPublictimelineable(ctx, s, authed.Account) if err != nil { - l.Debugf("filterPublicStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err) + log.Debugf("filterPublicStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err) continue } if !timelineable { @@ -259,7 +256,7 @@ func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth apiStatus, err := p.tc.StatusToAPIStatus(ctx, s, authed.Account) if err != nil { - l.Debugf("filterPublicStatuses: skipping status %s because it couldn't be converted to its api representation: %s", s.ID, err) + log.Debugf("filterPublicStatuses: skipping status %s because it couldn't be converted to its api representation: %s", s.ID, err) continue } @@ -270,14 +267,12 @@ func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth } func (p *processor) filterFavedStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { - l := logrus.WithField("func", "filterFavedStatuses") - apiStatuses := []*apimodel.Status{} for _, s := range statuses { targetAccount := >smodel.Account{} if err := p.db.GetByID(ctx, s.AccountID, targetAccount); err != nil { if err == db.ErrNoEntries { - l.Debugf("filterFavedStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) + log.Debugf("filterFavedStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) continue } return nil, gtserror.NewErrorInternalError(fmt.Errorf("filterPublicStatuses: error getting status author: %s", err)) @@ -285,7 +280,7 @@ func (p *processor) filterFavedStatuses(ctx context.Context, authed *oauth.Auth, timelineable, err := p.filter.StatusVisible(ctx, s, authed.Account) if err != nil { - l.Debugf("filterFavedStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err) + log.Debugf("filterFavedStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err) continue } if !timelineable { @@ -294,7 +289,7 @@ func (p *processor) filterFavedStatuses(ctx context.Context, authed *oauth.Auth, apiStatus, err := p.tc.StatusToAPIStatus(ctx, s, authed.Account) if err != nil { - l.Debugf("filterFavedStatuses: skipping status %s because it couldn't be converted to its api representation: %s", s.ID, err) + log.Debugf("filterFavedStatuses: skipping status %s because it couldn't be converted to its api representation: %s", s.ID, err) continue } diff --git a/internal/processing/streaming/openstream.go b/internal/processing/streaming/openstream.go index b005ef9bd..3623a7ceb 100644 --- a/internal/processing/streaming/openstream.go +++ b/internal/processing/streaming/openstream.go @@ -23,19 +23,20 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/stream" ) func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamTimeline string) (*stream.Stream, gtserror.WithCode) { - l := logrus.WithFields(logrus.Fields{ - "func": "OpenStreamForAccount", - "account": account.ID, - "streamType": streamTimeline, - }) + l := log.WithFields(kv.Fields{ + + {"account", account.ID}, + {"streamType", streamTimeline}, + }...) l.Debug("received open stream request") // each stream needs a unique ID so we know to close it diff --git a/internal/router/logger.go b/internal/router/logger.go index 692a616a4..2e23b9cfb 100644 --- a/internal/router/logger.go +++ b/internal/router/logger.go @@ -21,19 +21,20 @@ package router import ( "fmt" "net/http" - "os" "time" "codeberg.org/gruf/go-bytesize" "codeberg.org/gruf/go-errors/v2" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // loggingMiddleware provides a request logging and panic recovery gin handler. func loggingMiddleware(c *gin.Context) { // Initialize the logging fields - fields := make(logrus.Fields, 7) + fields := make(kv.Fields, 6, 7) // Determine pre-handler time before := time.Now() @@ -49,11 +50,12 @@ func loggingMiddleware(c *gin.Context) { } // Append panic information to the request ctx - _ = c.Error(fmt.Errorf("recovered panic: %v", r)) + err := fmt.Errorf("recovered panic: %v", r) + _ = c.Error(err) - // Dump a stacktrace to stderr + // Dump a stacktrace to error log callers := errors.GetCallers(3, 10) - fmt.Fprintf(os.Stderr, "recovered panic: %v\n%s", r, callers) + log.WithField("stacktrace", callers).Error(err) } // NOTE: @@ -63,32 +65,38 @@ func loggingMiddleware(c *gin.Context) { // and could lead to storing plaintext API keys in logs // Set request logging fields - fields["latency"] = time.Since(before) - fields["clientIP"] = c.ClientIP() - fields["userAgent"] = c.Request.UserAgent() - fields["method"] = c.Request.Method - fields["statusCode"] = code - fields["path"] = path + fields[0] = kv.Field{"latency", time.Since(before)} + fields[1] = kv.Field{"clientIP", c.ClientIP()} + fields[2] = kv.Field{"userAgent", c.Request.UserAgent()} + fields[3] = kv.Field{"method", c.Request.Method} + fields[4] = kv.Field{"statusCode", code} + fields[5] = kv.Field{"path", path} - // Create a log entry with fields - l := logrus.WithFields(fields) - l.Level = logrus.InfoLevel + var lvl level.LEVEL + + // Default is info + lvl = level.INFO if code >= 500 { // This is a server error - l.Level = logrus.ErrorLevel + lvl = level.ERROR if len(c.Errors) > 0 { // Add an error string log field - fields["error"] = c.Errors.String() + fields = append(fields, kv.Field{ + "error", c.Errors.String(), + }) } } // Generate a nicer looking bytecount size := bytesize.Size(c.Writer.Size()) + // Create log entry with fields + l := log.WithFields(fields...) + // Finally, write log entry with status text body size - l.Logf(l.Level, "%s: wrote %s", http.StatusText(code), size) + l.Logf(lvl, "%s: wrote %s", http.StatusText(code), size) }() // Process request diff --git a/internal/router/router.go b/internal/router/router.go index 5eb4cb222..da00b685d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -26,9 +26,9 @@ import ( "codeberg.org/gruf/go-debug" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" "golang.org/x/crypto/acme/autocert" ) @@ -94,10 +94,10 @@ func (r *router) Start() { ) // Start the LetsEncrypt autocert manager HTTP server. - logrus.Infof("letsencrypt listening on %s", srv.Addr) + log.Infof("letsencrypt listening on %s", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logrus.Fatalf("letsencrypt: listen: %s", err) + log.Fatalf("letsencrypt: listen: %s", err) } }() @@ -112,16 +112,16 @@ func (r *router) Start() { r.srv.Handler = debug.WithPprof(r.srv.Handler) if debug.DEBUG() { // Profiling requires timeouts longer than 30s, so reset these. - logrus.Warn("resetting http.Server{} timeout to support profiling") + log.Warn("resetting http.Server{} timeout to support profiling") r.srv.ReadTimeout = 0 r.srv.WriteTimeout = 0 } // Start the main listener. go func() { - logrus.Infof("listening on %s", r.srv.Addr) + log.Infof("listening on %s", r.srv.Addr) if err := listen(); err != nil && err != http.ErrServerClosed { - logrus.Fatalf("listen: %s", err) + log.Fatalf("listen: %s", err) } }() } diff --git a/internal/router/session.go b/internal/router/session.go index b49542428..eb4b83874 100644 --- a/internal/router/session.go +++ b/internal/router/session.go @@ -29,9 +29,9 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" "golang.org/x/net/idna" ) @@ -44,7 +44,7 @@ func SessionOptions() sessions.Options { case "strict": samesite = http.SameSiteStrictMode default: - logrus.Warnf("%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), config.GetAdvancedCookiesSamesite()) + log.Warnf("%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), config.GetAdvancedCookiesSamesite()) samesite = http.SameSiteLaxMode } diff --git a/internal/storage/local.go b/internal/storage/local.go index da57631f9..8ed3ca8d8 100644 --- a/internal/storage/local.go +++ b/internal/storage/local.go @@ -33,18 +33,23 @@ type Local struct { func (l *Local) Get(ctx context.Context, key string) ([]byte, error) { return l.KVStore.Get(key) } + func (l *Local) GetStream(ctx context.Context, key string) (io.ReadCloser, error) { return l.KVStore.GetStream(key) } + func (l *Local) PutStream(ctx context.Context, key string, r io.Reader) error { return l.KVStore.PutStream(key, r) } + func (l *Local) Put(ctx context.Context, key string, value []byte) error { return l.KVStore.Put(key, value) } + func (l *Local) Delete(ctx context.Context, key string) error { return l.KVStore.Delete(key) } + func (l *Local) URL(ctx context.Context, key string) *url.URL { return nil } diff --git a/internal/storage/s3.go b/internal/storage/s3.go index a15114488..ee51bea89 100644 --- a/internal/storage/s3.go +++ b/internal/storage/s3.go @@ -55,6 +55,7 @@ func (s *S3) Get(ctx context.Context, key string) ([]byte, error) { } return b, nil } + func (s *S3) GetStream(ctx context.Context, key string) (io.ReadCloser, error) { o, err := s.mc.GetObject(ctx, s.bucket, key, minio.GetObjectOptions{}) if err != nil { @@ -62,21 +63,25 @@ func (s *S3) GetStream(ctx context.Context, key string) (io.ReadCloser, error) { } return o, err } + func (s *S3) PutStream(ctx context.Context, key string, r io.Reader) error { if _, err := s.mc.PutObject(ctx, s.bucket, key, r, -1, minio.PutObjectOptions{}); err != nil { return fmt.Errorf("uploading data stream: %w", err) } return nil } + func (s *S3) Put(ctx context.Context, key string, value []byte) error { if _, err := s.mc.PutObject(ctx, s.bucket, key, bytes.NewBuffer(value), -1, minio.PutObjectOptions{}); err != nil { return fmt.Errorf("uploading data slice: %w", err) } return nil } + func (s *S3) Delete(ctx context.Context, key string) error { return s.mc.RemoveObject(ctx, s.bucket, key, minio.RemoveObjectOptions{}) } + func (s *S3) URL(ctx context.Context, key string) *url.URL { // it's safe to ignore the error here, as we just fall back to fetching the // file if the url request fails diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 88577442f..7a4ebc6c3 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -33,9 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" ) -var ( - ErrNotSupported = errors.New("driver does not suppport functionality") -) +var ErrNotSupported = errors.New("driver does not suppport functionality") // Driver implements the functionality to store and retrieve blobs // (images,video,audio) diff --git a/internal/text/common.go b/internal/text/common.go index 12c0f1dfa..9ed3fb06f 100644 --- a/internal/text/common.go +++ b/internal/text/common.go @@ -25,9 +25,8 @@ import ( "strings" "unicode" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/regexes" ) @@ -105,7 +104,7 @@ func (f *formatter) ReplaceMentions(ctx context.Context, in string, mentions []* if menchie.TargetAccount == nil { a, err := f.db.GetAccountByID(ctx, menchie.TargetAccountID) if err != nil { - logrus.Errorf("error getting account with id %s from the db: %s", menchie.TargetAccountID, err) + log.Errorf("error getting account with id %s from the db: %s", menchie.TargetAccountID, err) return match } menchie.TargetAccount = a diff --git a/internal/timeline/get.go b/internal/timeline/get.go index a3cc8ef9b..cb6399ec9 100644 --- a/internal/timeline/get.go +++ b/internal/timeline/get.go @@ -24,20 +24,21 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/log" ) const retries = 5 func (t *timeline) Get(ctx context.Context, amount int, maxID string, sinceID string, minID string, prepareNext bool) ([]Preparable, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "Get", - "accountID": t.accountID, - "amount": amount, - "maxID": maxID, - "sinceID": sinceID, - "minID": minID, - }) + l := log.WithFields(kv.Fields{ + + {"accountID", t.accountID}, + {"amount", amount}, + {"maxID", maxID}, + {"sinceID", sinceID}, + {"minID", minID}, + }...) l.Debug("entering get") var items []Preparable @@ -136,12 +137,12 @@ func (t *timeline) GetXFromTop(ctx context.Context, amount int) ([]Preparable, e } func (t *timeline) GetXBehindID(ctx context.Context, amount int, behindID string, attempts *int) ([]Preparable, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "GetXBehindID", - "amount": amount, - "behindID": behindID, - "attempts": *attempts, - }) + l := log.WithFields(kv.Fields{ + + {"amount", amount}, + {"behindID", behindID}, + {"attempts", attempts}, + }...) newAttempts := *attempts newAttempts++ diff --git a/internal/timeline/index.go b/internal/timeline/index.go index bda3a9c6c..76c16977b 100644 --- a/internal/timeline/index.go +++ b/internal/timeline/index.go @@ -24,14 +24,15 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (t *timeline) IndexBefore(ctx context.Context, itemID string, amount int) error { - l := logrus.WithFields(logrus.Fields{ - "func": "IndexBefore", - "amount": amount, - }) + l := log.WithFields(kv.Fields{ + + {"amount", amount}, + }...) // lazily initialize index if it hasn't been done already if t.itemIndex.data == nil { @@ -81,10 +82,10 @@ grabloop: } func (t *timeline) IndexBehind(ctx context.Context, itemID string, amount int) error { - l := logrus.WithFields(logrus.Fields{ - "func": "IndexBehind", - "amount": amount, - }) + l := log.WithFields(kv.Fields{ + + {"amount", amount}, + }...) // lazily initialize index if it hasn't been done already if t.itemIndex.data == nil { diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go index 02a388aba..51bd65fbf 100644 --- a/internal/timeline/manager.go +++ b/internal/timeline/manager.go @@ -24,7 +24,8 @@ import ( "strings" "sync" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/log" ) const ( @@ -98,11 +99,11 @@ type manager struct { } func (m *manager) Ingest(ctx context.Context, item Timelineable, timelineAccountID string) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "Ingest", - "timelineAccountID": timelineAccountID, - "itemID": item.GetID(), - }) + l := log.WithFields(kv.Fields{ + + {"timelineAccountID", timelineAccountID}, + {"itemID", item.GetID()}, + }...) t, err := m.getOrCreateTimeline(ctx, timelineAccountID) if err != nil { @@ -114,11 +115,11 @@ func (m *manager) Ingest(ctx context.Context, item Timelineable, timelineAccount } func (m *manager) IngestAndPrepare(ctx context.Context, item Timelineable, timelineAccountID string) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "IngestAndPrepare", - "timelineAccountID": timelineAccountID, - "itemID": item.GetID(), - }) + l := log.WithFields(kv.Fields{ + + {"timelineAccountID", timelineAccountID}, + {"itemID", item.GetID()}, + }...) t, err := m.getOrCreateTimeline(ctx, timelineAccountID) if err != nil { @@ -130,11 +131,11 @@ func (m *manager) IngestAndPrepare(ctx context.Context, item Timelineable, timel } func (m *manager) Remove(ctx context.Context, timelineAccountID string, itemID string) (int, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "Remove", - "timelineAccountID": timelineAccountID, - "itemID": itemID, - }) + l := log.WithFields(kv.Fields{ + + {"timelineAccountID", timelineAccountID}, + {"itemID", itemID}, + }...) t, err := m.getOrCreateTimeline(ctx, timelineAccountID) if err != nil { @@ -146,10 +147,10 @@ func (m *manager) Remove(ctx context.Context, timelineAccountID string, itemID s } func (m *manager) GetTimeline(ctx context.Context, timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]Preparable, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "GetTimeline", - "timelineAccountID": timelineAccountID, - }) + l := log.WithFields(kv.Fields{ + + {"timelineAccountID", timelineAccountID}, + }...) t, err := m.getOrCreateTimeline(ctx, timelineAccountID) if err != nil { diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go index dae9031e5..c643d1873 100644 --- a/internal/timeline/prepare.go +++ b/internal/timeline/prepare.go @@ -24,18 +24,19 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (t *timeline) prepareNextQuery(ctx context.Context, amount int, maxID string, sinceID string, minID string) error { - l := logrus.WithFields(logrus.Fields{ - "func": "prepareNextQuery", - "amount": amount, - "maxID": maxID, - "sinceID": sinceID, - "minID": minID, - }) + l := log.WithFields(kv.Fields{ + + {"amount", amount}, + {"maxID", maxID}, + {"sinceID", sinceID}, + {"minID", minID}, + }...) var err error @@ -169,10 +170,10 @@ prepareloop: } func (t *timeline) PrepareFromTop(ctx context.Context, amount int) error { - l := logrus.WithFields(logrus.Fields{ - "func": "PrepareFromTop", - "amount": amount, - }) + l := log.WithFields(kv.Fields{ + + {"amount", amount}, + }...) // lazily initialize prepared posts if it hasn't been done already if t.preparedItems.data == nil { diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go index 60d8108ec..1e70e28a7 100644 --- a/internal/timeline/remove.go +++ b/internal/timeline/remove.go @@ -23,15 +23,17 @@ import ( "context" "errors" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "Remove", - "accountTimeline": t.accountID, - "statusID": statusID, - }) + l := log.WithFields(kv.Fields{ + + {"accountTimeline", t.accountID}, + {"statusID", statusID}, + }...) + t.Lock() defer t.Unlock() var removed int @@ -79,11 +81,11 @@ func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) { } func (t *timeline) RemoveAllBy(ctx context.Context, accountID string) (int, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "RemoveAllBy", - "accountTimeline": t.accountID, - "accountID": accountID, - }) + l := log.WithFields(kv.Fields{ + + {"accountTimeline", t.accountID}, + {"accountID", accountID}, + }...) t.Lock() defer t.Unlock() var removed int diff --git a/internal/trans/import.go b/internal/trans/import.go index f4f3eb8d4..5307f5f75 100644 --- a/internal/trans/import.go +++ b/internal/trans/import.go @@ -26,8 +26,7 @@ import ( "io" "os" - "github.com/sirupsen/logrus" - + "github.com/superseriousbusiness/gotosocial/internal/log" transmodel "github.com/superseriousbusiness/gotosocial/internal/trans/model" ) @@ -49,7 +48,7 @@ func (i *importer) Import(ctx context.Context, path string) error { err := decoder.Decode(&entry) if err != nil { if err == io.EOF { - logrus.Infof("Import: reached end of file") + log.Infof("Import: reached end of file") return neatClose(file) } return fmt.Errorf("Import: error decoding in readLoop: %s", err) @@ -75,7 +74,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, account); err != nil { return fmt.Errorf("inputEntry: error adding account to database: %s", err) } - logrus.Infof("inputEntry: added account with id %s", account.ID) + log.Infof("inputEntry: added account with id %s", account.ID) return nil case transmodel.TransBlock: block, err := i.blockDecode(entry) @@ -85,7 +84,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, block); err != nil { return fmt.Errorf("inputEntry: error adding block to database: %s", err) } - logrus.Infof("inputEntry: added block with id %s", block.ID) + log.Infof("inputEntry: added block with id %s", block.ID) return nil case transmodel.TransDomainBlock: block, err := i.domainBlockDecode(entry) @@ -95,7 +94,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, block); err != nil { return fmt.Errorf("inputEntry: error adding domain block to database: %s", err) } - logrus.Infof("inputEntry: added domain block with id %s", block.ID) + log.Infof("inputEntry: added domain block with id %s", block.ID) return nil case transmodel.TransFollow: follow, err := i.followDecode(entry) @@ -105,7 +104,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, follow); err != nil { return fmt.Errorf("inputEntry: error adding follow to database: %s", err) } - logrus.Infof("inputEntry: added follow with id %s", follow.ID) + log.Infof("inputEntry: added follow with id %s", follow.ID) return nil case transmodel.TransFollowRequest: fr, err := i.followRequestDecode(entry) @@ -115,7 +114,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, fr); err != nil { return fmt.Errorf("inputEntry: error adding follow request to database: %s", err) } - logrus.Infof("inputEntry: added follow request with id %s", fr.ID) + log.Infof("inputEntry: added follow request with id %s", fr.ID) return nil case transmodel.TransInstance: inst, err := i.instanceDecode(entry) @@ -125,7 +124,7 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, inst); err != nil { return fmt.Errorf("inputEntry: error adding instance to database: %s", err) } - logrus.Infof("inputEntry: added instance with id %s", inst.ID) + log.Infof("inputEntry: added instance with id %s", inst.ID) return nil case transmodel.TransUser: user, err := i.userDecode(entry) @@ -135,11 +134,11 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, user); err != nil { return fmt.Errorf("inputEntry: error adding user to database: %s", err) } - logrus.Infof("inputEntry: added user with id %s", user.ID) + log.Infof("inputEntry: added user with id %s", user.ID) return nil } - logrus.Errorf("inputEntry: didn't recognize transtype '%s', skipping it", t) + log.Errorf("inputEntry: didn't recognize transtype '%s', skipping it", t) return nil } diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 45eb4fd79..255932d5d 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -29,12 +29,12 @@ import ( "codeberg.org/gruf/go-byteutil" "codeberg.org/gruf/go-cache/v2" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Controller generates transports for use in making federation requests to other servers. @@ -73,7 +73,7 @@ func NewController(db db.DB, federatingDB federatingdb.DB, clock pub.Clock, clie // Transport cache has TTL=1hr freq=1m c.cache.SetTTL(time.Hour, false) if !c.cache.Start(time.Minute) { - logrus.Panic("failed to start transport controller cache") + log.Panic("failed to start transport controller cache") } return c diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go index 35b088b88..c43625a0b 100644 --- a/internal/transport/derefinstance.go +++ b/internal/transport/derefinstance.go @@ -28,19 +28,16 @@ import ( "net/url" "strings" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/validate" ) func (t *transport) DereferenceInstance(ctx context.Context, iri *url.URL) (*gtsmodel.Instance, error) { - l := logrus.WithField("func", "DereferenceInstance") - var i *gtsmodel.Instance var err error @@ -48,26 +45,26 @@ func (t *transport) DereferenceInstance(ctx context.Context, iri *url.URL) (*gts // This will provide the most complete picture of an instance, and avoid unnecessary api calls. // // This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial. - l.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host) + log.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host) i, err = dereferenceByAPIV1Instance(ctx, t, iri) if err == nil { - l.Debugf("successfully dereferenced instance using /api/v1/instance") + log.Debugf("successfully dereferenced instance using /api/v1/instance") return i, nil } - l.Debugf("couldn't dereference instance using /api/v1/instance: %s", err) + log.Debugf("couldn't dereference instance using /api/v1/instance: %s", err) // If that doesn't work, try to dereference using /.well-known/nodeinfo. // This will involve two API calls and return less info overall, but should be more widely compatible. - l.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host) + log.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host) i, err = dereferenceByNodeInfo(ctx, t, iri) if err == nil { - l.Debugf("successfully dereferenced instance using /.well-known/nodeinfo") + log.Debugf("successfully dereferenced instance using /.well-known/nodeinfo") return i, nil } - l.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err) + log.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err) // we couldn't dereference the instance using any of the known methods, so just return a minimal representation - l.Debugf("returning minimal representation of instance %s", iri.Host) + log.Debugf("returning minimal representation of instance %s", iri.Host) id, err := id.NewRandomULID() if err != nil { return nil, fmt.Errorf("error creating new id for instance %s: %s", iri.Host, err) diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 22dfbeb9a..80710a519 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -31,11 +31,12 @@ import ( "time" errorsv2 "codeberg.org/gruf/go-errors/v2" + "codeberg.org/gruf/go-kv" "github.com/go-fed/httpsig" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/httpclient" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // Transport wraps the pub.Transport interface with some additional functionality for fetching remote media. @@ -90,11 +91,11 @@ func (t *transport) do(r *http.Request, signer func(*http.Request) error, retryO backoff := time.Second * 2 // Start a log entry for this request - l := logrus.WithFields(logrus.Fields{ - "pubKeyID": t.pubKeyID, - "method": r.Method, - "url": r.URL.String(), - }) + l := log.WithFields(kv.Fields{ + {"pubKeyID", t.pubKeyID}, + {"method", r.Method}, + {"url", r.URL.String()}, + }...) for i := 0; i < maxRetries; i++ { // Reset signing header fields diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index d50310ff8..e30608150 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -23,11 +23,10 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string, update bool) (*gtsmodel.Account, error) { @@ -185,7 +184,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab } status.URI = uriProp.GetIRI().String() - l := logrus.WithField("statusURI", status.URI) + l := log.WithField("statusURI", status.URI) // web url for viewing this status if statusURL, err := ap.ExtractURL(statusable); err == nil { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index a14a3dbab..bb611dd8f 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -25,13 +25,13 @@ import ( "fmt" "net/url" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // const ( @@ -219,7 +219,7 @@ func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab if err == nil { a.AvatarMediaAttachment = avatar } else { - logrus.Errorf("AccountToAS: error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err) + log.Errorf("AccountToAS: error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err) } } @@ -253,7 +253,7 @@ func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab if err == nil { a.HeaderMediaAttachment = header } else { - logrus.Errorf("AccountToAS: error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err) + log.Errorf("AccountToAS: error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err) } } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 138b85553..1ac49688a 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -23,12 +23,11 @@ import ( "fmt" "strings" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -105,7 +104,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A if err == nil { a.AvatarMediaAttachment = avi } else { - logrus.Errorf("AccountToAPIAccountPublic: error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err) + log.Errorf("AccountToAPIAccountPublic: error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err) } } if a.AvatarMediaAttachment != nil { @@ -123,7 +122,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A if err == nil { a.HeaderMediaAttachment = avi } else { - logrus.Errorf("AccountToAPIAccountPublic: error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err) + log.Errorf("AccountToAPIAccountPublic: error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err) } } if a.HeaderMediaAttachment != nil { @@ -388,7 +387,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, gtsAttachment := range s.Attachments { apiAttachment, err := c.AttachmentToAPIAttachment(ctx, gtsAttachment) if err != nil { - logrus.Errorf("error converting attachment with id %s: %s", gtsAttachment.ID, err) + log.Errorf("error converting attachment with id %s: %s", gtsAttachment.ID, err) continue } apiAttachments = append(apiAttachments, apiAttachment) @@ -399,12 +398,12 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, aID := range s.AttachmentIDs { gtsAttachment, err := c.db.GetAttachmentByID(ctx, aID) if err != nil { - logrus.Errorf("error getting attachment with id %s: %s", aID, err) + log.Errorf("error getting attachment with id %s: %s", aID, err) continue } apiAttachment, err := c.AttachmentToAPIAttachment(ctx, gtsAttachment) if err != nil { - logrus.Errorf("error converting attachment with id %s: %s", aID, err) + log.Errorf("error converting attachment with id %s: %s", aID, err) continue } apiAttachments = append(apiAttachments, apiAttachment) @@ -418,7 +417,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, gtsMention := range s.Mentions { apiMention, err := c.MentionToAPIMention(ctx, gtsMention) if err != nil { - logrus.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) + log.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) continue } apiMentions = append(apiMentions, apiMention) @@ -429,12 +428,12 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, mID := range s.MentionIDs { gtsMention, err := c.db.GetMention(ctx, mID) if err != nil { - logrus.Errorf("error getting mention with id %s: %s", mID, err) + log.Errorf("error getting mention with id %s: %s", mID, err) continue } apiMention, err := c.MentionToAPIMention(ctx, gtsMention) if err != nil { - logrus.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) + log.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) continue } apiMentions = append(apiMentions, apiMention) @@ -448,7 +447,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, gtsTag := range s.Tags { apiTag, err := c.TagToAPITag(ctx, gtsTag) if err != nil { - logrus.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) + log.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) continue } apiTags = append(apiTags, apiTag) @@ -459,12 +458,12 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, t := range s.TagIDs { gtsTag := >smodel.Tag{} if err := c.db.GetByID(ctx, t, gtsTag); err != nil { - logrus.Errorf("error getting tag with id %s: %s", t, err) + log.Errorf("error getting tag with id %s: %s", t, err) continue } apiTag, err := c.TagToAPITag(ctx, gtsTag) if err != nil { - logrus.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) + log.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) continue } apiTags = append(apiTags, apiTag) @@ -478,7 +477,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, gtsEmoji := range s.Emojis { apiEmoji, err := c.EmojiToAPIEmoji(ctx, gtsEmoji) if err != nil { - logrus.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) + log.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) continue } apiEmojis = append(apiEmojis, apiEmoji) @@ -489,12 +488,12 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r for _, e := range s.EmojiIDs { gtsEmoji := >smodel.Emoji{} if err := c.db.GetByID(ctx, e, gtsEmoji); err != nil { - logrus.Errorf("error getting emoji with id %s: %s", e, err) + log.Errorf("error getting emoji with id %s: %s", e, err) continue } apiEmoji, err := c.EmojiToAPIEmoji(ctx, gtsEmoji) if err != nil { - logrus.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) + log.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) continue } apiEmojis = append(apiEmojis, apiEmoji) diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go index 81e8e53de..c81bf14f1 100644 --- a/internal/typeutils/wrap.go +++ b/internal/typeutils/wrap.go @@ -14,7 +14,6 @@ import ( ) func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, originAccount *gtsmodel.Account) (vocab.ActivityStreamsUpdate, error) { - update := streams.NewActivityStreamsUpdate() // set the actor diff --git a/internal/validate/account_test.go b/internal/validate/account_test.go index 0aa4a5a9d..a023b2bcb 100644 --- a/internal/validate/account_test.go +++ b/internal/validate/account_test.go @@ -31,7 +31,6 @@ import ( ) func happyAccount() *gtsmodel.Account { - priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) diff --git a/internal/visibility/statusboostable.go b/internal/visibility/statusboostable.go index 9eed9e3e9..60699875d 100644 --- a/internal/visibility/statusboostable.go +++ b/internal/visibility/statusboostable.go @@ -23,15 +23,11 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (f *filter) StatusBoostable(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusBoostable", - }) - // if the status isn't visible, it certainly isn't boostable visible, err := f.StatusVisible(ctx, targetStatus, requestingAccount) if err != nil { @@ -43,23 +39,23 @@ func (f *filter) StatusBoostable(ctx context.Context, targetStatus *gtsmodel.Sta // direct messages are never boostable, even if they're visible if targetStatus.Visibility == gtsmodel.VisibilityDirect { - l.Trace("status is not boostable because it is a DM") + log.Trace("status is not boostable because it is a DM") return false, nil } // the original account should always be able to boost its own non-DM statuses if requestingAccount.ID == targetStatus.Account.ID { - l.Trace("status is boostable because author is booster") + log.Trace("status is boostable because author is booster") return true, nil } // if status is followers-only and not the author's, it is not boostable if targetStatus.Visibility == gtsmodel.VisibilityFollowersOnly { - l.Trace("status not boostable because it is followers-only") + log.Trace("status not boostable because it is followers-only") return false, nil } // otherwise, status is as boostable as it says it is - l.Trace("defaulting to status.boostable value") + log.Trace("defaulting to status.boostable value") return targetStatus.Boostable, nil } diff --git a/internal/visibility/statushometimelineable.go b/internal/visibility/statushometimelineable.go index af871bcaa..bd3c90b4d 100644 --- a/internal/visibility/statushometimelineable.go +++ b/internal/visibility/statushometimelineable.go @@ -22,15 +22,16 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (f *filter) StatusHometimelineable(ctx context.Context, targetStatus *gtsmodel.Status, timelineOwnerAccount *gtsmodel.Account) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusHometimelineable", - "statusID": targetStatus.ID, - }) + l := log.WithFields(kv.Fields{ + + {"statusID", targetStatus.ID}, + }...) // status owner should always be able to see their own status in their timeline so we can return early if this is the case if targetStatus.AccountID == timelineOwnerAccount.ID { diff --git a/internal/visibility/statuspublictimelineable.go b/internal/visibility/statuspublictimelineable.go index 9991a9c3a..848842225 100644 --- a/internal/visibility/statuspublictimelineable.go +++ b/internal/visibility/statuspublictimelineable.go @@ -22,15 +22,16 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (f *filter) StatusPublictimelineable(ctx context.Context, targetStatus *gtsmodel.Status, timelineOwnerAccount *gtsmodel.Account) (bool, error) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusPublictimelineable", - "statusID": targetStatus.ID, - }) + l := log.WithFields(kv.Fields{ + + {"statusID", targetStatus.ID}, + }...) // Don't timeline boosted statuses if targetStatus.BoostOfID != "" { diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go index 3d179eccd..fc8bfeaaa 100644 --- a/internal/visibility/statusvisible.go +++ b/internal/visibility/statusvisible.go @@ -22,18 +22,19 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) func (f *filter) StatusVisible(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) { const getBoosted = true - l := logrus.WithFields(logrus.Fields{ - "func": "StatusVisible", - "statusID": targetStatus.ID, - }) + l := log.WithFields(kv.Fields{ + + {"statusID", targetStatus.ID}, + }...) // Fetch any relevant accounts for the target status relevantAccounts, err := f.relevantAccounts(ctx, targetStatus, getBoosted) diff --git a/internal/web/assetscache.go b/internal/web/assetscache.go index 2bde7c499..57a0ade0b 100644 --- a/internal/web/assetscache.go +++ b/internal/web/assetscache.go @@ -30,7 +30,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/log" ) type eTagCacheEntry struct { @@ -119,7 +119,7 @@ func (m *Module) cacheControlMiddleware(fs http.FileSystem) gin.HandlerFunc { // either fetch etag from ttlcache or generate it eTag, err := m.getAssetETag(assetFilePath, fs) if err != nil { - logrus.Errorf("error getting ETag for %s: %s", assetFilePath, err) + log.Errorf("error getting ETag for %s: %s", assetFilePath, err) return } diff --git a/scripts/build.sh b/scripts/build.sh index a71a0899a..dde199549 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,6 +6,6 @@ set -eu DEBUG() { [ ! -z "${DEBUG-}" ]; } CGO_ENABLED=0 go build -trimpath \ - -tags "netgo osusergo static_build $(DEBUG && echo 'debugenv')" \ + -tags "netgo osusergo static_build kvformat $(DEBUG && echo 'debugenv')" \ -ldflags="-s -w -extldflags '-static' -X 'main.Version=${VERSION:-$(git describe --tags --abbrev=0)}'" \ ./cmd/gotosocial diff --git a/scripts/test.sh b/scripts/test.sh index befc82adf..5e9271821 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,7 +7,7 @@ set -e # "./..." = all tests # run tests with sqlite in-memory database -GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test -count 1 ./... +GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test -tags "netgo osusergo static_build kvformat" -count 1 ./... # run tests with postgres database at either GTS_DB_ADDRESS or default localhost -GTS_DB_TYPE="postgres" GTS_DB_ADDRESS="${GTS_DB_ADDRESS:-localhost}" go test -count 1 -p 1 ./... +GTS_DB_TYPE="postgres" GTS_DB_ADDRESS="${GTS_DB_ADDRESS:-localhost}" go test -tags "netgo osusergo static_build kvformat" -count 1 -p 1 ./... diff --git a/testrig/db.go b/testrig/db.go index b4c05d727..ae3132835 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -23,11 +23,11 @@ import ( "os" "strconv" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/bundb" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) var testModels = []interface{}{ @@ -91,7 +91,7 @@ func NewTestDB() db.DB { testDB, err := bundb.NewBunDBService(context.Background()) if err != nil { - logrus.Panic(err) + log.Panic(err) } return testDB } @@ -101,7 +101,7 @@ func CreateTestTables(db db.DB) { ctx := context.Background() for _, m := range testModels { if err := db.CreateTable(ctx, m); err != nil { - logrus.Panicf("error creating table for %+v: %s", m, err) + log.Panicf("error creating table for %+v: %s", m, err) } } } @@ -116,7 +116,7 @@ func CreateTestTables(db db.DB) { // verification will fail. func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { if db == nil { - logrus.Panic("db setup: db was nil") + log.Panic("db setup: db was nil") } CreateTestTables(db) @@ -125,128 +125,128 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { for _, v := range NewTestTokens() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestClients() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestApplications() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestBlocks() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestDomainBlocks() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestInstances() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestUsers() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } if accounts == nil { for _, v := range NewTestAccounts() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } } else { for _, v := range accounts { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } } for _, v := range NewTestAttachments() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestStatuses() { if err := db.PutStatus(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestEmojis() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestTags() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestMentions() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestFaves() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestFollows() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } for _, v := range NewTestNotifications() { if err := db.Put(ctx, v); err != nil { - logrus.Panic(err) + log.Panic(err) } } if err := db.CreateInstanceAccount(ctx); err != nil { - logrus.Panic(err) + log.Panic(err) } if err := db.CreateInstanceInstance(ctx); err != nil { - logrus.Panic(err) + log.Panic(err) } - logrus.Debug("testing db setup complete") + log.Debug("testing db setup complete") } // StandardDBTeardown drops all the standard testing tables/models from the database to ensure it's clean for the next test. func StandardDBTeardown(db db.DB) { ctx := context.Background() if db == nil { - logrus.Panic("db teardown: db was nil") + log.Panic("db teardown: db was nil") } for _, m := range testModels { if err := db.DropTable(ctx, m); err != nil { - logrus.Panic(err) + log.Panic(err) } } } diff --git a/testrig/log.go b/testrig/log.go index 1dbba83b5..48e335297 100644 --- a/testrig/log.go +++ b/testrig/log.go @@ -19,6 +19,7 @@ package testrig import ( + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/log" "gopkg.in/mcuadros/go-syslog.v2" "gopkg.in/mcuadros/go-syslog.v2/format" @@ -26,8 +27,19 @@ import ( // InitTestLog sets the global logger to trace level for logging func InitTestLog() { - if err := log.Initialize(); err != nil { - panic(err) + // Set the global log level from configuration + if err := log.ParseLevel(config.GetLogLevel()); err != nil { + log.Panicf("error parsing log level: %v", err) + } + + if config.GetSyslogEnabled() { + // Enable logging to syslog + if err := log.EnableSyslog( + config.GetSyslogProtocol(), + config.GetSyslogAddress(), + ); err != nil { + log.Panicf("error enabling syslogging: %v", err) + } } } diff --git a/testrig/transportcontroller.go b/testrig/transportcontroller.go index 48463c1df..6edf2b52e 100644 --- a/testrig/transportcontroller.go +++ b/testrig/transportcontroller.go @@ -25,7 +25,6 @@ import ( "net/http" "strings" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" @@ -33,12 +32,15 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/transport" ) -const applicationJSON = "application/json" -const applicationActivityJSON = "application/activity+json" +const ( + applicationJSON = "application/json" + applicationActivityJSON = "application/activity+json" +) // NewTestTransportController returns a test transport controller with the given http client. // @@ -171,7 +173,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat responseContentLength = len(attachment.Data) } - logrus.Debugf("returning response %s", string(responseBytes)) + log.Debugf("returning response %s", string(responseBytes)) reader := bytes.NewReader(responseBytes) readCloser := io.NopCloser(reader) return &http.Response{ @@ -264,7 +266,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt } if wfr == nil { - logrus.Debugf("webfinger response not available for %s", req.URL) + log.Debugf("webfinger response not available for %s", req.URL) responseCode = http.StatusNotFound responseBytes = []byte(`{"error":"not found"}`) responseContentType = applicationJSON diff --git a/vendor/codeberg.org/gruf/go-kv/LICENSE b/vendor/codeberg.org/gruf/go-kv/LICENSE new file mode 100644 index 000000000..b7c4417ac --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2021 gruf + +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. diff --git a/vendor/codeberg.org/gruf/go-kv/README.md b/vendor/codeberg.org/gruf/go-kv/README.md new file mode 100644 index 000000000..fa4735868 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/README.md @@ -0,0 +1,9 @@ +# go-kv + +This library provides a key-value field structure `kv.Field{}` that plays well with the `"fmt"` package. It gives an easy means of appending key-value fields to log entries, in a manner that also happens to look nice! (it's not far removed from using a `map[string]interface{}`). + +The formatting for these key-value fields is handled by the `"fmt"` package by default. If you set the `kvformat` build tag then it will use a custom formatting library found under `format/`. You can see the benchmarks for both below. + +![benchmarks](https://codeberg.org/gruf/go-kv/raw/main/benchmark.png) + +TODO: benchmarks comparing these to using `fmt.Sprintf("%q=%+v", ...)` yourself. \ No newline at end of file diff --git a/vendor/codeberg.org/gruf/go-kv/benchmark.png b/vendor/codeberg.org/gruf/go-kv/benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..08d838643fcf50db5ff830fc1bf9f8bf5f8c6837 GIT binary patch literal 97592 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 0 { + buf.Truncate(1) + } +} + +// String returns a string representation of receiving Field(s). +func (f Fields) String() string { + b := make([]byte, 0, bufsize*len(f)) + buf := byteutil.Buffer{B: b} + f.AppendFormat(&buf, false) + return buf.String() +} + +// GoString performs .String() but with type prefix. +func (f Fields) GoString() string { + b := make([]byte, 0, bufsize*len(f)) + buf := byteutil.Buffer{B: b} + f.AppendFormat(&buf, true) + return "kv.Fields{" + buf.String() + "}" +} + +// Field represents an individual key-value field. +type Field struct { + K string // Field key + V interface{} // Field value +} + +// Key returns the formatted key string of this Field. +func (f Field) Key() string { + buf := byteutil.Buffer{B: make([]byte, 0, bufsize/2)} + appendQuoteKey(&buf, f.K) + return buf.String() +} + +// String will return a string representation of this Field +// of the form `key=value` where `value` is formatted using +// fmt package's `%+v` directive. If the .X = true (verbose), +// then it uses '%#v'. Both key and value are escaped and +// quoted if necessary to fit on single line. +// +// If the `kvformat` build tag is provided, the formatting +// will be performed by the `kv/format` package. In this case +// the value will be formatted using the `{:v}` directive, or +// `{:?}` if .X = true (verbose). +func (f Field) String() string { + b := make([]byte, 0, bufsize) + buf := byteutil.Buffer{B: b} + f.AppendFormat(&buf, false) + return buf.String() +} + +// GoString performs .String() but with verbose always enabled. +func (f Field) GoString() string { + b := make([]byte, 0, bufsize) + buf := byteutil.Buffer{B: b} + f.AppendFormat(&buf, true) + return buf.String() +} diff --git a/vendor/codeberg.org/gruf/go-kv/field_fmt.go b/vendor/codeberg.org/gruf/go-kv/field_fmt.go new file mode 100644 index 000000000..c62393fe0 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/field_fmt.go @@ -0,0 +1,36 @@ +//go:build !kvformat +// +build !kvformat + +package kv + +import ( + "fmt" + + "codeberg.org/gruf/go-byteutil" +) + +// AppendFormat will append formatted format of Field to 'buf'. See .String() for details. +func (f Field) AppendFormat(buf *byteutil.Buffer, vbose bool) { + var fmtstr string + if vbose /* verbose */ { + fmtstr = `%#v` + } else /* regular */ { + fmtstr = `%+v` + } + appendQuoteKey(buf, f.K) + buf.WriteByte('=') + appendQuoteValue(buf, fmt.Sprintf(fmtstr, f.V)) +} + +// Value returns the formatted value string of this Field. +func (f Field) Value(vbose bool) string { + var fmtstr string + if vbose /* verbose */ { + fmtstr = `%#v` + } else /* regular */ { + fmtstr = `%+v` + } + buf := byteutil.Buffer{B: make([]byte, 0, bufsize/2)} + appendQuoteValue(&buf, fmt.Sprintf(fmtstr, f.V)) + return buf.String() +} diff --git a/vendor/codeberg.org/gruf/go-kv/field_format.go b/vendor/codeberg.org/gruf/go-kv/field_format.go new file mode 100644 index 000000000..db1ffc721 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/field_format.go @@ -0,0 +1,35 @@ +//go:build kvformat +// +build kvformat + +package kv + +import ( + "codeberg.org/gruf/go-byteutil" + "codeberg.org/gruf/go-kv/format" +) + +// AppendFormat will append formatted format of Field to 'buf'. See .String() for details. +func (f Field) AppendFormat(buf *byteutil.Buffer, vbose bool) { + var fmtstr string + if vbose /* verbose */ { + fmtstr = "{:?}" + } else /* regular */ { + fmtstr = "{:v}" + } + appendQuoteKey(buf, f.K) + buf.WriteByte('=') + format.Appendf(buf, fmtstr, f.V) +} + +// Value returns the formatted value string of this Field. +func (f Field) Value(vbose bool) string { + var fmtstr string + if vbose /* verbose */ { + fmtstr = "{:?}" + } else /* regular */ { + fmtstr = "{:v}" + } + buf := byteutil.Buffer{B: make([]byte, 0, bufsize/2)} + format.Appendf(&buf, fmtstr, f.V) + return buf.String() +} diff --git a/vendor/codeberg.org/gruf/go-kv/format/README.md b/vendor/codeberg.org/gruf/go-kv/format/README.md new file mode 100644 index 000000000..fe7b2338d --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/format/README.md @@ -0,0 +1,5 @@ +String formatting package using Rust-style formatting directives, geared specifcally towards formatting arguments for key-value log output. Provides ONLY default, key, value and verbose formatting directives. For most formatting cases you will be best served by `"fmt"`. + +Generally more visually friendly than `"fmt"` and performance is much improved. + +![benchmarks](https://codeberg.org/gruf/go-kv/raw/main/format/benchmark.png) \ No newline at end of file diff --git a/vendor/codeberg.org/gruf/go-kv/format/benchmark.png b/vendor/codeberg.org/gruf/go-kv/format/benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..7151fbc557c63c3a17e8942aee0a3af893fbd1bf GIT binary patch literal 36505 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 NX|tyWPg(Z+{{j+mit7LX literal 0 HcmV?d00001 diff --git a/vendor/codeberg.org/gruf/go-kv/format/format.go b/vendor/codeberg.org/gruf/go-kv/format/format.go new file mode 100644 index 000000000..df5a94b7c --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/format/format.go @@ -0,0 +1,922 @@ +package format + +import ( + "reflect" + "strconv" + "unicode/utf8" + + "codeberg.org/gruf/go-byteutil" +) + +const ( + // Flag bit constants, note they are prioritised in this order. + IsKeyBit = uint8(1) << 0 // set to indicate key formatting + VboseBit = uint8(1) << 1 // set to indicate verbose formatting + IsValBit = uint8(1) << 2 // set to indicate value formatting + PanicBit = uint8(1) << 3 // set after panic to prevent recursion +) + +// format provides formatting of values into a Buffer. +type format struct { + // Flags are the currently set value flags. + Flags uint8 + + // Derefs is the current value dereference count. + Derefs uint8 + + // CurDepth is the current Format iterator depth. + CurDepth uint8 + + // VType is the current value type. + VType string + + // Config is the set Formatter config (MUST NOT be nil). + Config *Formatter + + // Buffer is the currently set output buffer. + Buffer *byteutil.Buffer +} + +// AtMaxDepth returns whether format is currently at max depth. +func (f format) AtMaxDepth() bool { + return f.CurDepth > f.Config.MaxDepth +} + +// Key returns whether the isKey flag is set. +func (f format) Key() bool { + return (f.Flags & IsKeyBit) != 0 +} + +// Value returns whether the isVal flag is set. +func (f format) Value() bool { + return (f.Flags & IsValBit) != 0 +} + +// Verbose returns whether the verbose flag is set. +func (f format) Verbose() bool { + return (f.Flags & VboseBit) != 0 +} + +// Panic returns whether the panic flag is set. +func (f format) Panic() bool { + return (f.Flags & PanicBit) != 0 +} + +// SetKey returns format instance with the IsKey bit set to true, +// note this resets the dereference count. +func (f format) SetKey() format { + flags := f.Flags | IsKeyBit + flags &= ^IsValBit + return format{ + Flags: flags, + CurDepth: f.CurDepth, + Config: f.Config, + Buffer: f.Buffer, + } +} + +// SetValue returns format instance with the IsVal bit set to true, +// note this resets the dereference count. +func (f format) SetValue() format { + flags := f.Flags | IsValBit + flags &= ^IsKeyBit + return format{ + Flags: flags, + CurDepth: f.CurDepth, + Config: f.Config, + Buffer: f.Buffer, + } +} + +// SetVerbose returns format instance with the Vbose bit set to true, +// note this resets the dereference count. +func (f format) SetVerbose() format { + return format{ + Flags: f.Flags | VboseBit, + CurDepth: f.CurDepth, + Config: f.Config, + Buffer: f.Buffer, + } +} + +// SetPanic returns format instance with the panic bit set to true, +// note this resets the dereference count and sets IsVal (unsetting IsKey) bit. +func (f format) SetPanic() format { + flags := f.Flags | PanicBit + flags |= IsValBit + flags &= ^IsKeyBit + return format{ + Flags: flags, + CurDepth: f.CurDepth, + Config: f.Config, + Buffer: f.Buffer, + } +} + +// IncrDepth returns format instance with depth incremented and derefs reset. +func (f format) IncrDepth() format { + return format{ + Flags: f.Flags, + Derefs: f.Derefs, + CurDepth: f.CurDepth + 1, + Config: f.Config, + Buffer: f.Buffer, + } +} + +// IncrDerefs returns format instance with dereference count incremented. +func (f format) IncrDerefs() format { + return format{ + Flags: f.Flags, + Derefs: f.Derefs + 1, + CurDepth: f.CurDepth, + Config: f.Config, + Buffer: f.Buffer, + } +} + +func (f format) AppendType() { + const derefs = `********************************` + + `********************************` + + `********************************` + + `********************************` + + `********************************` + + `********************************` + + `********************************` + + `********************************` + f.Buffer.B = append(f.Buffer.B, derefs[:f.Derefs]...) + f.Buffer.B = append(f.Buffer.B, f.VType...) +} + +func (f format) AppendNil() { + if !f.Verbose() { + f.Buffer.B = append(f.Buffer.B, `nil`...) + return + } + + // Append nil with type + f.Buffer.B = append(f.Buffer.B, '(') + f.AppendType() + f.Buffer.B = append(f.Buffer.B, `)(nil`...) + f.Buffer.B = append(f.Buffer.B, ')') +} + +func (f format) AppendByte(b byte) { + switch { + // Always quoted + case f.Key(): + f.Buffer.B = append(f.Buffer.B, '\'') + f.Buffer.B = append(f.Buffer.B, byte2str(b)...) + f.Buffer.B = append(f.Buffer.B, '\'') + + // Always quoted ASCII with type + case f.Verbose(): + f._AppendPrimitiveTyped(func(f format) { + f.Buffer.B = append(f.Buffer.B, '\'') + f.Buffer.B = append(f.Buffer.B, byte2str(b)...) + f.Buffer.B = append(f.Buffer.B, '\'') + }) + + // Always quoted + case f.Value(): + f.Buffer.B = append(f.Buffer.B, '\'') + f.Buffer.B = append(f.Buffer.B, byte2str(b)...) + f.Buffer.B = append(f.Buffer.B, '\'') + + // Append as raw byte + default: + f.Buffer.B = append(f.Buffer.B, b) + } +} + +func (f format) AppendBytes(b []byte) { + switch { + // Bytes CAN be nil formatted + case b == nil: + f.AppendNil() + + // Handle bytes as string key + case f.Key(): + f.AppendStringKey(b2s(b)) + + // Append as separate ASCII quoted bytes in slice + case f.Verbose(): + f._AppendArrayTyped(func(f format) { + for i := 0; i < len(b); i++ { + f.Buffer.B = append(f.Buffer.B, '\'') + f.Buffer.B = append(f.Buffer.B, byte2str(b[i])...) + f.Buffer.B = append(f.Buffer.B, `',`...) + } + if len(b) > 0 { + f.Buffer.Truncate(1) + } + }) + + // Append as quoted string + case f.Value(): + f.AppendStringQuoted(b2s(b)) + + // Append as raw bytes + default: + f.Buffer.B = append(f.Buffer.B, b...) + } +} + +func (f format) AppendRune(r rune) { + switch { + // Quoted only if spaces/requires escaping + case f.Key(): + f.AppendRuneKey(r) + + // Always quoted ASCII with type + case f.Verbose(): + f._AppendPrimitiveTyped(func(f format) { + f.Buffer.B = strconv.AppendQuoteRuneToASCII(f.Buffer.B, r) + }) + + // Always quoted value + case f.Value(): + f.Buffer.B = strconv.AppendQuoteRune(f.Buffer.B, r) + + // Append as raw rune + default: + f.Buffer.WriteRune(r) + } +} + +func (f format) AppendRuneKey(r rune) { + if utf8.RuneLen(r) > 1 && (r < ' ' && r != '\t') || r == '`' || r == '\u007F' { + // Quote and escape this rune + f.Buffer.B = strconv.AppendQuoteRuneToASCII(f.Buffer.B, r) + } else { + // Simply append rune + f.Buffer.WriteRune(r) + } +} + +func (f format) AppendRunes(r []rune) { + switch { + // Runes CAN be nil formatted + case r == nil: + f.AppendNil() + + // Handle bytes as string key + case f.Key(): + f.AppendStringKey(string(r)) + + // Append as separate ASCII quoted bytes in slice + case f.Verbose(): + f._AppendArrayTyped(func(f format) { + for i := 0; i < len(r); i++ { + f.Buffer.B = strconv.AppendQuoteRuneToASCII(f.Buffer.B, r[i]) + f.Buffer.B = append(f.Buffer.B, ',') + } + if len(r) > 0 { + f.Buffer.Truncate(1) + } + }) + + // Append as quoted string + case f.Value(): + f.AppendStringQuoted(string(r)) + + // Append as raw bytes + default: + for i := 0; i < len(r); i++ { + f.Buffer.WriteRune(r[i]) + } + } +} + +func (f format) AppendString(s string) { + switch { + // Quoted only if spaces/requires escaping + case f.Key(): + f.AppendStringKey(s) + + // Always quoted with type + case f.Verbose(): + f._AppendPrimitiveTyped(func(f format) { + f.AppendStringQuoted(s) + }) + + // Always quoted string + case f.Value(): + f.AppendStringQuoted(s) + + // All else + default: + f.Buffer.B = append(f.Buffer.B, s...) + } +} + +func (f format) AppendStringKey(s string) { + if !strconv.CanBackquote(s) { + // Requires quoting AND escaping + f.Buffer.B = strconv.AppendQuote(f.Buffer.B, s) + } else if ContainsDoubleQuote(s) { + // Contains double quotes, needs escaping + f.Buffer.B = AppendEscape(f.Buffer.B, s) + } else if len(s) < 1 || ContainsSpaceOrTab(s) { + // Contains space, needs quotes + f.Buffer.B = append(f.Buffer.B, '"') + f.Buffer.B = append(f.Buffer.B, s...) + f.Buffer.B = append(f.Buffer.B, '"') + } else { + // All else write as-is + f.Buffer.B = append(f.Buffer.B, s...) + } +} + +func (f format) AppendStringQuoted(s string) { + if !strconv.CanBackquote(s) { + // Requires quoting AND escaping + f.Buffer.B = strconv.AppendQuote(f.Buffer.B, s) + } else if ContainsDoubleQuote(s) { + // Contains double quotes, needs escaping + f.Buffer.B = append(f.Buffer.B, '"') + f.Buffer.B = AppendEscape(f.Buffer.B, s) + f.Buffer.B = append(f.Buffer.B, '"') + } else { + // Simply append with quotes + f.Buffer.B = append(f.Buffer.B, '"') + f.Buffer.B = append(f.Buffer.B, s...) + f.Buffer.B = append(f.Buffer.B, '"') + } +} + +func (f format) AppendBool(b bool) { + if f.Verbose() { + // Append as bool with type information + f._AppendPrimitiveTyped(func(f format) { + f.Buffer.B = strconv.AppendBool(f.Buffer.B, b) + }) + } else { + // Simply append as bool + f.Buffer.B = strconv.AppendBool(f.Buffer.B, b) + } +} + +func (f format) AppendInt(i int64) { + f._AppendPrimitiveType(func(f format) { + f.Buffer.B = strconv.AppendInt(f.Buffer.B, i, 10) + }) +} + +func (f format) AppendUint(u uint64) { + f._AppendPrimitiveType(func(f format) { + f.Buffer.B = strconv.AppendUint(f.Buffer.B, u, 10) + }) +} + +func (f format) AppendFloat(l float64) { + f._AppendPrimitiveType(func(f format) { + f.AppendFloatValue(l) + }) +} + +func (f format) AppendFloatValue(l float64) { + f.Buffer.B = strconv.AppendFloat(f.Buffer.B, l, 'f', -1, 64) +} + +func (f format) AppendComplex(c complex128) { + f._AppendPrimitiveType(func(f format) { + f.AppendFloatValue(real(c)) + f.Buffer.B = append(f.Buffer.B, '+') + f.AppendFloatValue(imag(c)) + f.Buffer.B = append(f.Buffer.B, 'i') + }) +} + +func (f format) AppendPtr(u uint64) { + f._AppendPtrType(func(f format) { + if u == 0 { + // Append as nil + f.Buffer.B = append(f.Buffer.B, `nil`...) + } else { + // Append as hex number + f.Buffer.B = append(f.Buffer.B, `0x`...) + f.Buffer.B = strconv.AppendUint(f.Buffer.B, u, 16) + } + }) +} + +func (f format) AppendInterfaceOrReflect(i interface{}) { + if !f.AppendInterface(i) { + // Interface append failed, used reflected value + type + f.AppendReflectValue(reflect.ValueOf(i), reflect.TypeOf(i)) + } +} + +func (f format) AppendInterfaceOrReflectNext(v reflect.Value, t reflect.Type) { + // Check we haven't hit max + if f.AtMaxDepth() { + f.Buffer.B = append(f.Buffer.B, `...`...) + return + } + + // Incr the depth + f = f.IncrDepth() + + // Make actual call + f.AppendReflectOrInterface(v, t) +} + +func (f format) AppendReflectOrInterface(v reflect.Value, t reflect.Type) { + if !v.CanInterface() || + !f.AppendInterface(v.Interface()) { + // Interface append failed, use reflect + f.AppendReflectValue(v, t) + } +} + +func (f format) AppendInterface(i interface{}) bool { + switch i := i.(type) { + // Reflect types + case reflect.Type: + f.AppendReflectType(i) + case reflect.Value: + f.Buffer.B = append(f.Buffer.B, `reflect.Value`...) + f.Buffer.B = append(f.Buffer.B, '(') + f.Buffer.B = append(f.Buffer.B, i.String()...) + f.Buffer.B = append(f.Buffer.B, ')') + + // Bytes, runes and string types + case rune: + f.VType = `int32` + f.AppendRune(i) + case []rune: + f.VType = `[]int32` + f.AppendRunes(i) + case byte: + f.VType = `uint8` + f.AppendByte(i) + case []byte: + f.VType = `[]uint8` + f.AppendBytes(i) + case string: + f.VType = `string` + f.AppendString(i) + + // Int types + case int: + f.VType = `int` + f.AppendInt(int64(i)) + case int8: + f.VType = `int8` + f.AppendInt(int64(i)) + case int16: + f.VType = `int16` + f.AppendInt(int64(i)) + case int64: + f.VType = `int64` + f.AppendInt(int64(i)) + + // Uint types + case uint: + f.VType = `uint` + f.AppendUint(uint64(i)) + case uint16: + f.VType = `uint16` + f.AppendUint(uint64(i)) + case uint32: + f.VType = `uint32` + f.AppendUint(uint64(i)) + case uint64: + f.VType = `uint64` + f.AppendUint(uint64(i)) + + // Float types + case float32: + f.VType = `float32` + f.AppendFloat(float64(i)) + case float64: + f.VType = `float64` + f.AppendFloat(float64(i)) + + // Bool type + case bool: + f.VType = `bool` + f.AppendBool(i) + + // Complex types + case complex64: + f.VType = `complex64` + f.AppendComplex(complex128(i)) + case complex128: + f.VType = `complex128` + f.AppendComplex(complex128(i)) + + // Method types + case error: + return f._AppendMethodType(func() string { + return i.Error() + }, i) + case interface{ String() string }: + return f._AppendMethodType(func() string { + return i.String() + }, i) + + // No quick handler + default: + return false + } + + return true +} + +func (f format) AppendReflectType(t reflect.Type) { + switch f.VType = `reflect.Type`; { + case isNil(t) /* safer nil check */ : + f.AppendNil() + case f.Verbose(): + f.AppendType() + f.Buffer.B = append(f.Buffer.B, '(') + f.Buffer.B = append(f.Buffer.B, t.String()...) + f.Buffer.B = append(f.Buffer.B, ')') + default: + f.Buffer.B = append(f.Buffer.B, t.String()...) + } +} + +func (f format) AppendReflectValue(v reflect.Value, t reflect.Type) { + switch v.Kind() { + // String/byte types + case reflect.String: + f.VType = t.String() + f.AppendString(v.String()) + case reflect.Uint8: + f.VType = t.String() + f.AppendByte(byte(v.Uint())) + case reflect.Int32: + f.VType = t.String() + f.AppendRune(rune(v.Int())) + + // Float tpyes + case reflect.Float32, reflect.Float64: + f.VType = t.String() + f.AppendFloat(v.Float()) + + // Int types + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int64: + f.VType = t.String() + f.AppendInt(v.Int()) + + // Uint types + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + f.VType = t.String() + f.AppendUint(v.Uint()) + + // Complex types + case reflect.Complex64, reflect.Complex128: + f.VType = t.String() + f.AppendComplex(v.Complex()) + + // Bool type + case reflect.Bool: + f.VType = t.String() + f.AppendBool(v.Bool()) + + // Slice and array types + case reflect.Array: + f.AppendArray(v, t) + case reflect.Slice: + f.AppendSlice(v, t) + + // Map types + case reflect.Map: + f.AppendMap(v, t) + + // Struct types + case reflect.Struct: + f.AppendStruct(v, t) + + // Interface type + case reflect.Interface: + if v.IsNil() { + // Append nil ptr type + f.VType = t.String() + f.AppendNil() + } else { + // Append interface + v = v.Elem() + t = v.Type() + f.AppendReflectOrInterface(v, t) + } + + // Deref'able ptr type + case reflect.Ptr: + if v.IsNil() { + // Append nil ptr type + f.VType = t.String() + f.AppendNil() + } else { + // Deref to next level + f = f.IncrDerefs() + v, t = v.Elem(), t.Elem() + f.AppendReflectOrInterface(v, t) + } + + // 'raw' pointer types + case reflect.UnsafePointer, reflect.Func, reflect.Chan: + f.VType = t.String() + f.AppendPtr(uint64(v.Pointer())) + case reflect.Uintptr: + f.VType = t.String() + f.AppendPtr(v.Uint()) + + // Zero reflect value + case reflect.Invalid: + f.Buffer.B = append(f.Buffer.B, `nil`...) + + // All others + default: + f.VType = t.String() + f.AppendType() + } +} + +func (f format) AppendSlice(v reflect.Value, t reflect.Type) { + // Get slice value type + f.VType = t.String() + + if t.Elem().Kind() == reflect.Uint8 { + // This is a byte slice + f.AppendBytes(v.Bytes()) + return + } + + if v.IsNil() { + // Nil slice + f.AppendNil() + return + } + + if f.Verbose() { + // Append array with type information + f._AppendArrayTyped(func(f format) { + f.AppendArrayElems(v, t) + }) + } else { + // Simply append array as elems + f._AppendArray(func(f format) { + f.AppendArrayElems(v, t) + }) + } +} + +func (f format) AppendArray(v reflect.Value, t reflect.Type) { + // Get array value type + f.VType = t.String() + + if f.Verbose() { + // Append array with type information + f._AppendArrayTyped(func(f format) { + f.AppendArrayElems(v, t) + }) + } else { + // Simply append array as elems + f._AppendArray(func(f format) { + f.AppendArrayElems(v, t) + }) + } +} + +func (f format) AppendArrayElems(v reflect.Value, t reflect.Type) { + // Get no. elems + n := v.Len() + + // Get elem type + et := t.Elem() + + // Append values + for i := 0; i < n; i++ { + f.SetValue().AppendInterfaceOrReflectNext(v.Index(i), et) + f.Buffer.B = append(f.Buffer.B, ',') + } + + // Drop last comma + if n > 0 { + f.Buffer.Truncate(1) + } +} + +func (f format) AppendMap(v reflect.Value, t reflect.Type) { + // Get value type + f.VType = t.String() + + if v.IsNil() { + // Nil map -- no fields + f.AppendNil() + return + } + + // Append field formatted map fields + f._AppendFieldType(func(f format) { + f.AppendMapFields(v, t) + }) +} + +func (f format) AppendMapFields(v reflect.Value, t reflect.Type) { + // Get a map iterator + r := v.MapRange() + n := v.Len() + + // Get key/val types + kt := t.Key() + kv := t.Elem() + + // Iterate pairs + for r.Next() { + f.SetKey().AppendInterfaceOrReflectNext(r.Key(), kt) + f.Buffer.B = append(f.Buffer.B, '=') + f.SetValue().AppendInterfaceOrReflectNext(r.Value(), kv) + f.Buffer.B = append(f.Buffer.B, ' ') + } + + // Drop last space + if n > 0 { + f.Buffer.Truncate(1) + } +} + +func (f format) AppendStruct(v reflect.Value, t reflect.Type) { + // Get value type + f.VType = t.String() + + // Append field formatted struct fields + f._AppendFieldType(func(f format) { + f.AppendStructFields(v, t) + }) +} + +func (f format) AppendStructFields(v reflect.Value, t reflect.Type) { + // Get field no. + n := v.NumField() + + // Iterate struct fields + for i := 0; i < n; i++ { + vfield := v.Field(i) + tfield := t.Field(i) + + // Append field name + f.AppendStringKey(tfield.Name) + f.Buffer.B = append(f.Buffer.B, '=') + f.SetValue().AppendInterfaceOrReflectNext(vfield, tfield.Type) + + // Append separator + f.Buffer.B = append(f.Buffer.B, ' ') + } + + // Drop last space + if n > 0 { + f.Buffer.Truncate(1) + } +} + +func (f format) _AppendMethodType(method func() string, i interface{}) (ok bool) { + // Verbose -- no methods + if f.Verbose() { + return false + } + + // Catch nil type + if isNil(i) { + f.AppendNil() + return true + } + + // Catch any panics + defer func() { + if r := recover(); r != nil { + // DON'T recurse catchPanic() + if f.Panic() { + panic(r) + } + + // Attempt to decode panic into buf + f.Buffer.B = append(f.Buffer.B, `!{PANIC=`...) + f.SetPanic().AppendInterfaceOrReflect(r) + f.Buffer.B = append(f.Buffer.B, '}') + + // Ensure no further attempts + // to format after return + ok = true + } + }() + + // Get method result + result := method() + + switch { + // Append as key formatted + case f.Key(): + f.AppendStringKey(result) + + // Append as always quoted + case f.Value(): + f.AppendStringQuoted(result) + + // Append as-is + default: + f.Buffer.B = append(f.Buffer.B, result...) + } + + return true +} + +// _AppendPrimitiveType is a helper to append prefix/suffix for primitives (numbers/bools/bytes/runes). +func (f format) _AppendPrimitiveType(appendPrimitive func(format)) { + if f.Verbose() { + // Append value with type information + f._AppendPrimitiveTyped(appendPrimitive) + } else { + // Append simply as-is + appendPrimitive(f) + } +} + +// _AppendPrimitiveTyped is a helper to append prefix/suffix for primitives (numbers/bools/bytes/runes) with their types (if deref'd). +func (f format) _AppendPrimitiveTyped(appendPrimitive func(format)) { + if f.Derefs > 0 { + // Is deref'd, append type info + f.Buffer.B = append(f.Buffer.B, '(') + f.AppendType() + f.Buffer.WriteString(`)(`) + appendPrimitive(f) + f.Buffer.B = append(f.Buffer.B, ')') + } else { + // Simply append value + appendPrimitive(f) + } +} + +// _AppendPtrType is a helper to append prefix/suffix for ptr types (with type if necessary). +func (f format) _AppendPtrType(appendPtr func(format)) { + if f.Verbose() { + // Append value with type information + f.Buffer.B = append(f.Buffer.B, '(') + f.AppendType() + f.Buffer.WriteString(`)(`) + appendPtr(f) + f.Buffer.B = append(f.Buffer.B, ')') + } else { + // Append simply as-is + appendPtr(f) + } +} + +// _AppendArray is a helper to append prefix/suffix for array-types. +func (f format) _AppendArray(appendArray func(format)) { + f.Buffer.B = append(f.Buffer.B, '[') + appendArray(f) + f.Buffer.B = append(f.Buffer.B, ']') +} + +// _AppendArrayTyped is a helper to append prefix/suffix for array-types with their types. +func (f format) _AppendArrayTyped(appendArray func(format)) { + f.AppendType() + f.Buffer.B = append(f.Buffer.B, '{') + appendArray(f) + f.Buffer.B = append(f.Buffer.B, '}') +} + +// _AppendFields is a helper to append prefix/suffix for field-types (with type if necessary). +func (f format) _AppendFieldType(appendFields func(format)) { + if f.Verbose() { + f.AppendType() + } + f.Buffer.B = append(f.Buffer.B, '{') + appendFields(f) + f.Buffer.B = append(f.Buffer.B, '}') +} + +// byte2str returns 'c' as a string, escaping if necessary. +func byte2str(c byte) string { + switch c { + case '\a': + return `\a` + case '\b': + return `\b` + case '\f': + return `\f` + case '\n': + return `\n` + case '\r': + return `\r` + case '\t': + return `\t` + case '\v': + return `\v` + case '\'': + return `\\` + default: + if c < ' ' { + const hex = "0123456789abcdef" + return `\x` + + string(hex[c>>4]) + + string(hex[c&0xF]) + } + return string(c) + } +} diff --git a/vendor/codeberg.org/gruf/go-kv/format/formatter.go b/vendor/codeberg.org/gruf/go-kv/format/formatter.go new file mode 100644 index 000000000..fd8cf98df --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/format/formatter.go @@ -0,0 +1,349 @@ +package format + +import ( + "strconv" + "strings" + + "codeberg.org/gruf/go-byteutil" +) + +// Formatter allows configuring value and string formatting. +type Formatter struct { + // MaxDepth specifies the max depth of fields the formatter will iterate. + // Once max depth is reached, value will simply be formatted as "...". + // e.g. + // + // MaxDepth=1 + // type A struct{ + // Nested B + // } + // type B struct{ + // Nested C + // } + // type C struct{ + // Field string + // } + // + // Append(&buf, A{}) => {Nested={Nested={Field=...}}} + MaxDepth uint8 +} + +// Append will append formatted form of supplied values into 'buf'. +func (f *Formatter) Append(buf *byteutil.Buffer, v ...interface{}) { + fmt := format{Buffer: buf, Config: f} + for i := 0; i < len(v); i++ { + fmt.AppendInterfaceOrReflect(v[i]) + fmt.Buffer.B = append(fmt.Buffer.B, ' ') + } + if len(v) > 0 { + fmt.Buffer.Truncate(1) + } +} + +// Appendf will append the formatted string with supplied values into 'buf'. +// Supported format directives: +// - '{}' => format supplied arg, in place +// - '{0}' => format arg at index 0 of supplied, in place +// - '{:?}' => format supplied arg verbosely, in place +// - '{:k}' => format supplied arg as key, in place +// - '{:v}' => format supplied arg as value, in place +// +// To escape either of '{}' simply append an additional brace e.g. +// - '{{' => '{' +// - '}}' => '}' +// - '{{}}' => '{}' +// - '{{:?}}' => '{:?}' +// +// More formatting directives might be included in the future. +func (f *Formatter) Appendf(buf *byteutil.Buffer, s string, a ...interface{}) { + const ( + // ground state + modeNone = uint8(0) + + // prev reached '{' + modeOpen = uint8(1) + + // prev reached '}' + modeClose = uint8(2) + + // parsing directive index + modeIdx = uint8(3) + + // parsing directive operands + modeOp = uint8(4) + ) + + var ( + // mode is current parsing mode + mode uint8 + + // arg is the current arg index + arg int + + // carg is current directive-set arg index + carg int + + // last is the trailing cursor to see slice windows + last int + + // idx is the current index in 's' + idx int + + // fmt is the base argument formatter + fmt = format{ + Config: f, + Buffer: buf, + } + + // NOTE: these functions are defined here as function + // locals as it turned out to be better for performance + // doing it this way, than encapsulating their logic in + // some kind of parsing structure. Maybe if the parser + // was pooled along with the buffers it might work out + // better, but then it makes more internal functions i.e. + // .Append() .Appendf() less accessible outside package. + // + // Currently, passing '-gcflags "-l=4"' causes a not + // insignificant decrease in ns/op, which is likely due + // to more aggressive function inlining, which this + // function can obviously stand to benefit from :) + + // Str returns current string window slice, and updates + // the trailing cursor 'last' to current 'idx' + Str = func() string { + str := s[last:idx] + last = idx + return str + } + + // MoveUp moves the trailing cursor 'last' just past 'idx' + MoveUp = func() { + last = idx + 1 + } + + // MoveUpTo moves the trailing cursor 'last' either up to + // closest '}', or current 'idx', whichever is furthest. + // NOTE: by calling bytealg.IndexByteString() directly (and + // not the strconv pass-through, we shave-off complexity + // which allows this function to be inlined). + MoveUpTo = func() { + i := strings.IndexByte(s[idx:], '}') + if i >= 0 { + idx += i + } + MoveUp() + } + + // ParseIndex parses an integer from the current string + // window, updating 'last' to 'idx'. The string window + // is ASSUMED to contain only valid ASCII numbers. This + // only returns false if number exceeds platform int size + ParseIndex = func() bool { + var str string + + // Get current window + if str = Str(); len(str) < 1 { + return true + } + + // Index HAS to fit within platform integer size + if !(strconv.IntSize == 32 && (0 < len(s) && len(s) < 10)) || + !(strconv.IntSize == 64 && (0 < len(s) && len(s) < 19)) { + return false + } + + carg = 0 + + // Build integer from string + for i := 0; i < len(str); i++ { + carg = carg*10 + int(str[i]-'0') + } + + return true + } + + // ValidOp checks that for current ending idx, that a valid + // operand was achieved -- only 0, 1 are valid numbers. + ValidOp = func() bool { + diff := (idx - last) + last = idx + return diff < 2 + } + + // AppendArg will take either the directive-set, or + // iterated arg index, check within bounds of 'a' and + // append the that argument formatted to the buffer. + // On failure, it will append an error string + AppendArg = func() { + // Look for idx + if carg < 0 { + carg = arg + } + + // Incr idx + arg++ + + if carg < len(a) { + // Append formatted argument value + fmt.AppendInterfaceOrReflect(a[carg]) + } else { + // No argument found for index + fmt.Buffer.B = append(fmt.Buffer.B, `!{MISSING_ARG}`...) + } + } + + // Reset will reset the mode to ground, the flags + // to empty and parsed 'carg' to empty + Reset = func() { + mode = modeNone + fmt.CurDepth = 0 + fmt.Flags = 0 + fmt.VType = "" + fmt.Derefs = 0 + carg = -1 + } + ) + + for idx = 0; idx < len(s); idx++ { + // Get next char + c := s[idx] + + switch mode { + // Ground mode + case modeNone: + switch c { + case '{': + // Enter open mode + fmt.Buffer.B = append(fmt.Buffer.B, Str()...) + mode = modeOpen + MoveUp() + case '}': + // Enter close mode + fmt.Buffer.B = append(fmt.Buffer.B, Str()...) + mode = modeClose + MoveUp() + } + + // Encountered open '{' + case modeOpen: + switch c { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + // Starting index + mode = modeIdx + MoveUp() + case '{': + // Escaped bracket + fmt.Buffer.B = append(fmt.Buffer.B, '{') + mode = modeNone + MoveUp() + case '}': + // Format arg + AppendArg() + Reset() + MoveUp() + case ':': + // Starting operands + mode = modeOp + MoveUp() + default: + // Bad char, missing a close + fmt.Buffer.B = append(fmt.Buffer.B, `!{MISSING_CLOSE}`...) + mode = modeNone + MoveUpTo() + } + + // Encountered close '}' + case modeClose: + switch c { + case '}': + // Escaped close bracket + fmt.Buffer.B = append(fmt.Buffer.B, '}') + mode = modeNone + MoveUp() + default: + // Missing an open bracket + fmt.Buffer.B = append(fmt.Buffer.B, `!{MISSING_OPEN}`...) + mode = modeNone + MoveUp() + } + + // Preparing index + case modeIdx: + switch c { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + case ':': + if !ParseIndex() { + // Unable to parse an integer + fmt.Buffer.B = append(fmt.Buffer.B, `!{BAD_INDEX}`...) + mode = modeNone + MoveUpTo() + } else { + // Starting operands + mode = modeOp + MoveUp() + } + case '}': + if !ParseIndex() { + // Unable to parse an integer + fmt.Buffer.B = append(fmt.Buffer.B, `!{BAD_INDEX}`...) + } else { + // Format arg + AppendArg() + } + Reset() + MoveUp() + default: + // Not a valid index character + fmt.Buffer.B = append(fmt.Buffer.B, `!{BAD_INDEX}`...) + mode = modeNone + MoveUpTo() + } + + // Preparing operands + case modeOp: + switch c { + case 'k': + fmt.Flags |= IsKeyBit + case 'v': + fmt.Flags |= IsValBit + case '?': + fmt.Flags |= VboseBit + case '}': + if !ValidOp() { + // Bad operands parsed + fmt.Buffer.B = append(fmt.Buffer.B, `!{BAD_OPERAND}`...) + } else { + // Format arg + AppendArg() + } + Reset() + MoveUp() + default: + // Not a valid operand char + fmt.Buffer.B = append(fmt.Buffer.B, `!{BAD_OPERAND}`...) + Reset() + MoveUpTo() + } + } + } + + // Append any remaining + fmt.Buffer.B = append(fmt.Buffer.B, s[last:]...) +} + +// formatter is the default formatter instance. +var formatter = Formatter{ + MaxDepth: 10, +} + +// Append will append formatted form of supplied values into 'buf' using default formatter. +// See Formatter.Append() for more documentation. +func Append(buf *byteutil.Buffer, v ...interface{}) { + formatter.Append(buf, v...) +} + +// Appendf will append the formatted string with supplied values into 'buf' using default formatter. +// See Formatter.Appendf() for more documentation. +func Appendf(buf *byteutil.Buffer, s string, a ...interface{}) { + formatter.Appendf(buf, s, a...) +} diff --git a/vendor/codeberg.org/gruf/go-kv/format/util.go b/vendor/codeberg.org/gruf/go-kv/format/util.go new file mode 100644 index 000000000..c4c42329e --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/format/util.go @@ -0,0 +1,56 @@ +package format + +import ( + "strings" + "unsafe" +) + +// ContainsSpaceOrTab checks if "s" contains space or tabs. +func ContainsSpaceOrTab(s string) bool { + if i := strings.IndexByte(s, ' '); i != -1 { + return true // note using indexbyte as it is ASM. + } else if i := strings.IndexByte(s, '\t'); i != -1 { + return true + } + return false +} + +// ContainsDoubleQuote checks if "s" contains a double quote. +func ContainsDoubleQuote(s string) bool { + return (strings.IndexByte(s, '"') != -1) +} + +// AppendEscape will append 's' to 'dst' and escape any double quotes. +func AppendEscape(dst []byte, str string) []byte { + var delim bool + for i := range str { + if str[i] == '\\' && !delim { + // Set delim flag + delim = true + continue + } else if str[i] == '"' && !delim { + // Append escaped double quote + dst = append(dst, `\"`...) + continue + } else if delim { + // Append skipped slash + dst = append(dst, `\`...) + delim = false + } + + // Append char as-is + dst = append(dst, str[i]) + } + return dst +} + +// isNil will safely check if 'v' is nil without dealing with weird Go interface nil bullshit. +func isNil(i interface{}) bool { + type eface struct{ _type, data unsafe.Pointer } //nolint + return (*(*eface)(unsafe.Pointer(&i))).data == nil //nolint +} + +// b2s converts a byteslice to string without allocation. +func b2s(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/vendor/codeberg.org/gruf/go-kv/util.go b/vendor/codeberg.org/gruf/go-kv/util.go new file mode 100644 index 000000000..a9526bf3d --- /dev/null +++ b/vendor/codeberg.org/gruf/go-kv/util.go @@ -0,0 +1,71 @@ +package kv + +import ( + "strconv" + + "codeberg.org/gruf/go-byteutil" + "codeberg.org/gruf/go-kv/format" +) + +// appendQuoteKey will append and escape/quote a formatted key string. +func appendQuoteKey(buf *byteutil.Buffer, str string) { + switch { + case !strconv.CanBackquote(str): + // Append quoted and escaped string + buf.B = strconv.AppendQuote(buf.B, str) + case format.ContainsDoubleQuote(str): + // Double quote and escape string + buf.B = append(buf.B, '"') + buf.B = format.AppendEscape(buf.B, str) + buf.B = append(buf.B, '"') + case len(str) < 1 || format.ContainsSpaceOrTab(str): + // Double quote this string as-is + buf.WriteString(`"` + str + `"`) + default: + // Append string as-is + buf.WriteString(str) + } +} + +// appendQuoteValue will append and escape/quote a formatted value string. +func appendQuoteValue(buf *byteutil.Buffer, str string) { + switch { + case !strconv.CanBackquote(str): + // Append quoted and escaped string + buf.B = strconv.AppendQuote(buf.B, str) + return + case !doubleQuoted(str): + if format.ContainsDoubleQuote(str) { + // Double quote and escape string + buf.B = append(buf.B, '"') + buf.B = format.AppendEscape(buf.B, str) + buf.B = append(buf.B, '"') + return + } else if format.ContainsSpaceOrTab(str) { + // Double quote this string as-is + buf.WriteString(`"` + str + `"`) + return + } + } + + // Append string as-is + buf.WriteString(str) +} + +// doubleQuoted will return whether 'str' is double quoted. +func doubleQuoted(str string) bool { + if len(str) < 2 || + str[0] != '"' || str[len(str)-1] != '"' { + return false + } + var delim bool + for i := len(str) - 2; i >= 0; i-- { + switch str[i] { + case '\\': + delim = !delim + default: + return !delim + } + } + return !delim +} diff --git a/vendor/codeberg.org/gruf/go-logger/v2/LICENSE b/vendor/codeberg.org/gruf/go-logger/v2/LICENSE new file mode 100644 index 000000000..e4163ae35 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-logger/v2/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2022 gruf + +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. diff --git a/vendor/codeberg.org/gruf/go-logger/v2/level/levels.go b/vendor/codeberg.org/gruf/go-logger/v2/level/levels.go new file mode 100644 index 000000000..1804bdb23 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-logger/v2/level/levels.go @@ -0,0 +1,49 @@ +package level + +// LEVEL defines a level of logging. +type LEVEL uint8 + +// Default levels of logging. +const ( + UNSET LEVEL = 0 + PANIC LEVEL = 1 + FATAL LEVEL = 50 + ERROR LEVEL = 100 + WARN LEVEL = 150 + INFO LEVEL = 200 + DEBUG LEVEL = 250 + TRACE LEVEL = 254 + ALL LEVEL = ^LEVEL(0) +) + +// CanLog returns whether an incoming log of 'lvl' can be logged against receiving level. +func (loglvl LEVEL) CanLog(lvl LEVEL) bool { + return loglvl > lvl +} + +// Levels defines a mapping of log LEVELs to formatted level strings. +type Levels [int(ALL) + 1]string + +// Default returns the default set of log levels. +func Default() Levels { + return Levels{ + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", + PANIC: "PANIC", + + // we set these just so that + // it can be debugged when someone + // attempts to log with ALL/UNSET + ALL: "{all}", + UNSET: "{unset}", + } +} + +// Get fetches the level string for the provided value. +func (l Levels) Get(lvl LEVEL) string { + return l[int(lvl)] +} diff --git a/vendor/github.com/sirupsen/logrus/hooks/syslog/README.md b/vendor/github.com/sirupsen/logrus/hooks/syslog/README.md deleted file mode 100644 index 1bbc0f72d..000000000 --- a/vendor/github.com/sirupsen/logrus/hooks/syslog/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Syslog Hooks for Logrus :walrus: - -## Usage - -```go -import ( - "log/syslog" - "github.com/sirupsen/logrus" - lSyslog "github.com/sirupsen/logrus/hooks/syslog" -) - -func main() { - log := logrus.New() - hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "") - - if err == nil { - log.Hooks.Add(hook) - } -} -``` - -If you want to connect to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). Just assign empty string to the first two parameters of `NewSyslogHook`. It should look like the following. - -```go -import ( - "log/syslog" - "github.com/sirupsen/logrus" - lSyslog "github.com/sirupsen/logrus/hooks/syslog" -) - -func main() { - log := logrus.New() - hook, err := lSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "") - - if err == nil { - log.Hooks.Add(hook) - } -} -``` diff --git a/vendor/github.com/sirupsen/logrus/hooks/syslog/syslog.go b/vendor/github.com/sirupsen/logrus/hooks/syslog/syslog.go deleted file mode 100644 index 02b8df380..000000000 --- a/vendor/github.com/sirupsen/logrus/hooks/syslog/syslog.go +++ /dev/null @@ -1,55 +0,0 @@ -// +build !windows,!nacl,!plan9 - -package syslog - -import ( - "fmt" - "log/syslog" - "os" - - "github.com/sirupsen/logrus" -) - -// SyslogHook to send logs via syslog. -type SyslogHook struct { - Writer *syslog.Writer - SyslogNetwork string - SyslogRaddr string -} - -// Creates a hook to be added to an instance of logger. This is called with -// `hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_DEBUG, "")` -// `if err == nil { log.Hooks.Add(hook) }` -func NewSyslogHook(network, raddr string, priority syslog.Priority, tag string) (*SyslogHook, error) { - w, err := syslog.Dial(network, raddr, priority, tag) - return &SyslogHook{w, network, raddr}, err -} - -func (hook *SyslogHook) Fire(entry *logrus.Entry) error { - line, err := entry.String() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err) - return err - } - - switch entry.Level { - case logrus.PanicLevel: - return hook.Writer.Crit(line) - case logrus.FatalLevel: - return hook.Writer.Crit(line) - case logrus.ErrorLevel: - return hook.Writer.Err(line) - case logrus.WarnLevel: - return hook.Writer.Warning(line) - case logrus.InfoLevel: - return hook.Writer.Info(line) - case logrus.DebugLevel, logrus.TraceLevel: - return hook.Writer.Debug(line) - default: - return nil - } -} - -func (hook *SyslogHook) Levels() []logrus.Level { - return logrus.AllLevels -} diff --git a/vendor/modules.txt b/vendor/modules.txt index f1e143df8..6c794a1e3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -31,6 +31,13 @@ codeberg.org/gruf/go-fastpath # codeberg.org/gruf/go-hashenc v1.0.2 ## explicit; go 1.16 codeberg.org/gruf/go-hashenc +# codeberg.org/gruf/go-kv v1.3.2 +## explicit; go 1.16 +codeberg.org/gruf/go-kv +codeberg.org/gruf/go-kv/format +# codeberg.org/gruf/go-logger/v2 v2.0.6 +## explicit; go 1.16 +codeberg.org/gruf/go-logger/v2/level # codeberg.org/gruf/go-mutexes v1.1.2 ## explicit; go 1.14 codeberg.org/gruf/go-mutexes @@ -309,7 +316,6 @@ github.com/russross/blackfriday/v2 # github.com/sirupsen/logrus v1.8.1 ## explicit; go 1.13 github.com/sirupsen/logrus -github.com/sirupsen/logrus/hooks/syslog # github.com/spf13/afero v1.8.2 ## explicit; go 1.13 github.com/spf13/afero