From a2eb85eb25c15be9ebeb0547a188acc55f4ec0e8 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 14:27:23 +0300 Subject: [PATCH 01/26] wip: structured logging --- core/db/models/connection.go | 7 + core/engine.go | 32 +-- core/engine_test.go | 3 +- core/healthcheck/processor.go | 21 +- core/healthcheck/processor_test.go | 18 +- core/job_manager.go | 9 +- core/job_manager_test.go | 98 ++++---- core/logger/account_logger_decorator.go | 96 ------- core/logger/account_logger_decorator_test.go | 98 -------- core/logger/api_client_adapter.go | 21 ++ core/logger/attrs.go | 19 ++ core/logger/default.go | 87 +++++++ core/logger/handler.go | 8 + core/logger/logger.go | 235 +++--------------- core/logger/logger_test.go | 168 ------------- core/logger/nil_handler.go | 26 ++ core/logger/nil_logger.go | 45 ---- core/logger/nil_logger_test.go | 91 ------- core/logger/prefix_decorator.go | 111 --------- core/logger/prefix_decorator_test.go | 167 ------------- core/logger/zabbix_collector_adapter.go | 19 ++ core/module_features_uploader.go | 16 +- core/module_features_uploader_test.go | 16 +- core/sentry.go | 36 ++- core/sentry_test.go | 11 +- core/util/httputil/http_client_builder.go | 17 +- .../util/httputil/http_client_builder_test.go | 7 +- core/util/testutil/buffer_logger.go | 115 ++------- core/util/testutil/buffer_logger_test.go | 7 +- core/util/testutil/lockable_buffer.go | 150 +++++++++++ core/util/utils.go | 2 +- core/util/utils_test.go | 4 +- go.mod | 2 +- go.sum | 1 + 34 files changed, 543 insertions(+), 1220 deletions(-) delete mode 100644 core/logger/account_logger_decorator.go delete mode 100644 core/logger/account_logger_decorator_test.go create mode 100644 core/logger/api_client_adapter.go create mode 100644 core/logger/attrs.go create mode 100644 core/logger/default.go create mode 100644 core/logger/handler.go delete mode 100644 core/logger/logger_test.go create mode 100644 core/logger/nil_handler.go delete mode 100644 core/logger/nil_logger.go delete mode 100644 core/logger/nil_logger_test.go delete mode 100644 core/logger/prefix_decorator.go delete mode 100644 core/logger/prefix_decorator_test.go create mode 100644 core/logger/zabbix_collector_adapter.go create mode 100644 core/util/testutil/lockable_buffer.go diff --git a/core/db/models/connection.go b/core/db/models/connection.go index 8bb55d3..0f7e0a7 100644 --- a/core/db/models/connection.go +++ b/core/db/models/connection.go @@ -17,3 +17,10 @@ type Connection struct { ID int `gorm:"primary_key" json:"id"` Active bool `json:"active,omitempty"` } + +func (c Connection) Address() string { + if c.PublicURL != "" { + return c.PublicURL + } + return c.URL +} diff --git a/core/engine.go b/core/engine.go index bd3b38a..f997b47 100644 --- a/core/engine.go +++ b/core/engine.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "io/fs" + "log/slog" "net/http" "sync" @@ -13,7 +14,6 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" - "github.com/op/go-logging" "github.com/retailcrm/zabbix-metrics-collector" "golang.org/x/text/language" @@ -62,16 +62,15 @@ func (a AppInfo) Release() string { // Engine struct. type Engine struct { - logger logger.Logger - AppInfo AppInfo - Sessions sessions.Store - LogFormatter logging.Formatter - Config config.Configuration - Zabbix metrics.Transport - ginEngine *gin.Engine - csrf *middleware.CSRF - httpClient *http.Client - jobManager *JobManager + logger logger.Logger + AppInfo AppInfo + Sessions sessions.Store + Config config.Configuration + Zabbix metrics.Transport + ginEngine *gin.Engine + csrf *middleware.CSRF + httpClient *http.Client + jobManager *JobManager db.ORM Localizer util.Utils @@ -133,9 +132,6 @@ func (e *Engine) Prepare() *Engine { if e.DefaultError == "" { e.DefaultError = "error" } - if e.LogFormatter == nil { - e.LogFormatter = logger.DefaultLogFormatter() - } if e.LocaleMatcher == nil { e.LocaleMatcher = DefaultLocalizerMatcher() } @@ -150,9 +146,13 @@ func (e *Engine) Prepare() *Engine { e.Localizer.Preload(e.PreloadLanguages) } + if !e.Config.IsDebug() { + logger.DefaultOpts.Level = slog.LevelInfo + } + e.CreateDB(e.Config.GetDBConfig()) e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0) - e.SetLogger(logger.NewStandard(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter)) + e.SetLogger(logger.NewDefaultText()) e.Sentry.Localizer = &e.Localizer e.Utils.Logger = e.Logger() e.Sentry.Logger = e.Logger() @@ -175,7 +175,7 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine { } cfg := e.Config.GetZabbixConfig() sender := zabbix.NewSender(cfg.ServerHost, cfg.ServerPort) - e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, e.Logger()) + e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, logger.ZabbixCollectorAdapter(e.Logger())) return e } diff --git a/core/engine_test.go b/core/engine_test.go index 259ef92..b59e5d3 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -123,7 +123,6 @@ func (e *EngineTest) Test_Prepare() { assert.True(e.T(), e.engine.prepared) assert.NotNil(e.T(), e.engine.Config) assert.NotEmpty(e.T(), e.engine.DefaultError) - assert.NotEmpty(e.T(), e.engine.LogFormatter) assert.NotEmpty(e.T(), e.engine.LocaleMatcher) assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag)) assert.NotNil(e.T(), e.engine.DB) @@ -253,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() { defer func() { e.engine.logger = origLogger }() - e.engine.logger = &logger.StandardLogger{} + e.engine.logger = logger.NewDefaultNil() e.engine.SetLogger(nil) assert.NotNil(e.T(), e.engine.logger) } diff --git a/core/healthcheck/processor.go b/core/healthcheck/processor.go index fc11ccd..cb178d3 100644 --- a/core/healthcheck/processor.go +++ b/core/healthcheck/processor.go @@ -1,6 +1,8 @@ package healthcheck import ( + "log/slog" + "github.com/retailcrm/mg-transport-core/v2/core/logger" ) @@ -29,19 +31,19 @@ type CounterProcessor struct { func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen if counter.IsFailed() { if counter.IsFailureProcessed() { - c.debugLog("skipping counter id=%d because its failure is already processed", id) + c.debugLog("skipping counter because its failure is already processed", slog.Int(logger.CounterIDAttr, id)) return true } apiURL, apiKey, _, exists := c.ConnectionDataProvider(id) if !exists { - c.debugLog("cannot find connection data for counter id=%d", id) + c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id)) return true } err := c.Notifier(apiURL, apiKey, counter.Message()) if err != nil { - c.debugLog("cannot send notification for counter id=%d: %s (message: %s)", - id, err, counter.Message()) + c.debugLog("cannot send notification for counter", + slog.Int(logger.CounterIDAttr, id), logger.ErrAttr(err), slog.String(logger.FailureMessageAttr, counter.Message())) } counter.FailureProcessed() return true @@ -53,7 +55,8 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn // Ignore this counter for now because total count of requests is less than minimal count. // The results may not be representative. if (succeeded + failed) < c.MinRequests { - c.debugLog("skipping counter id=%d because it has fewer than %d requests", id, c.MinRequests) + c.debugLog("skipping counter because it has too few requests", + slog.Int(logger.CounterIDAttr, id), slog.Any("minRequests", c.MinRequests)) return true } @@ -72,13 +75,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id) if !exists { - c.debugLog("cannot find connection data for counter id=%d", id) + c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id)) return true } err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang)) if err != nil { - c.debugLog("cannot send notification for counter id=%d: %s (message: %s)", - id, err, counter.Message()) + c.debugLog("cannot send notification for counter", + slog.Int(logger.CounterIDAttr, id), logger.ErrAttr(err), slog.String(logger.FailureMessageAttr, counter.Message())) } counter.CountersProcessed() return true @@ -96,6 +99,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string { func (c CounterProcessor) debugLog(msg string, args ...interface{}) { if c.Debug { - c.Logger.Debugf(msg, args...) + c.Logger.Debug(msg, args...) } } diff --git a/core/healthcheck/processor_test.go b/core/healthcheck/processor_test.go index 9774310..ecaa6a1 100644 --- a/core/healthcheck/processor_test.go +++ b/core/healthcheck/processor_test.go @@ -95,7 +95,8 @@ func (t *CounterProcessorTest) Test_FailureProcessed() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "skipping counter id=1 because its failure is already processed") + t.Assert().Contains(log.String(), "skipping counter because its failure is already processed") + t.Assert().Contains(log.String(), "id=1") } func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { @@ -107,7 +108,8 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot find connection data for counter id=1") + t.Assert().Contains(log.String(), "cannot find connection data for counter") + t.Assert().Contains(log.String(), "id=1") } func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { @@ -121,7 +123,10 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot send notification for counter id=1: http status code: 500 (message: error message)") + t.Assert().Contains(log.String(), "cannot send notification for counter") + t.Assert().Contains(log.String(), "id=1") + t.Assert().Contains(log.String(), `error="http status code: 500"`) + t.Assert().Contains(log.String(), `message="error message"`) t.Assert().Equal(t.apiURL, n.apiURL) t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal("error message", n.message) @@ -155,7 +160,7 @@ func (t *CounterProcessorTest) Test_TooFewRequests() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), - fmt.Sprintf("skipping counter id=%d because it has fewer than %d requests", 1, DefaultMinRequests)) + fmt.Sprintf(`msg="skipping counter because it has too few requests" id=%d minRequests=%d`, 1, DefaultMinRequests)) } func (t *CounterProcessorTest) Test_ThresholdNotPassed() { @@ -200,7 +205,8 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot find connection data for counter id=1") + t.Assert().Contains(log.String(), "cannot find connection data for counter") + t.Assert().Contains(log.String(), "id=1") t.Assert().Empty(n.message) } @@ -218,7 +224,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot send notification for counter id=1: unknown error (message: )") + t.Assert().Contains(log.String(), `msg="cannot send notification for counter" id=1 error="unknown error" message=""`) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message) } diff --git a/core/job_manager.go b/core/job_manager.go index fac84de..ed1db6d 100644 --- a/core/job_manager.go +++ b/core/job_manager.go @@ -3,6 +3,7 @@ package core import ( "errors" "fmt" + "log/slog" "sync" "time" @@ -46,7 +47,7 @@ type Job struct { // SetLogger(logger). // SetLogging(false) // _ = manager.RegisterJob("updateTokens", &Job{ -// Command: func(log logger.Logger) error { +// Command: func(log logger.LoggerOld) error { // // logic goes here... // logger.Info("All tokens were updated successfully") // return nil @@ -146,14 +147,14 @@ func (j *Job) runOnceSync(name string, log logger.Logger) { // NewJobManager is a JobManager constructor. func NewJobManager() *JobManager { - return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewNil()} + return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewDefaultNil()} } // DefaultJobErrorHandler returns default error handler for a job. func DefaultJobErrorHandler() JobErrorHandler { return func(name string, err error, log logger.Logger) { if err != nil && name != "" { - log.Errorf("Job `%s` errored with an error: `%s`", name, err.Error()) + log.Error("job failed with an error", slog.String("job", name), logger.ErrAttr(err)) } } } @@ -162,7 +163,7 @@ func DefaultJobErrorHandler() JobErrorHandler { func DefaultJobPanicHandler() JobPanicHandler { return func(name string, recoverValue interface{}, log logger.Logger) { if recoverValue != nil && name != "" { - log.Errorf("Job `%s` panicked with value: `%#v`", name, recoverValue) + log.Error("job panicked with the value", slog.String("job", name), slog.Any("value", recoverValue)) } } } diff --git a/core/job_manager_test.go b/core/job_manager_test.go index 6da22de..dbfb09c 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -1,15 +1,16 @@ package core import ( + "context" "errors" "fmt" + "log/slog" "math/rand" "strings" "sync" "testing" "time" - "github.com/op/go-logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -25,7 +26,7 @@ type JobTest struct { executeErr chan error panicValue chan interface{} lastLog string - lastMsgLevel logging.Level + lastMsgLevel slog.Level syncBool bool } @@ -36,68 +37,69 @@ type JobManagerTest struct { syncRunnerFlag bool } -type callbackLoggerFunc func(level logging.Level, format string, args ...interface{}) +type callbackLoggerFunc func(ctx context.Context, level slog.Level, msg string, args ...any) type callbackLogger struct { fn callbackLoggerFunc } -func (n *callbackLogger) Fatal(args ...interface{}) { - n.fn(logging.CRITICAL, "", args...) +func (n *callbackLogger) Handler() slog.Handler { + return logger.NilHandler } -func (n *callbackLogger) Fatalf(format string, args ...interface{}) { - n.fn(logging.CRITICAL, format, args...) +func (n *callbackLogger) With(args ...any) logger.Logger { + return n } -func (n *callbackLogger) Panic(args ...interface{}) { - n.fn(logging.CRITICAL, "", args...) -} -func (n *callbackLogger) Panicf(format string, args ...interface{}) { - n.fn(logging.CRITICAL, format, args...) +func (n *callbackLogger) WithGroup(name string) logger.Logger { + return n } -func (n *callbackLogger) Critical(args ...interface{}) { - n.fn(logging.CRITICAL, "", args...) +func (n *callbackLogger) ForAccount(handler, conn, acc any) logger.Logger { + return n } -func (n *callbackLogger) Criticalf(format string, args ...interface{}) { - n.fn(logging.CRITICAL, format, args...) +func (n *callbackLogger) Enabled(ctx context.Context, level slog.Level) bool { + return true } -func (n *callbackLogger) Error(args ...interface{}) { - n.fn(logging.ERROR, "", args...) -} -func (n *callbackLogger) Errorf(format string, args ...interface{}) { - n.fn(logging.ERROR, format, args...) +func (n *callbackLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { + n.fn(ctx, level, msg, args...) } -func (n *callbackLogger) Warning(args ...interface{}) { - n.fn(logging.WARNING, "", args...) -} -func (n *callbackLogger) Warningf(format string, args ...interface{}) { - n.fn(logging.WARNING, format, args...) +func (n *callbackLogger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { } -func (n *callbackLogger) Notice(args ...interface{}) { - n.fn(logging.NOTICE, "", args...) -} -func (n *callbackLogger) Noticef(format string, args ...interface{}) { - n.fn(logging.NOTICE, format, args...) +func (n *callbackLogger) Debug(msg string, args ...any) { + n.Log(nil, slog.LevelDebug, msg, args...) } -func (n *callbackLogger) Info(args ...interface{}) { - n.fn(logging.INFO, "", args...) -} -func (n *callbackLogger) Infof(format string, args ...interface{}) { - n.fn(logging.INFO, format, args...) +func (n *callbackLogger) DebugContext(ctx context.Context, msg string, args ...any) { + n.Log(ctx, slog.LevelDebug, msg, args...) } -func (n *callbackLogger) Debug(args ...interface{}) { - n.fn(logging.DEBUG, "", args...) +func (n *callbackLogger) Info(msg string, args ...any) { + n.Log(nil, slog.LevelInfo, msg, args...) } -func (n *callbackLogger) Debugf(format string, args ...interface{}) { - n.fn(logging.DEBUG, format, args...) + +func (n *callbackLogger) InfoContext(ctx context.Context, msg string, args ...any) { + n.Log(ctx, slog.LevelInfo, msg, args...) +} + +func (n *callbackLogger) Warn(msg string, args ...any) { + n.Log(nil, slog.LevelWarn, msg, args...) +} + +func (n *callbackLogger) WarnContext(ctx context.Context, msg string, args ...any) { + n.Log(ctx, slog.LevelWarn, msg, args...) +} + +func (n *callbackLogger) Error(msg string, args ...any) { + n.Log(nil, slog.LevelError, msg, args...) +} + +func (n *callbackLogger) ErrorContext(ctx context.Context, msg string, args ...any) { + n.Log(ctx, slog.LevelError, msg, args...) } func TestJob(t *testing.T) { @@ -115,9 +117,9 @@ func TestDefaultJobErrorHandler(t *testing.T) { fn := DefaultJobErrorHandler() require.NotNil(t, fn) - fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) { + fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) { require.Len(t, i, 2) - assert.Equal(t, fmt.Sprintf("%s", i[1]), "test") + assert.Equal(t, "error=test", fmt.Sprintf("%s", i[1])) }}) } @@ -128,9 +130,9 @@ func TestDefaultJobPanicHandler(t *testing.T) { fn := DefaultJobPanicHandler() require.NotNil(t, fn) - fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) { + fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) { require.Len(t, i, 2) - assert.Equal(t, fmt.Sprintf("%s", i[1]), "test") + assert.Equal(t, "value=test", fmt.Sprintf("%s", i[1])) }}) } @@ -147,7 +149,7 @@ func (t *JobTest) testPanicHandler() JobPanicHandler { } func (t *JobTest) testLogger() logger.Logger { - return &callbackLogger{fn: func(level logging.Level, format string, args ...interface{}) { + return &callbackLogger{fn: func(_ context.Context, level slog.Level, format string, args ...interface{}) { if format == "" { var sb strings.Builder sb.Grow(3 * len(args)) // nolint:gomnd @@ -404,11 +406,11 @@ func (t *JobManagerTest) WaitForJob() bool { func (t *JobManagerTest) Test_SetLogger() { t.manager.logger = nil - t.manager.SetLogger(logger.NewStandard("test", logging.ERROR, logger.DefaultLogFormatter())) - assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger) + t.manager.SetLogger(logger.NewDefaultText()) + assert.IsType(t.T(), &logger.Default{}, t.manager.logger) t.manager.SetLogger(nil) - assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger) + assert.IsType(t.T(), &logger.Default{}, t.manager.logger) } func (t *JobManagerTest) Test_SetLogging() { diff --git a/core/logger/account_logger_decorator.go b/core/logger/account_logger_decorator.go deleted file mode 100644 index e35d8e9..0000000 --- a/core/logger/account_logger_decorator.go +++ /dev/null @@ -1,96 +0,0 @@ -package logger - -import ( - "fmt" - - "github.com/op/go-logging" -) - -// DefaultAccountLoggerFormat contains default prefix format for the AccountLoggerDecorator. -// Its messages will look like this (assuming you will provide the connection URL and account name): -// messageHandler (https://any.simla.com => @tg_account): sent message with id=1 -const DefaultAccountLoggerFormat = "%s (%s => %s):" - -type ComponentAware interface { - SetComponent(string) -} - -type ConnectionAware interface { - SetConnectionIdentifier(string) -} - -type AccountAware interface { - SetAccountIdentifier(string) -} - -type PrefixFormatAware interface { - SetPrefixFormat(string) -} - -type AccountLogger interface { - PrefixedLogger - ComponentAware - ConnectionAware - AccountAware - PrefixFormatAware -} - -type AccountLoggerDecorator struct { - format string - component string - connIdentifier string - accIdentifier string - PrefixDecorator -} - -func DecorateForAccount(base Logger, component, connIdentifier, accIdentifier string) AccountLogger { - return (&AccountLoggerDecorator{ - PrefixDecorator: PrefixDecorator{ - backend: base, - }, - component: component, - connIdentifier: connIdentifier, - accIdentifier: accIdentifier, - }).updatePrefix() -} - -// NewForAccount returns logger for account. It uses StandardLogger under the hood. -func NewForAccount( - transportCode, component, connIdentifier, accIdentifier string, - logLevel logging.Level, - logFormat logging.Formatter) AccountLogger { - return DecorateForAccount(NewStandard(transportCode, logLevel, logFormat), - component, connIdentifier, accIdentifier) -} - -func (a *AccountLoggerDecorator) SetComponent(s string) { - a.component = s - a.updatePrefix() -} - -func (a *AccountLoggerDecorator) SetConnectionIdentifier(s string) { - a.connIdentifier = s - a.updatePrefix() -} - -func (a *AccountLoggerDecorator) SetAccountIdentifier(s string) { - a.accIdentifier = s - a.updatePrefix() -} - -func (a *AccountLoggerDecorator) SetPrefixFormat(s string) { - a.format = s - a.updatePrefix() -} - -func (a *AccountLoggerDecorator) updatePrefix() AccountLogger { - a.SetPrefix(fmt.Sprintf(a.prefixFormat(), a.component, a.connIdentifier, a.accIdentifier)) - return a -} - -func (a *AccountLoggerDecorator) prefixFormat() string { - if a.format == "" { - return DefaultAccountLoggerFormat - } - return a.format -} diff --git a/core/logger/account_logger_decorator_test.go b/core/logger/account_logger_decorator_test.go deleted file mode 100644 index ab6a8b9..0000000 --- a/core/logger/account_logger_decorator_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package logger - -import ( - "bytes" - "fmt" - "testing" - - "github.com/op/go-logging" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -const ( - testComponent = "ComponentName" - testConnectionID = "https://test.retailcrm.pro" - testAccountID = "@account_name" -) - -type AccountLoggerDecoratorTest struct { - suite.Suite - buf *bytes.Buffer - logger AccountLogger -} - -func TestAccountLoggerDecorator(t *testing.T) { - suite.Run(t, new(AccountLoggerDecoratorTest)) -} - -func TestNewForAccount(t *testing.T) { - buf := &bytes.Buffer{} - logger := NewForAccount("code", "component", "conn", "acc", logging.DEBUG, DefaultLogFormatter()) - logger.(*AccountLoggerDecorator).backend.(*StandardLogger). - SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter())) - logger.Debugf("message %s", "text") - - assert.Contains(t, buf.String(), fmt.Sprintf(DefaultAccountLoggerFormat+" message", "component", "conn", "acc")) -} - -func (t *AccountLoggerDecoratorTest) SetupSuite() { - t.buf = &bytes.Buffer{} - t.logger = DecorateForAccount((&StandardLogger{}). - SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())), - testComponent, testConnectionID, testAccountID) -} - -func (t *AccountLoggerDecoratorTest) SetupTest() { - t.buf.Reset() - t.logger.SetComponent(testComponent) - t.logger.SetConnectionIdentifier(testConnectionID) - t.logger.SetAccountIdentifier(testAccountID) - t.logger.SetPrefixFormat(DefaultAccountLoggerFormat) -} - -func (t *AccountLoggerDecoratorTest) Test_LogWithNewFormat() { - t.logger.SetPrefixFormat("[%s (%s: %s)] =>") - t.logger.Infof("test message") - - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), - fmt.Sprintf("[%s (%s: %s)] =>", testComponent, testConnectionID, testAccountID)) -} - -func (t *AccountLoggerDecoratorTest) Test_Log() { - t.logger.Infof("test message") - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), - fmt.Sprintf(DefaultAccountLoggerFormat, testComponent, testConnectionID, testAccountID)) -} - -func (t *AccountLoggerDecoratorTest) Test_SetComponent() { - t.logger.SetComponent("NewComponent") - t.logger.Infof("test message") - - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), - fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", testConnectionID, testAccountID)) -} - -func (t *AccountLoggerDecoratorTest) Test_SetConnectionIdentifier() { - t.logger.SetComponent("NewComponent") - t.logger.SetConnectionIdentifier("https://test.simla.com") - t.logger.Infof("test message") - - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), - fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", testAccountID)) -} - -func (t *AccountLoggerDecoratorTest) Test_SetAccountIdentifier() { - t.logger.SetComponent("NewComponent") - t.logger.SetConnectionIdentifier("https://test.simla.com") - t.logger.SetAccountIdentifier("@new_account_name") - t.logger.Infof("test message") - - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), - fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", "@new_account_name")) -} diff --git a/core/logger/api_client_adapter.go b/core/logger/api_client_adapter.go new file mode 100644 index 0000000..7951336 --- /dev/null +++ b/core/logger/api_client_adapter.go @@ -0,0 +1,21 @@ +package logger + +import ( + "fmt" + + retailcrm "github.com/retailcrm/api-client-go/v2" +) + +type apiClientAdapter struct { + logger Logger +} + +// APIClientAdapter returns BasicLogger that calls underlying logger. +func APIClientAdapter(logger Logger) retailcrm.BasicLogger { + return &apiClientAdapter{logger: logger} +} + +// Printf data in the log using Debug method. +func (l *apiClientAdapter) Printf(format string, v ...interface{}) { + l.logger.Debug(fmt.Sprintf(format, v...)) +} diff --git a/core/logger/attrs.go b/core/logger/attrs.go new file mode 100644 index 0000000..293e0b5 --- /dev/null +++ b/core/logger/attrs.go @@ -0,0 +1,19 @@ +package logger + +import "log/slog" + +const ( + HandlerAttr = "handler" + ConnectionAttr = "connection" + AccountAttr = "account" + CounterIDAttr = "counterId" + ErrorAttr = "error" + FailureMessageAttr = "failureMessage" +) + +func ErrAttr(err any) slog.Attr { + if err == nil { + return slog.String(ErrorAttr, "") + } + return slog.Any(ErrorAttr, err) +} diff --git a/core/logger/default.go b/core/logger/default.go new file mode 100644 index 0000000..78240a3 --- /dev/null +++ b/core/logger/default.go @@ -0,0 +1,87 @@ +package logger + +import ( + "context" + "log/slog" + "os" +) + +type Default struct { + Logger *slog.Logger +} + +func NewDefault(log *slog.Logger) Logger { + return &Default{Logger: log} +} + +func NewDefaultText() Logger { + return NewDefault(slog.New(slog.NewTextHandler(os.Stdout, DefaultOpts))) +} + +func NewDefaultJSON() Logger { + return NewDefault(slog.New(slog.NewJSONHandler(os.Stdout, DefaultOpts))) +} + +func NewDefaultNil() Logger { + return NewDefault(slog.New(NilHandler)) +} + +func (d *Default) Handler() slog.Handler { + return d.Logger.Handler() +} + +func (d *Default) ForAccount(handler, conn, acc any) Logger { + return d.With(slog.Any(HandlerAttr, handler), slog.Any(ConnectionAttr, conn), slog.Any(AccountAttr, acc)) +} + +func (d *Default) With(args ...any) Logger { + return &Default{Logger: d.Logger.With(args...)} +} + +func (d *Default) WithGroup(name string) Logger { + return &Default{Logger: d.Logger.WithGroup(name)} +} + +func (d *Default) Enabled(ctx context.Context, level slog.Level) bool { + return d.Logger.Enabled(ctx, level) +} + +func (d *Default) Log(ctx context.Context, level slog.Level, msg string, args ...any) { + d.Logger.Log(ctx, level, msg, args...) +} + +func (d *Default) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { + d.Logger.LogAttrs(ctx, level, msg, attrs...) +} + +func (d *Default) Debug(msg string, args ...any) { + d.Logger.Debug(msg, args...) +} + +func (d *Default) DebugContext(ctx context.Context, msg string, args ...any) { + d.Logger.DebugContext(ctx, msg, args...) +} + +func (d *Default) Info(msg string, args ...any) { + d.Logger.Info(msg, args...) +} + +func (d *Default) InfoContext(ctx context.Context, msg string, args ...any) { + d.Logger.InfoContext(ctx, msg, args...) +} + +func (d *Default) Warn(msg string, args ...any) { + d.Logger.Warn(msg, args...) +} + +func (d *Default) WarnContext(ctx context.Context, msg string, args ...any) { + d.Logger.WarnContext(ctx, msg, args...) +} + +func (d *Default) Error(msg string, args ...any) { + d.Logger.Error(msg, args...) +} + +func (d *Default) ErrorContext(ctx context.Context, msg string, args ...any) { + d.Logger.ErrorContext(ctx, msg, args...) +} diff --git a/core/logger/handler.go b/core/logger/handler.go new file mode 100644 index 0000000..1c5ffe9 --- /dev/null +++ b/core/logger/handler.go @@ -0,0 +1,8 @@ +package logger + +import "log/slog" + +var DefaultOpts = &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelDebug, +} diff --git a/core/logger/logger.go b/core/logger/logger.go index 32445f9..4fb9f16 100644 --- a/core/logger/logger.go +++ b/core/logger/logger.go @@ -1,205 +1,44 @@ package logger import ( - "io" - "os" - "sync" - - "github.com/op/go-logging" + "context" + "log/slog" ) -// Logger contains methods which should be present in logger implementation. +// LoggerOld contains methods which should be present in logger implementation. +type LoggerOld interface { + Fatal(args ...any) + Fatalf(format string, args ...any) + Panic(args ...any) + Panicf(format string, args ...any) + Critical(args ...any) + Criticalf(format string, args ...any) + Error(args ...any) + Errorf(format string, args ...any) + Warning(args ...any) + Warningf(format string, args ...any) + Notice(args ...any) + Noticef(format string, args ...any) + Info(args ...any) + Infof(format string, args ...any) + Debug(args ...any) + Debugf(format string, args ...any) +} + type Logger interface { - Fatal(args ...interface{}) - Fatalf(format string, args ...interface{}) - Panic(args ...interface{}) - Panicf(format string, args ...interface{}) - Critical(args ...interface{}) - Criticalf(format string, args ...interface{}) - Error(args ...interface{}) - Errorf(format string, args ...interface{}) - Warning(args ...interface{}) - Warningf(format string, args ...interface{}) - Notice(args ...interface{}) - Noticef(format string, args ...interface{}) - Info(args ...interface{}) - Infof(format string, args ...interface{}) - Debug(args ...interface{}) - Debugf(format string, args ...interface{}) -} - -// StandardLogger is a default implementation of Logger. Uses github.com/op/go-logging under the hood. -// This logger can prevent any write operations (disabled by default, use .Exclusive() method to enable). -type StandardLogger struct { - logger *logging.Logger - mutex *sync.RWMutex -} - -// NewStandard will create new StandardLogger with specified formatter. -// Usage: -// logger := NewLogger("telegram", logging.ERROR, DefaultLogFormatter()) -func NewStandard(transportCode string, logLevel logging.Level, logFormat logging.Formatter) *StandardLogger { - return &StandardLogger{ - logger: NewBase(os.Stdout, transportCode, logLevel, logFormat), - } -} - -// NewBase is a constructor for underlying logger in the StandardLogger struct. -func NewBase(out io.Writer, transportCode string, logLevel logging.Level, logFormat logging.Formatter) *logging.Logger { - logger := logging.MustGetLogger(transportCode) - logBackend := logging.NewLogBackend(out, "", 0) - formatBackend := logging.NewBackendFormatter(logBackend, logFormat) - backend1Leveled := logging.AddModuleLevel(formatBackend) - backend1Leveled.SetLevel(logLevel, "") - logger.SetBackend(backend1Leveled) - - return logger -} - -// DefaultLogFormatter will return default formatter for logs. -func DefaultLogFormatter() logging.Formatter { - return logging.MustStringFormatter( - `%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`, - ) -} - -// Exclusive makes logger goroutine-safe. -func (l *StandardLogger) Exclusive() *StandardLogger { - if l.mutex == nil { - l.mutex = &sync.RWMutex{} - } - - return l -} - -// SetBaseLogger replaces base logger with the provided instance. -func (l *StandardLogger) SetBaseLogger(logger *logging.Logger) *StandardLogger { - l.logger = logger - return l -} - -// lock locks logger. -func (l *StandardLogger) lock() { - if l.mutex != nil { - l.mutex.Lock() - } -} - -// unlock unlocks logger. -func (l *StandardLogger) unlock() { - if l.mutex != nil { - l.mutex.Unlock() - } -} - -// Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1). -func (l *StandardLogger) Fatal(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Fatal(args...) -} - -// Fatalf is equivalent to l.Critical followed by a call to os.Exit(1). -func (l *StandardLogger) Fatalf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Fatalf(format, args...) -} - -// Panic is equivalent to l.Critical(fmt.Sprint()) followed by a call to panic(). -func (l *StandardLogger) Panic(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Panic(args...) -} - -// Panicf is equivalent to l.Critical followed by a call to panic(). -func (l *StandardLogger) Panicf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Panicf(format, args...) -} - -// Critical logs a message using CRITICAL as log level. -func (l *StandardLogger) Critical(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Critical(args...) -} - -// Criticalf logs a message using CRITICAL as log level. -func (l *StandardLogger) Criticalf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Criticalf(format, args...) -} - -// Error logs a message using ERROR as log level. -func (l *StandardLogger) Error(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Error(args...) -} - -// Errorf logs a message using ERROR as log level. -func (l *StandardLogger) Errorf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Errorf(format, args...) -} - -// Warning logs a message using WARNING as log level. -func (l *StandardLogger) Warning(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Warning(args...) -} - -// Warningf logs a message using WARNING as log level. -func (l *StandardLogger) Warningf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Warningf(format, args...) -} - -// Notice logs a message using NOTICE as log level. -func (l *StandardLogger) Notice(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Notice(args...) -} - -// Noticef logs a message using NOTICE as log level. -func (l *StandardLogger) Noticef(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Noticef(format, args...) -} - -// Info logs a message using INFO as log level. -func (l *StandardLogger) Info(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Info(args...) -} - -// Infof logs a message using INFO as log level. -func (l *StandardLogger) Infof(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Infof(format, args...) -} - -// Debug logs a message using DEBUG as log level. -func (l *StandardLogger) Debug(args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Debug(args...) -} - -// Debugf logs a message using DEBUG as log level. -func (l *StandardLogger) Debugf(format string, args ...interface{}) { - l.lock() - defer l.unlock() - l.logger.Debugf(format, args...) + Handler() slog.Handler + With(args ...any) Logger + WithGroup(name string) Logger + ForAccount(handler, conn, acc any) Logger + Enabled(ctx context.Context, level slog.Level) bool + Log(ctx context.Context, level slog.Level, msg string, args ...any) + LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) + Debug(msg string, args ...any) + DebugContext(ctx context.Context, msg string, args ...any) + Info(msg string, args ...any) + InfoContext(ctx context.Context, msg string, args ...any) + Warn(msg string, args ...any) + WarnContext(ctx context.Context, msg string, args ...any) + Error(msg string, args ...any) + ErrorContext(ctx context.Context, msg string, args ...any) } diff --git a/core/logger/logger_test.go b/core/logger/logger_test.go deleted file mode 100644 index cb9c371..0000000 --- a/core/logger/logger_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package logger - -import ( - "bytes" - "testing" - - "github.com/op/go-logging" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type StandardLoggerTest struct { - suite.Suite - logger *StandardLogger - buf *bytes.Buffer -} - -func TestLogger_NewLogger(t *testing.T) { - logger := NewStandard("code", logging.DEBUG, DefaultLogFormatter()) - assert.NotNil(t, logger) -} - -func TestLogger_DefaultLogFormatter(t *testing.T) { - formatter := DefaultLogFormatter() - - assert.NotNil(t, formatter) - assert.IsType(t, logging.MustStringFormatter(`%{message}`), formatter) -} - -func Test_Logger(t *testing.T) { - suite.Run(t, new(StandardLoggerTest)) -} - -func (t *StandardLoggerTest) SetupSuite() { - t.buf = &bytes.Buffer{} - t.logger = (&StandardLogger{}). - Exclusive(). - SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())) -} - -func (t *StandardLoggerTest) SetupTest() { - t.buf.Reset() -} - -// TODO Cover Fatal and Fatalf (implementation below is no-op) -// func (t *StandardLoggerTest) Test_Fatal() { -// if os.Getenv("FLAG") == "1" { -// t.logger.Fatal("test", "fatal") -// return -// } - -// cmd := exec.Command(os.Args[0], "-test.run=TestGetConfig") -// cmd.Env = append(os.Environ(), "FLAG=1") -// err := cmd.Run() - -// e, ok := err.(*exec.ExitError) -// expectedErrorString := "test fatal" -// t.Assert().Equal(true, ok) -// t.Assert().Equal(expectedErrorString, e.Error()) -// } - -func (t *StandardLoggerTest) Test_Panic() { - defer func() { - t.Assert().NotNil(recover()) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), "panic") - }() - t.logger.Panic("panic") -} - -func (t *StandardLoggerTest) Test_Panicf() { - defer func() { - t.Assert().NotNil(recover()) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), "panicf") - }() - t.logger.Panicf("panicf") -} - -func (t *StandardLoggerTest) Test_Critical() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), "critical") - }() - t.logger.Critical("critical") -} - -func (t *StandardLoggerTest) Test_Criticalf() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), "critical") - }() - t.logger.Criticalf("critical") -} - -func (t *StandardLoggerTest) Test_Warning() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "WARN") - t.Assert().Contains(t.buf.String(), "warning") - }() - t.logger.Warning("warning") -} - -func (t *StandardLoggerTest) Test_Notice() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "NOTI") - t.Assert().Contains(t.buf.String(), "notice") - }() - t.logger.Notice("notice") -} - -func (t *StandardLoggerTest) Test_Info() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), "info") - }() - t.logger.Info("info") -} - -func (t *StandardLoggerTest) Test_Debug() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "DEBU") - t.Assert().Contains(t.buf.String(), "debug") - }() - t.logger.Debug("debug") -} - -func (t *StandardLoggerTest) Test_Warningf() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "WARN") - t.Assert().Contains(t.buf.String(), "warning") - }() - t.logger.Warningf("%s", "warning") -} - -func (t *StandardLoggerTest) Test_Noticef() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "NOTI") - t.Assert().Contains(t.buf.String(), "notice") - }() - t.logger.Noticef("%s", "notice") -} - -func (t *StandardLoggerTest) Test_Infof() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), "info") - }() - t.logger.Infof("%s", "info") -} - -func (t *StandardLoggerTest) Test_Debugf() { - defer func() { - t.Require().Nil(recover()) - t.Assert().Contains(t.buf.String(), "DEBU") - t.Assert().Contains(t.buf.String(), "debug") - }() - t.logger.Debugf("%s", "debug") -} diff --git a/core/logger/nil_handler.go b/core/logger/nil_handler.go new file mode 100644 index 0000000..6b167b3 --- /dev/null +++ b/core/logger/nil_handler.go @@ -0,0 +1,26 @@ +package logger + +import ( + "context" + "log/slog" +) + +var NilHandler slog.Handler = &nilHandler{} + +type nilHandler struct{} + +func (n *nilHandler) Enabled(ctx context.Context, level slog.Level) bool { + return false +} + +func (n *nilHandler) Handle(ctx context.Context, record slog.Record) error { + return nil +} + +func (n *nilHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return n +} + +func (n *nilHandler) WithGroup(name string) slog.Handler { + return n +} diff --git a/core/logger/nil_logger.go b/core/logger/nil_logger.go deleted file mode 100644 index 440b4d0..0000000 --- a/core/logger/nil_logger.go +++ /dev/null @@ -1,45 +0,0 @@ -package logger - -import ( - "fmt" - "os" -) - -// Nil provides Logger implementation that does almost nothing when called. -// Panic, Panicf, Fatal and Fatalf methods still cause panic and immediate program termination respectively. -// All other methods won't do anything at all. -type Nil struct{} - -func (n Nil) Fatal(args ...interface{}) { - os.Exit(1) -} - -func (n Nil) Fatalf(format string, args ...interface{}) { - os.Exit(1) -} - -func (n Nil) Panic(args ...interface{}) { - panic(fmt.Sprint(args...)) -} - -func (n Nil) Panicf(format string, args ...interface{}) { - panic(fmt.Sprintf(format, args...)) -} - -func (n Nil) Critical(args ...interface{}) {} -func (n Nil) Criticalf(format string, args ...interface{}) {} -func (n Nil) Error(args ...interface{}) {} -func (n Nil) Errorf(format string, args ...interface{}) {} -func (n Nil) Warning(args ...interface{}) {} -func (n Nil) Warningf(format string, args ...interface{}) {} -func (n Nil) Notice(args ...interface{}) {} -func (n Nil) Noticef(format string, args ...interface{}) {} -func (n Nil) Info(args ...interface{}) {} -func (n Nil) Infof(format string, args ...interface{}) {} -func (n Nil) Debug(args ...interface{}) {} -func (n Nil) Debugf(format string, args ...interface{}) {} - -// NewNil is a Nil logger constructor. -func NewNil() Logger { - return &Nil{} -} diff --git a/core/logger/nil_logger_test.go b/core/logger/nil_logger_test.go deleted file mode 100644 index f41c03e..0000000 --- a/core/logger/nil_logger_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package logger - -import ( - "bytes" - "io" - "os" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type NilTest struct { - suite.Suite - logger Logger - realStdout *os.File - r *os.File - w *os.File -} - -func TestNilLogger(t *testing.T) { - suite.Run(t, new(NilTest)) -} - -func (t *NilTest) SetupSuite() { - t.logger = NewNil() -} - -func (t *NilTest) SetupTest() { - t.realStdout = os.Stdout - t.r, t.w, _ = os.Pipe() - os.Stdout = t.w -} - -func (t *NilTest) TearDownTest() { - if t.realStdout != nil { - t.Require().NoError(t.w.Close()) - os.Stdout = t.realStdout - } -} - -func (t *NilTest) readStdout() string { - outC := make(chan string) - go func() { - var buf bytes.Buffer - _, err := io.Copy(&buf, t.r) - t.Require().NoError(err) - outC <- buf.String() - close(outC) - }() - - t.Require().NoError(t.w.Close()) - os.Stdout = t.realStdout - t.realStdout = nil - - select { - case c := <-outC: - return c - case <-time.After(time.Second): - return "" - } -} - -func (t *NilTest) Test_Noop() { - t.logger.Critical("message") - t.logger.Criticalf("message") - t.logger.Error("message") - t.logger.Errorf("message") - t.logger.Warning("message") - t.logger.Warningf("message") - t.logger.Notice("message") - t.logger.Noticef("message") - t.logger.Info("message") - t.logger.Infof("message") - t.logger.Debug("message") - t.logger.Debugf("message") - - t.Assert().Empty(t.readStdout()) -} - -func (t *NilTest) Test_Panic() { - t.Assert().Panics(func() { - t.logger.Panic("") - }) -} - -func (t *NilTest) Test_Panicf() { - t.Assert().Panics(func() { - t.logger.Panicf("") - }) -} diff --git a/core/logger/prefix_decorator.go b/core/logger/prefix_decorator.go deleted file mode 100644 index 2e83912..0000000 --- a/core/logger/prefix_decorator.go +++ /dev/null @@ -1,111 +0,0 @@ -package logger - -import "github.com/op/go-logging" - -// PrefixAware is implemented if the logger allows you to change the prefix. -type PrefixAware interface { - SetPrefix(string) - Prefix() string -} - -// PrefixedLogger is a base interface for the logger with prefix. -type PrefixedLogger interface { - Logger - PrefixAware -} - -// PrefixDecorator is an implementation of the PrefixedLogger. It will allow you to decorate any Logger with -// the provided predefined prefix. -type PrefixDecorator struct { - backend Logger - prefix []interface{} -} - -// DecorateWithPrefix using provided base logger and provided prefix. -// No internal state of the base logger will be touched. -func DecorateWithPrefix(backend Logger, prefix string) PrefixedLogger { - return &PrefixDecorator{backend: backend, prefix: []interface{}{prefix}} -} - -// NewWithPrefix returns logger with prefix. It uses StandardLogger under the hood. -func NewWithPrefix(transportCode, prefix string, logLevel logging.Level, logFormat logging.Formatter) PrefixedLogger { - return DecorateWithPrefix(NewStandard(transportCode, logLevel, logFormat), prefix) -} - -// SetPrefix will replace existing prefix with the provided value. -// Use this format for prefixes: "prefix here:" - omit space at the end (it will be inserted automatically). -func (p *PrefixDecorator) SetPrefix(prefix string) { - p.prefix = []interface{}{prefix} -} - -func (p *PrefixDecorator) getFormat(fmt string) string { - return p.prefix[0].(string) + " " + fmt -} - -func (p *PrefixDecorator) Prefix() string { - return p.prefix[0].(string) -} - -func (p *PrefixDecorator) Fatal(args ...interface{}) { - p.backend.Fatal(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Fatalf(format string, args ...interface{}) { - p.backend.Fatalf(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Panic(args ...interface{}) { - p.backend.Panic(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Panicf(format string, args ...interface{}) { - p.backend.Panicf(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Critical(args ...interface{}) { - p.backend.Critical(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Criticalf(format string, args ...interface{}) { - p.backend.Criticalf(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Error(args ...interface{}) { - p.backend.Error(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Errorf(format string, args ...interface{}) { - p.backend.Errorf(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Warning(args ...interface{}) { - p.backend.Warning(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Warningf(format string, args ...interface{}) { - p.backend.Warningf(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Notice(args ...interface{}) { - p.backend.Notice(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Noticef(format string, args ...interface{}) { - p.backend.Noticef(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Info(args ...interface{}) { - p.backend.Info(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Infof(format string, args ...interface{}) { - p.backend.Infof(p.getFormat(format), args...) -} - -func (p *PrefixDecorator) Debug(args ...interface{}) { - p.backend.Debug(append(p.prefix, args...)...) -} - -func (p *PrefixDecorator) Debugf(format string, args ...interface{}) { - p.backend.Debugf(p.getFormat(format), args...) -} diff --git a/core/logger/prefix_decorator_test.go b/core/logger/prefix_decorator_test.go deleted file mode 100644 index 5b66ec5..0000000 --- a/core/logger/prefix_decorator_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package logger - -import ( - "bytes" - "testing" - - "github.com/op/go-logging" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -const testPrefix = "TestPrefix:" - -type PrefixDecoratorTest struct { - suite.Suite - buf *bytes.Buffer - logger PrefixedLogger -} - -func TestPrefixDecorator(t *testing.T) { - suite.Run(t, new(PrefixDecoratorTest)) -} - -func TestNewWithPrefix(t *testing.T) { - buf := &bytes.Buffer{} - logger := NewWithPrefix("code", "Prefix:", logging.DEBUG, DefaultLogFormatter()) - logger.(*PrefixDecorator).backend.(*StandardLogger). - SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter())) - logger.Debugf("message %s", "text") - - assert.Contains(t, buf.String(), "Prefix: message text") -} - -func (t *PrefixDecoratorTest) SetupSuite() { - t.buf = &bytes.Buffer{} - t.logger = DecorateWithPrefix((&StandardLogger{}). - SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())), testPrefix) -} - -func (t *PrefixDecoratorTest) SetupTest() { - t.buf.Reset() - t.logger.SetPrefix(testPrefix) -} - -func (t *PrefixDecoratorTest) Test_SetPrefix() { - t.logger.Info("message") - t.Assert().Equal(testPrefix, t.logger.Prefix()) - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), testPrefix+" message") - - t.logger.SetPrefix(testPrefix + testPrefix) - t.logger.Info("message") - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), testPrefix+testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Panic() { - t.Require().Panics(func() { - t.logger.Panic("message") - }) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Panicf() { - t.Require().Panics(func() { - t.logger.Panicf("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Critical() { - t.Require().NotPanics(func() { - t.logger.Critical("message") - }) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Criticalf() { - t.Require().NotPanics(func() { - t.logger.Criticalf("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "CRIT") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Error() { - t.Require().NotPanics(func() { - t.logger.Error("message") - }) - t.Assert().Contains(t.buf.String(), "ERRO") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Errorf() { - t.Require().NotPanics(func() { - t.logger.Errorf("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "ERRO") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Warning() { - t.Require().NotPanics(func() { - t.logger.Warning("message") - }) - t.Assert().Contains(t.buf.String(), "WARN") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Warningf() { - t.Require().NotPanics(func() { - t.logger.Warningf("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "WARN") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Notice() { - t.Require().NotPanics(func() { - t.logger.Notice("message") - }) - t.Assert().Contains(t.buf.String(), "NOTI") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Noticef() { - t.Require().NotPanics(func() { - t.logger.Noticef("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "NOTI") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Info() { - t.Require().NotPanics(func() { - t.logger.Info("message") - }) - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Infof() { - t.Require().NotPanics(func() { - t.logger.Infof("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "INFO") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Debug() { - t.Require().NotPanics(func() { - t.logger.Debug("message") - }) - t.Assert().Contains(t.buf.String(), "DEBU") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} - -func (t *PrefixDecoratorTest) Test_Debugf() { - t.Require().NotPanics(func() { - t.logger.Debugf("%s", "message") - }) - t.Assert().Contains(t.buf.String(), "DEBU") - t.Assert().Contains(t.buf.String(), testPrefix+" message") -} diff --git a/core/logger/zabbix_collector_adapter.go b/core/logger/zabbix_collector_adapter.go new file mode 100644 index 0000000..0dc9067 --- /dev/null +++ b/core/logger/zabbix_collector_adapter.go @@ -0,0 +1,19 @@ +package logger + +import ( + "fmt" + + metrics "github.com/retailcrm/zabbix-metrics-collector" +) + +type zabbixCollectorAdapter struct { + log Logger +} + +func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) { + a.log.Error(fmt.Sprintf(format, args...)) +} + +func ZabbixCollectorAdapter(log Logger) metrics.ErrorLogger { + return &zabbixCollectorAdapter{log: log} +} diff --git a/core/module_features_uploader.go b/core/module_features_uploader.go index f2979d9..33a9e44 100644 --- a/core/module_features_uploader.go +++ b/core/module_features_uploader.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "html/template" + "log/slog" "os" "github.com/aws/aws-sdk-go-v2/aws" @@ -64,14 +65,13 @@ func NewModuleFeaturesUploader( awsConfig.WithCredentialsProvider(customProvider), ) if err != nil { - log.Fatal(err) + log.Error("cannot load S3 configuration", logger.ErrAttr(err)) return nil } client := manager.NewUploader(s3.NewFromConfig(cfg)) - if err != nil { - log.Fatal(err) + log.Error("cannot load S3 configuration", logger.ErrAttr(err)) return nil } @@ -87,18 +87,18 @@ func NewModuleFeaturesUploader( } func (s *ModuleFeaturesUploader) Upload() { - s.log.Debugf("upload module features started...") + s.log.Debug("upload module features started...") content, err := os.ReadFile(s.featuresFilename) if err != nil { - s.log.Errorf("cannot read markdown file %s %s", s.featuresFilename, err.Error()) + s.log.Error("cannot read markdown file %s %s", slog.String("fileName", s.featuresFilename), logger.ErrAttr(err)) return } for _, lang := range languages { translated, err := s.translate(content, lang) if err != nil { - s.log.Errorf("cannot translate module features file to %s: %s", lang.String(), err.Error()) + s.log.Error("cannot translate module features file", slog.String("lang", lang.String()), logger.ErrAttr(err)) continue } @@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() { resp, err := s.uploadFile(html, lang.String()) if err != nil { - s.log.Errorf("cannot upload file %s: %s", lang.String(), err.Error()) + s.log.Error("cannot upload file", slog.String("lang", lang.String()), logger.ErrAttr(err)) continue } @@ -114,7 +114,7 @@ func (s *ModuleFeaturesUploader) Upload() { } fmt.Println() - s.log.Debugf("upload module features finished") + s.log.Debug("upload module features finished") } func (s *ModuleFeaturesUploader) translate(content []byte, lang language.Tag) ([]byte, error) { diff --git a/core/module_features_uploader_test.go b/core/module_features_uploader_test.go index 60bc9b1..936d688 100644 --- a/core/module_features_uploader_test.go +++ b/core/module_features_uploader_test.go @@ -1,19 +1,18 @@ package core import ( - "bytes" "context" - "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "os" "testing" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/retailcrm/mg-transport-core/v2/core/util/testutil" + "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/op/go-logging" "github.com/stretchr/testify/suite" "golang.org/x/text/language" "github.com/retailcrm/mg-transport-core/v2/core/config" - "github.com/retailcrm/mg-transport-core/v2/core/logger" ) type ModuleFeaturesUploaderTest struct { @@ -36,8 +35,7 @@ func (t *ModuleFeaturesUploaderTest) TearDownSuite() { } func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() { - logs := &bytes.Buffer{} - log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter()) + log := testutil.NewBufferedLogger() conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt") @@ -50,8 +48,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeature } func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() { - logs := &bytes.Buffer{} - log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter()) + log := testutil.NewBufferedLogger() conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt") content := "test content " + t.localizer.GetLocalizedMessage("message") @@ -62,8 +59,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() { } func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() { - logs := &bytes.Buffer{} - log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter()) + log := testutil.NewBufferedLogger() conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md") content := "test content" diff --git a/core/sentry.go b/core/sentry.go index 3511c1c..b7d2e0c 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "log/slog" "net" "net/http" "net/http/httputil" @@ -22,9 +23,6 @@ import ( "github.com/gin-gonic/gin" ) -// reset is borrowed directly from the gin. -const reset = "\033[0m" - // ErrorHandlerFunc will handle errors. type ErrorHandlerFunc func(recovery interface{}, c *gin.Context) @@ -142,17 +140,17 @@ func (s *Sentry) SentryMiddlewares() []gin.HandlerFunc { // obtainErrorLogger extracts logger from the context or builds it right here from tags used in Sentry events // Those tags can be configured with SentryLoggerConfig field. -func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger { +func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.Logger { if item, ok := c.Get("logger"); ok { - if accountLogger, ok := item.(logger.AccountLogger); ok { - return accountLogger + if ctxLogger, ok := item.(logger.Logger); ok { + return ctxLogger } } connectionID := "{no connection ID}" accountID := "{no account ID}" if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" { - return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID) + return s.Logger.ForAccount("Sentry", connectionID, accountID) } for tag := range s.tagsFromContext(c) { @@ -164,7 +162,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger { } } - return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID) + return s.Logger.ForAccount("Sentry", connectionID, accountID) } // tagsSetterMiddleware sets event tags into Sentry events. @@ -201,13 +199,13 @@ func (s *Sentry) exceptionCaptureMiddleware() gin.HandlerFunc { // nolint:gocogn for _, err := range publicErrors { messages[index] = err.Error() s.CaptureException(c, err) - l.Error(err) + l.Error(err.Error()) index++ } for _, err := range privateErrors { s.CaptureException(c, err) - l.Error(err) + l.Error(err.Error()) } if privateLen > 0 || recovery != nil { @@ -250,8 +248,9 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint } } if l != nil { - stack := stacktrace.FormattedStack(3, l.Prefix()+" ") - formattedErr := fmt.Sprintf("%s %s", l.Prefix(), err) + // TODO: Check if we can output stacktraces with prefix data like before if we really need it. + stack := stacktrace.FormattedStack(3, "trace: ") + formattedErr := logger.ErrAttr(err) httpRequest, _ := httputil.DumpRequest(c.Request, false) headers := strings.Split(string(httpRequest), "\r\n") for idx, header := range headers { @@ -259,18 +258,17 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint if current[0] == "Authorization" { headers[idx] = current[0] + ": *" } - headers[idx] = l.Prefix() + " " + headers[idx] + headers[idx] = "header: " + headers[idx] } - headersToStr := strings.Join(headers, "\r\n") + headersToStr := slog.String("headers", strings.Join(headers, "\r\n")) + formattedStack := slog.String("stacktrace", string(stack)) switch { case brokenPipe: - l.Errorf("%s\n%s%s", formattedErr, headersToStr, reset) + l.Error("error", formattedErr, headersToStr) case gin.IsDebugging(): - l.Errorf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), headersToStr, formattedErr, stack, reset) + l.Error("[Recovery] panic recovered", headersToStr, formattedErr, formattedStack) default: - l.Errorf("[Recovery] %s panic recovered:\n%s\n%s%s", - timeFormat(time.Now()), formattedErr, stack, reset) + l.Error("[Recovery] panic recovered", formattedErr, formattedStack) } } if brokenPipe { diff --git a/core/sentry_test.go b/core/sentry_test.go index 9a8d319..fe574f2 100644 --- a/core/sentry_test.go +++ b/core/sentry_test.go @@ -2,7 +2,6 @@ package core import ( "errors" - "fmt" "net/http" "net/http/httptest" "sync" @@ -278,7 +277,7 @@ func (s *SentryTest) TestSentry_CaptureException() { func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() { ctx, _ := s.ginCtxMock() - log := logger.DecorateForAccount(testutil.NewBufferedLogger(), "component", "conn", "acc") + log := testutil.NewBufferedLogger().ForAccount("component", "conn", "acc") ctx.Set("logger", log) s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx)) @@ -299,12 +298,8 @@ func (s *SentryTest) TestSentry_obtainErrorLogger_Constructed() { s.Assert().NotNil(log) s.Assert().NotNil(logNoConfig) - s.Assert().Implements((*logger.AccountLogger)(nil), log) - s.Assert().Implements((*logger.AccountLogger)(nil), logNoConfig) - s.Assert().Equal( - fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "{no connection ID}", "{no account ID}"), - logNoConfig.Prefix()) - s.Assert().Equal(fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "conn_url", "acc_name"), log.Prefix()) + s.Assert().Implements((*logger.Logger)(nil), log) + s.Assert().Implements((*logger.Logger)(nil), logNoConfig) } func (s *SentryTest) TestSentry_MiddlewaresError() { diff --git a/core/util/httputil/http_client_builder.go b/core/util/httputil/http_client_builder.go index 3e1a17b..0948fcb 100644 --- a/core/util/httputil/http_client_builder.go +++ b/core/util/httputil/http_client_builder.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "log/slog" "net" "net/http" "net/url" @@ -231,11 +232,11 @@ func (b *HTTPClientBuilder) buildMocks() error { } if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { - b.logf("Mock address is \"%s\"\n", net.JoinHostPort(b.mockHost, b.mockPort)) - b.logf("Mocked domains: ") + b.log("Mock address has been set", slog.String("address", net.JoinHostPort(b.mockHost, b.mockPort))) + b.log("Mocked domains: ") for _, domain := range b.mockedDomains { - b.logf(" - %s\n", domain) + b.log(fmt.Sprintf(" - %s\n", domain)) } b.httpTransport.Proxy = nil @@ -259,7 +260,7 @@ func (b *HTTPClientBuilder) buildMocks() error { addr = net.JoinHostPort(b.mockHost, b.mockPort) } - b.logf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr) + b.log(fmt.Sprintf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr)) } } @@ -270,13 +271,13 @@ func (b *HTTPClientBuilder) buildMocks() error { return nil } -// logf prints logs via Engine or via fmt.Printf. -func (b *HTTPClientBuilder) logf(format string, args ...interface{}) { +// log prints logs via Engine or via fmt.Println. +func (b *HTTPClientBuilder) log(msg string, args ...interface{}) { if b.logging { if b.logger != nil { - b.logger.Infof(format, args...) + b.logger.Info(msg, args...) } else { - fmt.Printf(format, args...) + fmt.Println(append([]any{msg}, args...)) } } } diff --git a/core/util/httputil/http_client_builder_test.go b/core/util/httputil/http_client_builder_test.go index 277e281..dae78c4 100644 --- a/core/util/httputil/http_client_builder_test.go +++ b/core/util/httputil/http_client_builder_test.go @@ -16,7 +16,6 @@ import ( "testing" "time" - "github.com/op/go-logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -137,14 +136,14 @@ func (t *HTTPClientBuilderTest) Test_buildMocks() { } func (t *HTTPClientBuilderTest) Test_WithLogger() { - logger := logger.NewStandard("telegram", logging.ERROR, logger.DefaultLogFormatter()) builder := NewHTTPClientBuilder() require.Nil(t.T(), builder.logger) builder.WithLogger(nil) assert.Nil(t.T(), builder.logger) - builder.WithLogger(logger) + log := logger.NewDefaultText() + builder.WithLogger(log) assert.NotNil(t.T(), builder.logger) } @@ -153,7 +152,7 @@ func (t *HTTPClientBuilderTest) Test_logf() { assert.Nil(t.T(), recover()) }() - t.builder.logf("test %s", "string") + t.builder.log(fmt.Sprintf("test %s", "string")) } func (t *HTTPClientBuilderTest) Test_Build() { diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index 79619d6..f28713d 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -1,13 +1,9 @@ package testutil import ( - "bytes" "fmt" "io" - "os" - "sync" - - "github.com/op/go-logging" + "log/slog" "github.com/retailcrm/mg-transport-core/v2/core/logger" ) @@ -29,119 +25,46 @@ type BufferedLogger interface { // BufferLogger is an implementation of the BufferedLogger. type BufferLogger struct { - buf bytes.Buffer - rw sync.RWMutex + logger.Default + buf LockableBuffer } // NewBufferedLogger returns new BufferedLogger instance. func NewBufferedLogger() BufferedLogger { - return &BufferLogger{} + bl := &BufferLogger{} + bl.Logger = slog.New(slog.NewTextHandler(&bl.buf, logger.DefaultOpts)) + return bl +} + +// With doesn't do anything here and only added for backwards compatibility with the interface. +func (l *BufferLogger) With(args ...any) logger.Logger { + return &BufferLogger{ + Default: logger.Default{ + Logger: l.Logger.With(args...), + }, + } +} + +func (l *BufferLogger) ForAccount(handler, conn, acc any) logger.Logger { + return l.With(slog.Any(logger.HandlerAttr, handler), slog.Any(logger.ConnectionAttr, conn), slog.Any(logger.AccountAttr, acc)) } // Read bytes from the logger buffer. io.Reader implementation. func (l *BufferLogger) Read(p []byte) (n int, err error) { - defer l.rw.RUnlock() - l.rw.RLock() return l.buf.Read(p) } // String contents of the logger buffer. fmt.Stringer implementation. func (l *BufferLogger) String() string { - defer l.rw.RUnlock() - l.rw.RLock() return l.buf.String() } // Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents. func (l *BufferLogger) Bytes() []byte { - defer l.rw.RUnlock() - l.rw.RLock() return l.buf.Bytes() } // Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents. func (l *BufferLogger) Reset() { - defer l.rw.Unlock() - l.rw.Lock() l.buf.Reset() } - -func (l *BufferLogger) write(level logging.Level, args ...interface{}) { - defer l.rw.Unlock() - l.rw.Lock() - l.buf.WriteString(fmt.Sprintln(append([]interface{}{level.String(), "=>"}, args...)...)) -} - -func (l *BufferLogger) writef(level logging.Level, format string, args ...interface{}) { - defer l.rw.Unlock() - l.rw.Lock() - l.buf.WriteString(fmt.Sprintf(level.String()+" => "+format, args...)) -} - -func (l *BufferLogger) Fatal(args ...interface{}) { - l.write(logging.CRITICAL, args...) - os.Exit(1) -} - -func (l *BufferLogger) Fatalf(format string, args ...interface{}) { - l.writef(logging.CRITICAL, format, args...) - os.Exit(1) -} - -func (l *BufferLogger) Panic(args ...interface{}) { - l.write(logging.CRITICAL, args...) - panic(fmt.Sprint(args...)) -} - -func (l *BufferLogger) Panicf(format string, args ...interface{}) { - l.writef(logging.CRITICAL, format, args...) - panic(fmt.Sprintf(format, args...)) -} - -func (l *BufferLogger) Critical(args ...interface{}) { - l.write(logging.CRITICAL, args...) -} - -func (l *BufferLogger) Criticalf(format string, args ...interface{}) { - l.writef(logging.CRITICAL, format, args...) -} - -func (l *BufferLogger) Error(args ...interface{}) { - l.write(logging.ERROR, args...) -} - -func (l *BufferLogger) Errorf(format string, args ...interface{}) { - l.writef(logging.ERROR, format, args...) -} - -func (l *BufferLogger) Warning(args ...interface{}) { - l.write(logging.WARNING, args...) -} - -func (l *BufferLogger) Warningf(format string, args ...interface{}) { - l.writef(logging.WARNING, format, args...) -} - -func (l *BufferLogger) Notice(args ...interface{}) { - l.write(logging.NOTICE, args...) -} - -func (l *BufferLogger) Noticef(format string, args ...interface{}) { - l.writef(logging.NOTICE, format, args...) -} - -func (l *BufferLogger) Info(args ...interface{}) { - l.write(logging.INFO, args...) -} - -func (l *BufferLogger) Infof(format string, args ...interface{}) { - l.writef(logging.INFO, format, args...) -} - -func (l *BufferLogger) Debug(args ...interface{}) { - l.write(logging.DEBUG, args...) -} - -func (l *BufferLogger) Debugf(format string, args ...interface{}) { - l.writef(logging.DEBUG, format, args...) -} diff --git a/core/util/testutil/buffer_logger_test.go b/core/util/testutil/buffer_logger_test.go index 8a76d2a..0bfa23e 100644 --- a/core/util/testutil/buffer_logger_test.go +++ b/core/util/testutil/buffer_logger_test.go @@ -4,7 +4,6 @@ import ( "io" "testing" - "github.com/op/go-logging" "github.com/stretchr/testify/suite" ) @@ -30,17 +29,17 @@ func (t *BufferLoggerTest) Test_Read() { data, err := io.ReadAll(t.logger) t.Require().NoError(err) - t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), data) + t.Assert().Contains(string(data), "level=DEBUG msg=test") } func (t *BufferLoggerTest) Test_Bytes() { t.logger.Debug("test") - t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), t.logger.Bytes()) + t.Assert().Contains(string(t.logger.Bytes()), "level=DEBUG msg=test") } func (t *BufferLoggerTest) Test_String() { t.logger.Debug("test") - t.Assert().Equal(logging.DEBUG.String()+" => test\n", t.logger.String()) + t.Assert().Contains(t.logger.String(), "level=DEBUG msg=test") } func (t *BufferLoggerTest) TestRace() { diff --git a/core/util/testutil/lockable_buffer.go b/core/util/testutil/lockable_buffer.go new file mode 100644 index 0000000..ae71356 --- /dev/null +++ b/core/util/testutil/lockable_buffer.go @@ -0,0 +1,150 @@ +package testutil + +import ( + "bytes" + "io" + "sync" +) + +type LockableBuffer struct { + buf bytes.Buffer + rw sync.RWMutex +} + +func (b *LockableBuffer) Bytes() []byte { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Bytes() +} + +func (b *LockableBuffer) AvailableBuffer() []byte { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.AvailableBuffer() +} + +func (b *LockableBuffer) String() string { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.String() +} + +func (b *LockableBuffer) Len() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Len() +} + +func (b *LockableBuffer) Cap() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Cap() +} + +func (b *LockableBuffer) Available() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Available() +} + +func (b *LockableBuffer) Truncate(n int) { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Truncate(n) +} + +func (b *LockableBuffer) Reset() { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Reset() +} + +func (b *LockableBuffer) Grow(n int) { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Grow(n) +} + +func (b *LockableBuffer) Write(p []byte) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Write(p) +} + +func (b *LockableBuffer) WriteString(s string) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteString(s) +} + +func (b *LockableBuffer) ReadFrom(r io.Reader) (n int64, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadFrom(r) +} + +func (b *LockableBuffer) WriteTo(w io.Writer) (n int64, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteTo(w) +} + +func (b *LockableBuffer) WriteByte(c byte) error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteByte(c) +} + +func (b *LockableBuffer) WriteRune(r rune) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteRune(r) +} + +func (b *LockableBuffer) Read(p []byte) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Read(p) +} + +func (b *LockableBuffer) Next(n int) []byte { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Next(n) +} + +func (b *LockableBuffer) ReadByte() (byte, error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadByte() +} + +func (b *LockableBuffer) ReadRune() (r rune, size int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadRune() +} + +func (b *LockableBuffer) UnreadRune() error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.UnreadRune() +} + +func (b *LockableBuffer) UnreadByte() error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.UnreadByte() +} + +func (b *LockableBuffer) ReadBytes(delim byte) (line []byte, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadBytes(delim) +} + +func (b *LockableBuffer) ReadString(delim byte) (line string, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadString(delim) +} diff --git a/core/util/utils.go b/core/util/utils.go index 2ff8f5f..92914c0 100644 --- a/core/util/utils.go +++ b/core/util/utils.go @@ -132,7 +132,7 @@ func (u *Utils) GenerateToken() string { func (u *Utils) GetAPIClient( url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) { client := retailcrm.New(url, key). - WithLogger(retailcrm.DebugLoggerAdapter(u.Logger)) + WithLogger(logger.APIClientAdapter(u.Logger)) client.Debug = u.IsDebug cr, status, err := client.APICredentials() diff --git a/core/util/utils_test.go b/core/util/utils_test.go index ce7a015..2890c54 100644 --- a/core/util/utils_test.go +++ b/core/util/utils_test.go @@ -9,7 +9,7 @@ import ( "time" "github.com/h2non/gock" - "github.com/op/go-logging" + retailcrm "github.com/retailcrm/api-client-go/v2" v1 "github.com/retailcrm/mg-transport-api-client-go/v1" "github.com/stretchr/testify/assert" @@ -38,7 +38,7 @@ func mgClient() *v1.MgClient { } func (u *UtilsTest) SetupSuite() { - logger := logger.NewStandard("code", logging.DEBUG, logger.DefaultLogFormatter()) + logger := logger.NewDefaultText() awsConfig := config.AWS{ AccessKeyID: "access key id (will be removed)", SecretAccessKey: "secret access key", diff --git a/go.mod b/go.mod index 0a8c1e7..9d9e50e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/retailcrm/mg-transport-core/v2 -go 1.18 +go 1.21 require ( github.com/DATA-DOG/go-sqlmock v1.3.3 diff --git a/go.sum b/go.sum index 6f7ec25..ae08450 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= From 2d1b2351eeaa9b18bf94c210951c6761b5cbf98a Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 14:41:37 +0300 Subject: [PATCH 02/26] add json unmarshaling helper --- core/util/utils.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/util/utils.go b/core/util/utils.go index 92914c0..58280fa 100644 --- a/core/util/utils.go +++ b/core/util/utils.go @@ -1,11 +1,13 @@ package util import ( + "bytes" // nolint:gosec "crypto/sha1" "crypto/sha256" "encoding/json" "fmt" + "io" "net/http" "regexp" "strings" @@ -16,6 +18,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/gin-gonic/gin" retailcrm "github.com/retailcrm/api-client-go/v2" v1 "github.com/retailcrm/mg-transport-api-client-go/v1" @@ -281,3 +284,15 @@ func GetCurrencySymbol(code string) string { func FormatCurrencyValue(value float32) string { return fmt.Sprintf("%.2f", value) } + +// BindJSONWithRaw will perform usual ShouldBindJSON and will return the original body data. +func BindJSONWithRaw(c *gin.Context, obj any) ([]byte, error) { + closer := c.Request.Body + defer func() { _ = closer.Close() }() + data, err := io.ReadAll(closer) + if err != nil { + return []byte, err + } + c.Request.Body = io.NopCloser(bytes.NewReader(data)) + return data, c.ShouldBindJSON(obj) +} From b97f05e66638bc7e3a109a068d180ae8e5efa392 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:16:00 +0300 Subject: [PATCH 03/26] add more predetermined log keys --- core/logger/attrs.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 293e0b5..39e304a 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -9,6 +9,9 @@ const ( CounterIDAttr = "counterId" ErrorAttr = "error" FailureMessageAttr = "failureMessage" + HTTPMethodAttr = "httpMethod" + HTTPStatusAttr = "httpStatusCode" + HTTPStatusNameAttr = "httpStatusName" ) func ErrAttr(err any) slog.Attr { From 4c1f29ba01cfaf09b579379dfb25d6f8d38e2a05 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:19:01 +0300 Subject: [PATCH 04/26] add HTTPStatus helper --- core/logger/attrs.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 39e304a..6e802c8 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -1,6 +1,9 @@ package logger -import "log/slog" +import ( + "log/slog" + "net/http" +) const ( HandlerAttr = "handler" @@ -20,3 +23,7 @@ func ErrAttr(err any) slog.Attr { } return slog.Any(ErrorAttr, err) } + +func HTTPStatus(code int) (slog.Attr, slog.Attr) { + return slog.Int(HTTPStatusAttr, code), slog.String(HTTPStatusNameAttr, http.StatusText(code)) +} From 57d7084b743d16df8731923b40aca93202fd3c58 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:20:04 +0300 Subject: [PATCH 05/26] update HTTPStatus helper --- core/logger/attrs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 6e802c8..a8cc17b 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -24,6 +24,6 @@ func ErrAttr(err any) slog.Attr { return slog.Any(ErrorAttr, err) } -func HTTPStatus(code int) (slog.Attr, slog.Attr) { - return slog.Int(HTTPStatusAttr, code), slog.String(HTTPStatusNameAttr, http.StatusText(code)) +func HTTPStatus(code int) []any { + return []any{slog.Int(HTTPStatusAttr, code), slog.String(HTTPStatusNameAttr, http.StatusText(code))} } From 8eb0d6b31f2cf4ae4784934a2a6ff0cc3a809e03 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:30:01 +0300 Subject: [PATCH 06/26] add Body helper --- core/logger/attrs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index a8cc17b..518d180 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -12,6 +12,7 @@ const ( CounterIDAttr = "counterId" ErrorAttr = "error" FailureMessageAttr = "failureMessage" + BodyAttr = "body" HTTPMethodAttr = "httpMethod" HTTPStatusAttr = "httpStatusCode" HTTPStatusNameAttr = "httpStatusName" @@ -27,3 +28,7 @@ func ErrAttr(err any) slog.Attr { func HTTPStatus(code int) []any { return []any{slog.Int(HTTPStatusAttr, code), slog.String(HTTPStatusNameAttr, http.StatusText(code))} } + +func Body(val any) slog.Attr { + return slog.Any(BodyAttr, val) +} From 7f0709888ca9a13d30165c7ba887c6f41a8cf5a2 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:36:55 +0300 Subject: [PATCH 07/26] fix http status helpers --- core/logger/attrs.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 518d180..8674c7c 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -25,8 +25,12 @@ func ErrAttr(err any) slog.Attr { return slog.Any(ErrorAttr, err) } -func HTTPStatus(code int) []any { - return []any{slog.Int(HTTPStatusAttr, code), slog.String(HTTPStatusNameAttr, http.StatusText(code))} +func HTTPStatusCode(code int) slog.Attr { + return slog.Int(HTTPStatusAttr, code) +} + +func HTTPStatusName(code int) slog.Attr { + return slog.String(HTTPStatusNameAttr, http.StatusText(code)) } func Body(val any) slog.Attr { From 4bca4c491e5dc6982835e92e60e11dd78ac9118f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 15:40:46 +0300 Subject: [PATCH 08/26] fix for BuildJSONWithRaw --- core/util/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/util/utils.go b/core/util/utils.go index 58280fa..6e098cc 100644 --- a/core/util/utils.go +++ b/core/util/utils.go @@ -291,7 +291,7 @@ func BindJSONWithRaw(c *gin.Context, obj any) ([]byte, error) { defer func() { _ = closer.Close() }() data, err := io.ReadAll(closer) if err != nil { - return []byte, err + return []byte{}, err } c.Request.Body = io.NopCloser(bytes.NewReader(data)) return data, c.ShouldBindJSON(obj) From e9e9c4c442ca0a06b9ad3c8d071074dd8e264464 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 16:55:46 +0300 Subject: [PATCH 09/26] update adapters & fixes --- core/healthcheck/processor.go | 4 ++-- core/healthcheck/processor_test.go | 14 +++++++------- core/job_manager.go | 2 +- core/logger/attrs.go | 12 ++++++++++-- core/logger/mg_transport_client_adapter.go | 18 ++++++++++++++++++ core/module_features_uploader.go | 10 +++++----- core/sentry.go | 2 +- go.mod | 2 +- go.sum | 2 ++ 9 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 core/logger/mg_transport_client_adapter.go diff --git a/core/healthcheck/processor.go b/core/healthcheck/processor.go index cb178d3..e75ca93 100644 --- a/core/healthcheck/processor.go +++ b/core/healthcheck/processor.go @@ -43,7 +43,7 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn err := c.Notifier(apiURL, apiKey, counter.Message()) if err != nil { c.debugLog("cannot send notification for counter", - slog.Int(logger.CounterIDAttr, id), logger.ErrAttr(err), slog.String(logger.FailureMessageAttr, counter.Message())) + slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message())) } counter.FailureProcessed() return true @@ -81,7 +81,7 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang)) if err != nil { c.debugLog("cannot send notification for counter", - slog.Int(logger.CounterIDAttr, id), logger.ErrAttr(err), slog.String(logger.FailureMessageAttr, counter.Message())) + slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message())) } counter.CountersProcessed() return true diff --git a/core/healthcheck/processor_test.go b/core/healthcheck/processor_test.go index ecaa6a1..8ffa8d5 100644 --- a/core/healthcheck/processor_test.go +++ b/core/healthcheck/processor_test.go @@ -96,7 +96,7 @@ func (t *CounterProcessorTest) Test_FailureProcessed() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "skipping counter because its failure is already processed") - t.Assert().Contains(log.String(), "id=1") + t.Assert().Contains(log.String(), "counterId=1") } func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { @@ -109,7 +109,7 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), "id=1") + t.Assert().Contains(log.String(), "counterId=1") } func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { @@ -124,9 +124,9 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot send notification for counter") - t.Assert().Contains(log.String(), "id=1") + t.Assert().Contains(log.String(), "counterId=1") t.Assert().Contains(log.String(), `error="http status code: 500"`) - t.Assert().Contains(log.String(), `message="error message"`) + t.Assert().Contains(log.String(), `failureMessage="error message"`) t.Assert().Equal(t.apiURL, n.apiURL) t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal("error message", n.message) @@ -160,7 +160,7 @@ func (t *CounterProcessorTest) Test_TooFewRequests() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), - fmt.Sprintf(`msg="skipping counter because it has too few requests" id=%d minRequests=%d`, 1, DefaultMinRequests)) + fmt.Sprintf(`msg="skipping counter because it has too few requests" counterId=%d minRequests=%d`, 1, DefaultMinRequests)) } func (t *CounterProcessorTest) Test_ThresholdNotPassed() { @@ -206,7 +206,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), "id=1") + t.Assert().Contains(log.String(), "counterId=1") t.Assert().Empty(n.message) } @@ -224,7 +224,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), `msg="cannot send notification for counter" id=1 error="unknown error" message=""`) + t.Assert().Contains(log.String(), `msg="cannot send notification for counter" counterId=1 error="unknown error" failureMessage=""`) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message) } diff --git a/core/job_manager.go b/core/job_manager.go index ed1db6d..8676246 100644 --- a/core/job_manager.go +++ b/core/job_manager.go @@ -154,7 +154,7 @@ func NewJobManager() *JobManager { func DefaultJobErrorHandler() JobErrorHandler { return func(name string, err error, log logger.Logger) { if err != nil && name != "" { - log.Error("job failed with an error", slog.String("job", name), logger.ErrAttr(err)) + log.Error("job failed with an error", slog.String("job", name), logger.Err(err)) } } } diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 8674c7c..009f3d6 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -1,6 +1,7 @@ package logger import ( + "fmt" "log/slog" "net/http" ) @@ -18,7 +19,7 @@ const ( HTTPStatusNameAttr = "httpStatusName" ) -func ErrAttr(err any) slog.Attr { +func Err(err any) slog.Attr { if err == nil { return slog.String(ErrorAttr, "") } @@ -34,5 +35,12 @@ func HTTPStatusName(code int) slog.Attr { } func Body(val any) slog.Attr { - return slog.Any(BodyAttr, val) + switch item := val.(type) { + case string: + return slog.String(BodyAttr, item) + case []byte: + return slog.String(BodyAttr, string(item)) + default: + return slog.String(BodyAttr, fmt.Sprintf("%#v", val)) + } } diff --git a/core/logger/mg_transport_client_adapter.go b/core/logger/mg_transport_client_adapter.go new file mode 100644 index 0000000..0b1dbb7 --- /dev/null +++ b/core/logger/mg_transport_client_adapter.go @@ -0,0 +1,18 @@ +package logger + +import ( + "fmt" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +type mgTransportClientAdapter struct { + log Logger +} + +func MGTransportClientAdapter(log Logger) v1.DebugLogger { + return &mgTransportClientAdapter{log: log} +} + +func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) { + m.log.Debug(fmt.Sprintf(msg, args...)) +} diff --git a/core/module_features_uploader.go b/core/module_features_uploader.go index 33a9e44..306870b 100644 --- a/core/module_features_uploader.go +++ b/core/module_features_uploader.go @@ -65,13 +65,13 @@ func NewModuleFeaturesUploader( awsConfig.WithCredentialsProvider(customProvider), ) if err != nil { - log.Error("cannot load S3 configuration", logger.ErrAttr(err)) + log.Error("cannot load S3 configuration", logger.Err(err)) return nil } client := manager.NewUploader(s3.NewFromConfig(cfg)) if err != nil { - log.Error("cannot load S3 configuration", logger.ErrAttr(err)) + log.Error("cannot load S3 configuration", logger.Err(err)) return nil } @@ -91,14 +91,14 @@ func (s *ModuleFeaturesUploader) Upload() { content, err := os.ReadFile(s.featuresFilename) if err != nil { - s.log.Error("cannot read markdown file %s %s", slog.String("fileName", s.featuresFilename), logger.ErrAttr(err)) + s.log.Error("cannot read markdown file %s %s", slog.String("fileName", s.featuresFilename), logger.Err(err)) return } for _, lang := range languages { translated, err := s.translate(content, lang) if err != nil { - s.log.Error("cannot translate module features file", slog.String("lang", lang.String()), logger.ErrAttr(err)) + s.log.Error("cannot translate module features file", slog.String("lang", lang.String()), logger.Err(err)) continue } @@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() { resp, err := s.uploadFile(html, lang.String()) if err != nil { - s.log.Error("cannot upload file", slog.String("lang", lang.String()), logger.ErrAttr(err)) + s.log.Error("cannot upload file", slog.String("lang", lang.String()), logger.Err(err)) continue } diff --git a/core/sentry.go b/core/sentry.go index b7d2e0c..4b23565 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -250,7 +250,7 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint if l != nil { // TODO: Check if we can output stacktraces with prefix data like before if we really need it. stack := stacktrace.FormattedStack(3, "trace: ") - formattedErr := logger.ErrAttr(err) + formattedErr := logger.Err(err) httpRequest, _ := httputil.DumpRequest(c.Request, false) headers := strings.Split(string(httpRequest), "\r\n") for idx, header := range headers { diff --git a/go.mod b/go.mod index 9d9e50e..ef530ce 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pkg/errors v0.9.1 github.com/retailcrm/api-client-go/v2 v2.1.3 - github.com/retailcrm/mg-transport-api-client-go v1.1.32 + github.com/retailcrm/mg-transport-api-client-go v1.3.4 github.com/retailcrm/zabbix-metrics-collector v1.0.0 github.com/stretchr/testify v1.8.3 go.uber.org/atomic v1.10.0 diff --git a/go.sum b/go.sum index ae08450..eaf8e2f 100644 --- a/go.sum +++ b/go.sum @@ -379,6 +379,8 @@ github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKK github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs= github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= +github.com/retailcrm/mg-transport-api-client-go v1.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE= +github.com/retailcrm/mg-transport-api-client-go v1.3.4/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro= github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA= github.com/retailcrm/zabbix-metrics-collector v1.0.0/go.mod h1:3Orc+gfSg1tXj89QNvOn22t0cO1i2whR/4NJUGonWJA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= From 8fd4bc26bf3683d668dc5bba18b3ca923a08e0c3 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 17:56:21 +0300 Subject: [PATCH 10/26] partial GIN logging replacement --- core/engine.go | 16 ++++++++++++++++ core/job_manager.go | 1 + core/logger/writer_adapter.go | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 core/logger/writer_adapter.go diff --git a/core/engine.go b/core/engine.go index f997b47..de7948d 100644 --- a/core/engine.go +++ b/core/engine.go @@ -179,6 +179,22 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine { return e } +func (e *Engine) HijackGinLogs() *Engine { + if e.Logger() == nil { + return e + } + gin.DefaultWriter = logger.WriterAdapter(e.Logger(), slog.LevelDebug) + gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), slog.LevelError) + gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { + e.Logger().Debug("route", + slog.String(logger.HTTPMethodAttr, httpMethod), + slog.String("path", absolutePath), + slog.String(logger.HandlerAttr, handlerName), + slog.Int("handlerCount", nuHandlers)) + } + return e +} + // TemplateFuncMap combines func map for templates. func (e *Engine) TemplateFuncMap(functions template.FuncMap) template.FuncMap { funcMap := e.LocalizationFuncMap() diff --git a/core/job_manager.go b/core/job_manager.go index 8676246..da50bdd 100644 --- a/core/job_manager.go +++ b/core/job_manager.go @@ -74,6 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf } }() + log = log.With(logger.HandlerAttr, name) err := j.Command(log) if err != nil && j.ErrorHandler != nil { j.ErrorHandler(name, err, log) diff --git a/core/logger/writer_adapter.go b/core/logger/writer_adapter.go new file mode 100644 index 0000000..890fe80 --- /dev/null +++ b/core/logger/writer_adapter.go @@ -0,0 +1,21 @@ +package logger + +import ( + "context" + "io" + "log/slog" +) + +type writerAdapter struct { + log Logger + level slog.Level +} + +func WriterAdapter(log Logger, level slog.Level) io.Writer { + return &writerAdapter{log: log, level: level} +} + +func (w *writerAdapter) Write(p []byte) (n int, err error) { + w.log.Log(context.Background(), w.level, string(p)) + return len(p), nil +} From f5bda28e92d34fc8ec97029c263ffa4a6fef1c36 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 18 Oct 2023 18:16:59 +0300 Subject: [PATCH 11/26] gin middleware for logging --- core/engine.go | 7 ++----- core/logger/attrs.go | 6 +++--- core/logger/gin.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 core/logger/gin.go diff --git a/core/engine.go b/core/engine.go index de7948d..8d87d6b 100644 --- a/core/engine.go +++ b/core/engine.go @@ -111,11 +111,6 @@ func (e *Engine) initGin() { e.buildSentryConfig() e.InitSentrySDK() r.Use(e.SentryMiddlewares()...) - - if e.Config.IsDebug() { - r.Use(gin.Logger()) - } - r.Use(e.LocalizationMiddleware()) e.ginEngine = r } @@ -179,6 +174,8 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine { return e } +// HijackGinLogs will take control of GIN debug logs and will convert them into structured logs. +// It will also affect default logging middleware. Use logger.GinMiddleware to circumvent this. func (e *Engine) HijackGinLogs() *Engine { if e.Logger() == nil { return e diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 009f3d6..3f21387 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -14,9 +14,9 @@ const ( ErrorAttr = "error" FailureMessageAttr = "failureMessage" BodyAttr = "body" - HTTPMethodAttr = "httpMethod" - HTTPStatusAttr = "httpStatusCode" - HTTPStatusNameAttr = "httpStatusName" + HTTPMethodAttr = "method" + HTTPStatusAttr = "statusCode" + HTTPStatusNameAttr = "statusName" ) func Err(err any) slog.Attr { diff --git a/core/logger/gin.go b/core/logger/gin.go new file mode 100644 index 0000000..21eb8ec --- /dev/null +++ b/core/logger/gin.go @@ -0,0 +1,35 @@ +package logger + +import ( + "github.com/gin-gonic/gin" + "log/slog" + "time" +) + +// GinMiddleware will construct Gin middleware which will log requests. +func GinMiddleware(log Logger) gin.HandlerFunc { + return func(c *gin.Context) { + // Start timer + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + // Process request + c.Next() + + end := time.Now() + if raw != "" { + path = path + "?" + raw + } + + log.Info("[GIN] request", + slog.String("startTime", start.Format(time.RFC3339)), + slog.String("endTime", end.Format(time.RFC3339)), + slog.Any("latency", end.Sub(start)/time.Millisecond), + slog.String("remoteAddress", c.ClientIP()), + slog.String(HTTPMethodAttr, c.Request.Method), + slog.String("path", path), + slog.Int("bodySize", c.Writer.Size()), + ) + } +} From 7d1f722a9e05b87a84be2ea4a4877e25c18cf13e Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Tue, 24 Oct 2023 13:01:34 +0300 Subject: [PATCH 12/26] allow SetLogger call before Prepare --- core/engine.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/engine.go b/core/engine.go index 8d87d6b..655920e 100644 --- a/core/engine.go +++ b/core/engine.go @@ -260,6 +260,9 @@ func (e *Engine) SetLogger(l logger.Logger) *Engine { e.mutex.Lock() defer e.mutex.Unlock() + if !e.prepared && e.logger != nil { + return e + } e.logger = l return e } From 2455c1170458c8f49e20c407f4ee21e25bd8a7d2 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Tue, 24 Oct 2023 14:15:28 +0300 Subject: [PATCH 13/26] move gin to handler field --- core/logger/gin.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/logger/gin.go b/core/logger/gin.go index 21eb8ec..0443468 100644 --- a/core/logger/gin.go +++ b/core/logger/gin.go @@ -22,7 +22,8 @@ func GinMiddleware(log Logger) gin.HandlerFunc { path = path + "?" + raw } - log.Info("[GIN] request", + log.Info("request", + slog.String(HandlerAttr, "GIN"), slog.String("startTime", start.Format(time.RFC3339)), slog.String("endTime", end.Format(time.RFC3339)), slog.Any("latency", end.Sub(start)/time.Millisecond), From 96ac6a6940c530e223d9569661946c50c5a7f52f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 24 Nov 2023 15:23:07 +0300 Subject: [PATCH 14/26] integrate zap logger --- core/engine.go | 20 ++- core/engine_test.go | 2 +- core/healthcheck/processor.go | 17 ++- core/healthcheck/processor_test.go | 16 +-- core/job_manager.go | 10 +- core/job_manager_test.go | 120 +++++++++++------- core/logger/attrs.go | 29 +++-- core/logger/default.go | 112 ++++++---------- core/logger/gin.go | 21 +-- core/logger/handler.go | 8 -- core/logger/logger.go | 44 ------- core/logger/mg_transport_client_adapter.go | 1 + core/logger/nil.go | 52 ++++++++ core/logger/nil_handler.go | 26 ---- core/logger/writer_adapter.go | 10 +- core/logger/zap.go | 52 ++++++++ core/module_features_uploader.go | 8 +- core/sentry.go | 6 +- core/util/httputil/http_client_builder.go | 4 +- .../util/httputil/http_client_builder_test.go | 2 +- core/util/testutil/buffer_logger.go | 25 +++- core/util/testutil/buffer_logger_test.go | 6 +- core/util/testutil/lockable_buffer.go | 5 + core/util/utils.go | 5 +- core/util/utils_test.go | 2 +- go.mod | 2 + go.sum | 11 +- 27 files changed, 330 insertions(+), 286 deletions(-) delete mode 100644 core/logger/handler.go delete mode 100644 core/logger/logger.go create mode 100644 core/logger/nil.go delete mode 100644 core/logger/nil_handler.go create mode 100644 core/logger/zap.go diff --git a/core/engine.go b/core/engine.go index 655920e..8c318b0 100644 --- a/core/engine.go +++ b/core/engine.go @@ -5,7 +5,6 @@ import ( "fmt" "html/template" "io/fs" - "log/slog" "net/http" "sync" @@ -15,6 +14,7 @@ import ( "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "github.com/retailcrm/zabbix-metrics-collector" + "go.uber.org/zap" "golang.org/x/text/language" "github.com/retailcrm/mg-transport-core/v2/core/config" @@ -141,13 +141,9 @@ func (e *Engine) Prepare() *Engine { e.Localizer.Preload(e.PreloadLanguages) } - if !e.Config.IsDebug() { - logger.DefaultOpts.Level = slog.LevelInfo - } - e.CreateDB(e.Config.GetDBConfig()) e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0) - e.SetLogger(logger.NewDefaultText()) + e.SetLogger(logger.NewDefault(e.Config.IsDebug())) e.Sentry.Localizer = &e.Localizer e.Utils.Logger = e.Logger() e.Sentry.Logger = e.Logger() @@ -180,14 +176,14 @@ func (e *Engine) HijackGinLogs() *Engine { if e.Logger() == nil { return e } - gin.DefaultWriter = logger.WriterAdapter(e.Logger(), slog.LevelDebug) - gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), slog.LevelError) + gin.DefaultWriter = logger.WriterAdapter(e.Logger(), zap.DebugLevel) + gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), zap.ErrorLevel) gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { e.Logger().Debug("route", - slog.String(logger.HTTPMethodAttr, httpMethod), - slog.String("path", absolutePath), - slog.String(logger.HandlerAttr, handlerName), - slog.Int("handlerCount", nuHandlers)) + zap.String(logger.HTTPMethodAttr, httpMethod), + zap.String("path", absolutePath), + zap.String(logger.HandlerAttr, handlerName), + zap.Int("handlerCount", nuHandlers)) } return e } diff --git a/core/engine_test.go b/core/engine_test.go index b59e5d3..2d89d76 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -252,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() { defer func() { e.engine.logger = origLogger }() - e.engine.logger = logger.NewDefaultNil() + e.engine.logger = logger.NewNil() e.engine.SetLogger(nil) assert.NotNil(e.T(), e.engine.logger) } diff --git a/core/healthcheck/processor.go b/core/healthcheck/processor.go index e75ca93..f7f4216 100644 --- a/core/healthcheck/processor.go +++ b/core/healthcheck/processor.go @@ -1,9 +1,8 @@ package healthcheck import ( - "log/slog" - "github.com/retailcrm/mg-transport-core/v2/core/logger" + "go.uber.org/zap" ) const ( @@ -31,19 +30,19 @@ type CounterProcessor struct { func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen if counter.IsFailed() { if counter.IsFailureProcessed() { - c.debugLog("skipping counter because its failure is already processed", slog.Int(logger.CounterIDAttr, id)) + c.debugLog("skipping counter because its failure is already processed", zap.Int(logger.CounterIDAttr, id)) return true } apiURL, apiKey, _, exists := c.ConnectionDataProvider(id) if !exists { - c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id)) + c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id)) return true } err := c.Notifier(apiURL, apiKey, counter.Message()) if err != nil { c.debugLog("cannot send notification for counter", - slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message())) + zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message())) } counter.FailureProcessed() return true @@ -56,7 +55,7 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn // The results may not be representative. if (succeeded + failed) < c.MinRequests { c.debugLog("skipping counter because it has too few requests", - slog.Int(logger.CounterIDAttr, id), slog.Any("minRequests", c.MinRequests)) + zap.Int(logger.CounterIDAttr, id), zap.Any("minRequests", c.MinRequests)) return true } @@ -75,13 +74,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id) if !exists { - c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id)) + c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id)) return true } err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang)) if err != nil { c.debugLog("cannot send notification for counter", - slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message())) + zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message())) } counter.CountersProcessed() return true @@ -99,6 +98,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string { func (c CounterProcessor) debugLog(msg string, args ...interface{}) { if c.Debug { - c.Logger.Debug(msg, args...) + c.Logger.Debug(msg, logger.AnyZapFields(args)...) } } diff --git a/core/healthcheck/processor_test.go b/core/healthcheck/processor_test.go index 8ffa8d5..0492454 100644 --- a/core/healthcheck/processor_test.go +++ b/core/healthcheck/processor_test.go @@ -96,7 +96,7 @@ func (t *CounterProcessorTest) Test_FailureProcessed() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "skipping counter because its failure is already processed") - t.Assert().Contains(log.String(), "counterId=1") + t.Assert().Contains(log.String(), `"counterId": 1`) } func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { @@ -109,7 +109,7 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), "counterId=1") + t.Assert().Contains(log.String(), `"counterId": 1`) } func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { @@ -124,9 +124,9 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot send notification for counter") - t.Assert().Contains(log.String(), "counterId=1") - t.Assert().Contains(log.String(), `error="http status code: 500"`) - t.Assert().Contains(log.String(), `failureMessage="error message"`) + t.Assert().Contains(log.String(), `"counterId": 1`) + t.Assert().Contains(log.String(), `"error": "http status code: 500"`) + t.Assert().Contains(log.String(), `"failureMessage": "error message"`) t.Assert().Equal(t.apiURL, n.apiURL) t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal("error message", n.message) @@ -160,7 +160,7 @@ func (t *CounterProcessorTest) Test_TooFewRequests() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), - fmt.Sprintf(`msg="skipping counter because it has too few requests" counterId=%d minRequests=%d`, 1, DefaultMinRequests)) + fmt.Sprintf(`skipping counter because it has too few requests {"counterId": %d, "minRequests": %d}`, 1, DefaultMinRequests)) } func (t *CounterProcessorTest) Test_ThresholdNotPassed() { @@ -206,7 +206,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() { p.Process(1, c) c.AssertExpectations(t.T()) t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), "counterId=1") + t.Assert().Contains(log.String(), `"counterId": 1`) t.Assert().Empty(n.message) } @@ -224,7 +224,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), `msg="cannot send notification for counter" counterId=1 error="unknown error" failureMessage=""`) + t.Assert().Contains(log.String(), `cannot send notification for counter {"counterId": 1, "error": "unknown error", "failureMessage": ""}`) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message) } diff --git a/core/job_manager.go b/core/job_manager.go index da50bdd..4a734cc 100644 --- a/core/job_manager.go +++ b/core/job_manager.go @@ -3,11 +3,11 @@ package core import ( "errors" "fmt" - "log/slog" "sync" "time" "github.com/retailcrm/mg-transport-core/v2/core/logger" + "go.uber.org/zap" ) // JobFunc is empty func which should be executed in a parallel goroutine. @@ -74,7 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf } }() - log = log.With(logger.HandlerAttr, name) + log = log.With(logger.Handler(name)) err := j.Command(log) if err != nil && j.ErrorHandler != nil { j.ErrorHandler(name, err, log) @@ -148,14 +148,14 @@ func (j *Job) runOnceSync(name string, log logger.Logger) { // NewJobManager is a JobManager constructor. func NewJobManager() *JobManager { - return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewDefaultNil()} + return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewNil()} } // DefaultJobErrorHandler returns default error handler for a job. func DefaultJobErrorHandler() JobErrorHandler { return func(name string, err error, log logger.Logger) { if err != nil && name != "" { - log.Error("job failed with an error", slog.String("job", name), logger.Err(err)) + log.Error("job failed with an error", zap.String("job", name), logger.Err(err)) } } } @@ -164,7 +164,7 @@ func DefaultJobErrorHandler() JobErrorHandler { func DefaultJobPanicHandler() JobPanicHandler { return func(name string, recoverValue interface{}, log logger.Logger) { if recoverValue != nil && name != "" { - log.Error("job panicked with the value", slog.String("job", name), slog.Any("value", recoverValue)) + log.Error("job panicked with the value", zap.String("job", name), zap.Any("value", recoverValue)) } } } diff --git a/core/job_manager_test.go b/core/job_manager_test.go index dbfb09c..6de4fc5 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -1,10 +1,8 @@ package core import ( - "context" "errors" "fmt" - "log/slog" "math/rand" "strings" "sync" @@ -14,6 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/retailcrm/mg-transport-core/v2/core/logger" ) @@ -26,7 +26,7 @@ type JobTest struct { executeErr chan error panicValue chan interface{} lastLog string - lastMsgLevel slog.Level + lastMsgLevel zapcore.Level syncBool bool } @@ -37,18 +37,58 @@ type JobManagerTest struct { syncRunnerFlag bool } -type callbackLoggerFunc func(ctx context.Context, level slog.Level, msg string, args ...any) +type callbackLoggerFunc func(level zapcore.Level, msg string, args ...zap.Field) type callbackLogger struct { - fn callbackLoggerFunc + fields []zap.Field + fn callbackLoggerFunc } -func (n *callbackLogger) Handler() slog.Handler { - return logger.NilHandler +func (n *callbackLogger) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { + return &zapcore.CheckedEntry{} } -func (n *callbackLogger) With(args ...any) logger.Logger { - return n +func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) { + n.fn(zap.PanicLevel, msg, fields...) +} + +func (n *callbackLogger) Panic(msg string, fields ...zap.Field) { + n.fn(zap.PanicLevel, msg, fields...) +} + +func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) { + n.fn(zap.FatalLevel, msg, fields...) +} + +func (n *callbackLogger) Sync() error { + return nil +} + +func (n *callbackLogger) clone() *callbackLogger { + return &callbackLogger{fn: n.fn, fields: n.fields} +} + +func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger { + cl := &callbackLogger{fn: n.fn, fields: n.fields} + existing := cl.fields + if len(existing) == 0 { + cl.fields = fields + return cl + } + cl.fields = append(existing, fields...) + return cl +} + +func (n *callbackLogger) Level() zapcore.Level { + return zapcore.DebugLevel +} + +func (n *callbackLogger) With(args ...zap.Field) logger.Logger { + return n.cloneWithFields(args) +} + +func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger { + return n.cloneWithFields(args) } func (n *callbackLogger) WithGroup(name string) logger.Logger { @@ -59,47 +99,24 @@ func (n *callbackLogger) ForAccount(handler, conn, acc any) logger.Logger { return n } -func (n *callbackLogger) Enabled(ctx context.Context, level slog.Level) bool { - return true +func (n *callbackLogger) Log(level zapcore.Level, msg string, args ...zap.Field) { + n.fn(level, msg, args...) } -func (n *callbackLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { - n.fn(ctx, level, msg, args...) +func (n *callbackLogger) Debug(msg string, args ...zap.Field) { + n.Log(zap.DebugLevel, msg, args...) } -func (n *callbackLogger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { +func (n *callbackLogger) Info(msg string, args ...zap.Field) { + n.Log(zap.InfoLevel, msg, args...) } -func (n *callbackLogger) Debug(msg string, args ...any) { - n.Log(nil, slog.LevelDebug, msg, args...) +func (n *callbackLogger) Warn(msg string, args ...zap.Field) { + n.Log(zap.WarnLevel, msg, args...) } -func (n *callbackLogger) DebugContext(ctx context.Context, msg string, args ...any) { - n.Log(ctx, slog.LevelDebug, msg, args...) -} - -func (n *callbackLogger) Info(msg string, args ...any) { - n.Log(nil, slog.LevelInfo, msg, args...) -} - -func (n *callbackLogger) InfoContext(ctx context.Context, msg string, args ...any) { - n.Log(ctx, slog.LevelInfo, msg, args...) -} - -func (n *callbackLogger) Warn(msg string, args ...any) { - n.Log(nil, slog.LevelWarn, msg, args...) -} - -func (n *callbackLogger) WarnContext(ctx context.Context, msg string, args ...any) { - n.Log(ctx, slog.LevelWarn, msg, args...) -} - -func (n *callbackLogger) Error(msg string, args ...any) { - n.Log(nil, slog.LevelError, msg, args...) -} - -func (n *callbackLogger) ErrorContext(ctx context.Context, msg string, args ...any) { - n.Log(ctx, slog.LevelError, msg, args...) +func (n *callbackLogger) Error(msg string, args ...zap.Field) { + n.Log(zap.ErrorLevel, msg, args...) } func TestJob(t *testing.T) { @@ -117,9 +134,9 @@ func TestDefaultJobErrorHandler(t *testing.T) { fn := DefaultJobErrorHandler() require.NotNil(t, fn) - fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) { + fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) { require.Len(t, i, 2) - assert.Equal(t, "error=test", fmt.Sprintf("%s", i[1])) + assert.Equal(t, "error=test", fmt.Sprintf("%s=%v", i[1].Key, i[1].Interface)) }}) } @@ -130,9 +147,9 @@ func TestDefaultJobPanicHandler(t *testing.T) { fn := DefaultJobPanicHandler() require.NotNil(t, fn) - fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) { + fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) { require.Len(t, i, 2) - assert.Equal(t, "value=test", fmt.Sprintf("%s", i[1])) + assert.Equal(t, "value=test", fmt.Sprintf("%s=%s", i[1].Key, i[1].Interface)) }}) } @@ -149,7 +166,7 @@ func (t *JobTest) testPanicHandler() JobPanicHandler { } func (t *JobTest) testLogger() logger.Logger { - return &callbackLogger{fn: func(_ context.Context, level slog.Level, format string, args ...interface{}) { + return &callbackLogger{fn: func(level zapcore.Level, format string, args ...zap.Field) { if format == "" { var sb strings.Builder sb.Grow(3 * len(args)) // nolint:gomnd @@ -161,7 +178,12 @@ func (t *JobTest) testLogger() logger.Logger { format = strings.TrimRight(sb.String(), " ") } - t.lastLog = fmt.Sprintf(format, args...) + anyFields := []any{} + for _, item := range args { + anyFields = append(anyFields, item.Key+"="+fmt.Sprint(item.Interface)) + } + + t.lastLog = fmt.Sprintf(format, anyFields...) t.lastMsgLevel = level }} } @@ -406,7 +428,7 @@ func (t *JobManagerTest) WaitForJob() bool { func (t *JobManagerTest) Test_SetLogger() { t.manager.logger = nil - t.manager.SetLogger(logger.NewDefaultText()) + t.manager.SetLogger(logger.NewDefault(true)) assert.IsType(t.T(), &logger.Default{}, t.manager.logger) t.manager.SetLogger(nil) diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 3f21387..17bd746 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -2,8 +2,9 @@ package logger import ( "fmt" - "log/slog" "net/http" + + "go.uber.org/zap" ) const ( @@ -19,28 +20,32 @@ const ( HTTPStatusNameAttr = "statusName" ) -func Err(err any) slog.Attr { +func Err(err any) zap.Field { if err == nil { - return slog.String(ErrorAttr, "") + return zap.String(ErrorAttr, "") } - return slog.Any(ErrorAttr, err) + return zap.Any(ErrorAttr, err) } -func HTTPStatusCode(code int) slog.Attr { - return slog.Int(HTTPStatusAttr, code) +func Handler(name string) zap.Field { + return zap.String(HandlerAttr, name) } -func HTTPStatusName(code int) slog.Attr { - return slog.String(HTTPStatusNameAttr, http.StatusText(code)) +func HTTPStatusCode(code int) zap.Field { + return zap.Int(HTTPStatusAttr, code) } -func Body(val any) slog.Attr { +func HTTPStatusName(code int) zap.Field { + return zap.String(HTTPStatusNameAttr, http.StatusText(code)) +} + +func Body(val any) zap.Field { switch item := val.(type) { case string: - return slog.String(BodyAttr, item) + return zap.String(BodyAttr, item) case []byte: - return slog.String(BodyAttr, string(item)) + return zap.String(BodyAttr, string(item)) default: - return slog.String(BodyAttr, fmt.Sprintf("%#v", val)) + return zap.String(BodyAttr, fmt.Sprintf("%#v", val)) } } diff --git a/core/logger/default.go b/core/logger/default.go index 78240a3..dc92980 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -1,87 +1,59 @@ package logger import ( - "context" - "log/slog" - "os" + "strconv" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) +type Logger interface { + With(fields ...zap.Field) Logger + WithLazy(fields ...zap.Field) Logger + Level() zapcore.Level + Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry + Log(lvl zapcore.Level, msg string, fields ...zap.Field) + Debug(msg string, fields ...zap.Field) + Info(msg string, fields ...zap.Field) + Warn(msg string, fields ...zap.Field) + Error(msg string, fields ...zap.Field) + DPanic(msg string, fields ...zap.Field) + Panic(msg string, fields ...zap.Field) + Fatal(msg string, fields ...zap.Field) + ForAccount(handler, conn, acc any) Logger + Sync() error +} + type Default struct { - Logger *slog.Logger + *zap.Logger } -func NewDefault(log *slog.Logger) Logger { - return &Default{Logger: log} +func NewDefault(debug bool) Logger { + return &Default{ + Logger: NewZap(debug), + } } -func NewDefaultText() Logger { - return NewDefault(slog.New(slog.NewTextHandler(os.Stdout, DefaultOpts))) +func (l *Default) With(fields ...zap.Field) Logger { + return l.With(fields...).(Logger) } -func NewDefaultJSON() Logger { - return NewDefault(slog.New(slog.NewJSONHandler(os.Stdout, DefaultOpts))) +func (l *Default) WithLazy(fields ...zap.Field) Logger { + return l.WithLazy(fields...).(Logger) } -func NewDefaultNil() Logger { - return NewDefault(slog.New(NilHandler)) +func (l *Default) ForAccount(handler, conn, acc any) Logger { + return l.WithLazy(zap.Any(HandlerAttr, handler), zap.Any(ConnectionAttr, conn), zap.Any(AccountAttr, acc)) } -func (d *Default) Handler() slog.Handler { - return d.Logger.Handler() -} - -func (d *Default) ForAccount(handler, conn, acc any) Logger { - return d.With(slog.Any(HandlerAttr, handler), slog.Any(ConnectionAttr, conn), slog.Any(AccountAttr, acc)) -} - -func (d *Default) With(args ...any) Logger { - return &Default{Logger: d.Logger.With(args...)} -} - -func (d *Default) WithGroup(name string) Logger { - return &Default{Logger: d.Logger.WithGroup(name)} -} - -func (d *Default) Enabled(ctx context.Context, level slog.Level) bool { - return d.Logger.Enabled(ctx, level) -} - -func (d *Default) Log(ctx context.Context, level slog.Level, msg string, args ...any) { - d.Logger.Log(ctx, level, msg, args...) -} - -func (d *Default) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { - d.Logger.LogAttrs(ctx, level, msg, attrs...) -} - -func (d *Default) Debug(msg string, args ...any) { - d.Logger.Debug(msg, args...) -} - -func (d *Default) DebugContext(ctx context.Context, msg string, args ...any) { - d.Logger.DebugContext(ctx, msg, args...) -} - -func (d *Default) Info(msg string, args ...any) { - d.Logger.Info(msg, args...) -} - -func (d *Default) InfoContext(ctx context.Context, msg string, args ...any) { - d.Logger.InfoContext(ctx, msg, args...) -} - -func (d *Default) Warn(msg string, args ...any) { - d.Logger.Warn(msg, args...) -} - -func (d *Default) WarnContext(ctx context.Context, msg string, args ...any) { - d.Logger.WarnContext(ctx, msg, args...) -} - -func (d *Default) Error(msg string, args ...any) { - d.Logger.Error(msg, args...) -} - -func (d *Default) ErrorContext(ctx context.Context, msg string, args ...any) { - d.Logger.ErrorContext(ctx, msg, args...) +func AnyZapFields(args []interface{}) []zap.Field { + fields := make([]zap.Field, len(args)) + for i := 0; i < len(fields); i++ { + if val, ok := args[i].(zap.Field); ok { + fields[i] = val + continue + } + fields[i] = zap.Any("arg"+strconv.Itoa(i), args[i]) + } + return fields } diff --git a/core/logger/gin.go b/core/logger/gin.go index 0443468..5ecf3b2 100644 --- a/core/logger/gin.go +++ b/core/logger/gin.go @@ -1,9 +1,10 @@ package logger import ( - "github.com/gin-gonic/gin" - "log/slog" "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" ) // GinMiddleware will construct Gin middleware which will log requests. @@ -23,14 +24,14 @@ func GinMiddleware(log Logger) gin.HandlerFunc { } log.Info("request", - slog.String(HandlerAttr, "GIN"), - slog.String("startTime", start.Format(time.RFC3339)), - slog.String("endTime", end.Format(time.RFC3339)), - slog.Any("latency", end.Sub(start)/time.Millisecond), - slog.String("remoteAddress", c.ClientIP()), - slog.String(HTTPMethodAttr, c.Request.Method), - slog.String("path", path), - slog.Int("bodySize", c.Writer.Size()), + zap.String(HandlerAttr, "GIN"), + zap.String("startTime", start.Format(time.RFC3339)), + zap.String("endTime", end.Format(time.RFC3339)), + zap.Any("latency", end.Sub(start)/time.Millisecond), + zap.String("remoteAddress", c.ClientIP()), + zap.String(HTTPMethodAttr, c.Request.Method), + zap.String("path", path), + zap.Int("bodySize", c.Writer.Size()), ) } } diff --git a/core/logger/handler.go b/core/logger/handler.go deleted file mode 100644 index 1c5ffe9..0000000 --- a/core/logger/handler.go +++ /dev/null @@ -1,8 +0,0 @@ -package logger - -import "log/slog" - -var DefaultOpts = &slog.HandlerOptions{ - AddSource: false, - Level: slog.LevelDebug, -} diff --git a/core/logger/logger.go b/core/logger/logger.go deleted file mode 100644 index 4fb9f16..0000000 --- a/core/logger/logger.go +++ /dev/null @@ -1,44 +0,0 @@ -package logger - -import ( - "context" - "log/slog" -) - -// LoggerOld contains methods which should be present in logger implementation. -type LoggerOld interface { - Fatal(args ...any) - Fatalf(format string, args ...any) - Panic(args ...any) - Panicf(format string, args ...any) - Critical(args ...any) - Criticalf(format string, args ...any) - Error(args ...any) - Errorf(format string, args ...any) - Warning(args ...any) - Warningf(format string, args ...any) - Notice(args ...any) - Noticef(format string, args ...any) - Info(args ...any) - Infof(format string, args ...any) - Debug(args ...any) - Debugf(format string, args ...any) -} - -type Logger interface { - Handler() slog.Handler - With(args ...any) Logger - WithGroup(name string) Logger - ForAccount(handler, conn, acc any) Logger - Enabled(ctx context.Context, level slog.Level) bool - Log(ctx context.Context, level slog.Level, msg string, args ...any) - LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) - Debug(msg string, args ...any) - DebugContext(ctx context.Context, msg string, args ...any) - Info(msg string, args ...any) - InfoContext(ctx context.Context, msg string, args ...any) - Warn(msg string, args ...any) - WarnContext(ctx context.Context, msg string, args ...any) - Error(msg string, args ...any) - ErrorContext(ctx context.Context, msg string, args ...any) -} diff --git a/core/logger/mg_transport_client_adapter.go b/core/logger/mg_transport_client_adapter.go index 0b1dbb7..917056f 100644 --- a/core/logger/mg_transport_client_adapter.go +++ b/core/logger/mg_transport_client_adapter.go @@ -2,6 +2,7 @@ package logger import ( "fmt" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" ) diff --git a/core/logger/nil.go b/core/logger/nil.go new file mode 100644 index 0000000..74d0257 --- /dev/null +++ b/core/logger/nil.go @@ -0,0 +1,52 @@ +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Nil struct{} + +func NewNil() Logger { + return &Nil{} +} + +func (l *Nil) With(fields ...zap.Field) Logger { + return l +} + +func (l *Nil) WithLazy(fields ...zap.Field) Logger { + return l +} + +func (l *Nil) Level() zapcore.Level { + return zapcore.DebugLevel +} + +func (l *Nil) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { + return &zapcore.CheckedEntry{} +} + +func (l *Nil) Log(lvl zapcore.Level, msg string, fields ...zap.Field) {} + +func (l *Nil) Debug(msg string, fields ...zap.Field) {} + +func (l *Nil) Info(msg string, fields ...zap.Field) {} + +func (l *Nil) Warn(msg string, fields ...zap.Field) {} + +func (l *Nil) Error(msg string, fields ...zap.Field) {} + +func (l *Nil) DPanic(msg string, fields ...zap.Field) {} + +func (l *Nil) Panic(msg string, fields ...zap.Field) {} + +func (l *Nil) Fatal(msg string, fields ...zap.Field) {} + +func (l *Nil) ForAccount(handler, conn, acc any) Logger { + return l +} + +func (l *Nil) Sync() error { + return nil +} diff --git a/core/logger/nil_handler.go b/core/logger/nil_handler.go deleted file mode 100644 index 6b167b3..0000000 --- a/core/logger/nil_handler.go +++ /dev/null @@ -1,26 +0,0 @@ -package logger - -import ( - "context" - "log/slog" -) - -var NilHandler slog.Handler = &nilHandler{} - -type nilHandler struct{} - -func (n *nilHandler) Enabled(ctx context.Context, level slog.Level) bool { - return false -} - -func (n *nilHandler) Handle(ctx context.Context, record slog.Record) error { - return nil -} - -func (n *nilHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - return n -} - -func (n *nilHandler) WithGroup(name string) slog.Handler { - return n -} diff --git a/core/logger/writer_adapter.go b/core/logger/writer_adapter.go index 890fe80..e80459e 100644 --- a/core/logger/writer_adapter.go +++ b/core/logger/writer_adapter.go @@ -1,21 +1,21 @@ package logger import ( - "context" "io" - "log/slog" + + "go.uber.org/zap/zapcore" ) type writerAdapter struct { log Logger - level slog.Level + level zapcore.Level } -func WriterAdapter(log Logger, level slog.Level) io.Writer { +func WriterAdapter(log Logger, level zapcore.Level) io.Writer { return &writerAdapter{log: log, level: level} } func (w *writerAdapter) Write(p []byte) (n int, err error) { - w.log.Log(context.Background(), w.level, string(p)) + w.log.Log(w.level, string(p)) return len(p), nil } diff --git a/core/logger/zap.go b/core/logger/zap.go new file mode 100644 index 0000000..69db51e --- /dev/null +++ b/core/logger/zap.go @@ -0,0 +1,52 @@ +package logger + +import ( + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func NewZap(debug bool) *zap.Logger { + level := zapcore.InfoLevel + if debug { + level = zapcore.DebugLevel + } + log, err := zap.Config{ + Level: zap.NewAtomicLevelAt(level), + Development: debug, + Encoding: "console", + EncoderConfig: EncoderConfig(), + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + }.Build() + if err != nil { + panic(err) + } + return log +} + +func EncoderConfig() zapcore.EncoderConfig { + return zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "timestamp", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + StacktraceKey: "", + LineEnding: "\n", + EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString("level=" + level.String()) + }, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString("time=" + t.Format(time.RFC3339)) + }, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString("caller=" + caller.TrimmedPath()) + }, + EncodeName: zapcore.FullNameEncoder, + ConsoleSeparator: " ", + } +} diff --git a/core/module_features_uploader.go b/core/module_features_uploader.go index 306870b..d014938 100644 --- a/core/module_features_uploader.go +++ b/core/module_features_uploader.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "html/template" - "log/slog" "os" "github.com/aws/aws-sdk-go-v2/aws" @@ -13,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/gomarkdown/markdown" + "go.uber.org/zap" "golang.org/x/text/language" "github.com/retailcrm/mg-transport-core/v2/core/config" @@ -91,14 +91,14 @@ func (s *ModuleFeaturesUploader) Upload() { content, err := os.ReadFile(s.featuresFilename) if err != nil { - s.log.Error("cannot read markdown file %s %s", slog.String("fileName", s.featuresFilename), logger.Err(err)) + s.log.Error("cannot read markdown file %s %s", zap.String("fileName", s.featuresFilename), logger.Err(err)) return } for _, lang := range languages { translated, err := s.translate(content, lang) if err != nil { - s.log.Error("cannot translate module features file", slog.String("lang", lang.String()), logger.Err(err)) + s.log.Error("cannot translate module features file", zap.String("lang", lang.String()), logger.Err(err)) continue } @@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() { resp, err := s.uploadFile(html, lang.String()) if err != nil { - s.log.Error("cannot upload file", slog.String("lang", lang.String()), logger.Err(err)) + s.log.Error("cannot upload file", zap.String("lang", lang.String()), logger.Err(err)) continue } diff --git a/core/sentry.go b/core/sentry.go index 4b23565..27b2057 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "log/slog" "net" "net/http" "net/http/httputil" @@ -16,6 +15,7 @@ import ( "github.com/getsentry/sentry-go" sentrygin "github.com/getsentry/sentry-go/gin" "github.com/pkg/errors" + "go.uber.org/zap" "github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/stacktrace" @@ -260,8 +260,8 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint } headers[idx] = "header: " + headers[idx] } - headersToStr := slog.String("headers", strings.Join(headers, "\r\n")) - formattedStack := slog.String("stacktrace", string(stack)) + headersToStr := zap.String("headers", strings.Join(headers, "\r\n")) + formattedStack := zap.String("stacktrace", string(stack)) switch { case brokenPipe: l.Error("error", formattedErr, headersToStr) diff --git a/core/util/httputil/http_client_builder.go b/core/util/httputil/http_client_builder.go index 0948fcb..3d8bf3a 100644 --- a/core/util/httputil/http_client_builder.go +++ b/core/util/httputil/http_client_builder.go @@ -12,9 +12,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/retailcrm/mg-transport-core/v2/core/config" - "github.com/retailcrm/mg-transport-core/v2/core/logger" ) @@ -275,7 +273,7 @@ func (b *HTTPClientBuilder) buildMocks() error { func (b *HTTPClientBuilder) log(msg string, args ...interface{}) { if b.logging { if b.logger != nil { - b.logger.Info(msg, args...) + b.logger.Info(msg, logger.AnyZapFields(args)...) } else { fmt.Println(append([]any{msg}, args...)) } diff --git a/core/util/httputil/http_client_builder_test.go b/core/util/httputil/http_client_builder_test.go index dae78c4..f2e9356 100644 --- a/core/util/httputil/http_client_builder_test.go +++ b/core/util/httputil/http_client_builder_test.go @@ -142,7 +142,7 @@ func (t *HTTPClientBuilderTest) Test_WithLogger() { builder.WithLogger(nil) assert.Nil(t.T(), builder.logger) - log := logger.NewDefaultText() + log := logger.NewDefault(true) builder.WithLogger(log) assert.NotNil(t.T(), builder.logger) } diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index f28713d..a7d0723 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -3,9 +3,11 @@ package testutil import ( "fmt" "io" - "log/slog" + "os" "github.com/retailcrm/mg-transport-core/v2/core/logger" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) // ReadBuffer is implemented by the BufferLogger. @@ -32,21 +34,32 @@ type BufferLogger struct { // NewBufferedLogger returns new BufferedLogger instance. func NewBufferedLogger() BufferedLogger { bl := &BufferLogger{} - bl.Logger = slog.New(slog.NewTextHandler(&bl.buf, logger.DefaultOpts)) + bl.Logger = zap.New( + zapcore.NewCore( + zapcore.NewConsoleEncoder( + logger.EncoderConfig()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) return bl } -// With doesn't do anything here and only added for backwards compatibility with the interface. -func (l *BufferLogger) With(args ...any) logger.Logger { +func (l *BufferLogger) With(fields ...zapcore.Field) logger.Logger { return &BufferLogger{ Default: logger.Default{ - Logger: l.Logger.With(args...), + Logger: l.Logger.With(fields...), + }, + } +} + +func (l *BufferLogger) WithLazy(fields ...zapcore.Field) logger.Logger { + return &BufferLogger{ + Default: logger.Default{ + Logger: l.Logger.WithLazy(fields...), }, } } func (l *BufferLogger) ForAccount(handler, conn, acc any) logger.Logger { - return l.With(slog.Any(logger.HandlerAttr, handler), slog.Any(logger.ConnectionAttr, conn), slog.Any(logger.AccountAttr, acc)) + return l.WithLazy( + zap.Any(logger.HandlerAttr, handler), zap.Any(logger.ConnectionAttr, conn), zap.Any(logger.AccountAttr, acc)) } // Read bytes from the logger buffer. io.Reader implementation. diff --git a/core/util/testutil/buffer_logger_test.go b/core/util/testutil/buffer_logger_test.go index 0bfa23e..dd4865c 100644 --- a/core/util/testutil/buffer_logger_test.go +++ b/core/util/testutil/buffer_logger_test.go @@ -29,17 +29,17 @@ func (t *BufferLoggerTest) Test_Read() { data, err := io.ReadAll(t.logger) t.Require().NoError(err) - t.Assert().Contains(string(data), "level=DEBUG msg=test") + t.Assert().Contains(string(data), "level=debug test") } func (t *BufferLoggerTest) Test_Bytes() { t.logger.Debug("test") - t.Assert().Contains(string(t.logger.Bytes()), "level=DEBUG msg=test") + t.Assert().Contains(string(t.logger.Bytes()), "level=debug test") } func (t *BufferLoggerTest) Test_String() { t.logger.Debug("test") - t.Assert().Contains(t.logger.String(), "level=DEBUG msg=test") + t.Assert().Contains(t.logger.String(), "level=debug test") } func (t *BufferLoggerTest) TestRace() { diff --git a/core/util/testutil/lockable_buffer.go b/core/util/testutil/lockable_buffer.go index ae71356..8636a9a 100644 --- a/core/util/testutil/lockable_buffer.go +++ b/core/util/testutil/lockable_buffer.go @@ -148,3 +148,8 @@ func (b *LockableBuffer) ReadString(delim byte) (line string, err error) { b.rw.Lock() return b.buf.ReadString(delim) } + +// Sync is a no-op. +func (b *LockableBuffer) Sync() error { + return nil +} diff --git a/core/util/utils.go b/core/util/utils.go index 6e098cc..27e6e71 100644 --- a/core/util/utils.go +++ b/core/util/utils.go @@ -21,7 +21,6 @@ import ( "github.com/gin-gonic/gin" retailcrm "github.com/retailcrm/api-client-go/v2" v1 "github.com/retailcrm/mg-transport-api-client-go/v1" - "github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/logger" @@ -145,12 +144,12 @@ func (u *Utils) GetAPIClient( if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 { if len(credentials) == 0 || len(cr.Scopes) > 0 { - u.Logger.Error(url, status, res) + u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res)) return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res) } if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 { - u.Logger.Error(url, status, res) + u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res)) return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res) } } diff --git a/core/util/utils_test.go b/core/util/utils_test.go index 2890c54..ffa2374 100644 --- a/core/util/utils_test.go +++ b/core/util/utils_test.go @@ -38,7 +38,7 @@ func mgClient() *v1.MgClient { } func (u *UtilsTest) SetupSuite() { - logger := logger.NewDefaultText() + logger := logger.NewDefault(true) awsConfig := config.AWS{ AccessKeyID: "access key id (will be removed)", SecretAccessKey: "secret access key", diff --git a/go.mod b/go.mod index ef530ce..b458d9d 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/retailcrm/zabbix-metrics-collector v1.0.0 github.com/stretchr/testify v1.8.3 go.uber.org/atomic v1.10.0 + go.uber.org/zap v1.26.0 golang.org/x/text v0.14.0 gopkg.in/gormigrate.v1 v1.6.0 gopkg.in/yaml.v2 v2.4.0 @@ -77,6 +78,7 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect diff --git a/go.sum b/go.sum index eaf8e2f..3c610f9 100644 --- a/go.sum +++ b/go.sum @@ -297,8 +297,9 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= @@ -377,8 +378,6 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKKB//MUAe0Zb0= github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= -github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs= -github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= github.com/retailcrm/mg-transport-api-client-go v1.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE= github.com/retailcrm/mg-transport-api-client-go v1.3.4/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro= github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA= @@ -444,6 +443,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= From 194e9c40e94c267077ec30fa26cd8af2478143c7 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 19 Jan 2024 13:17:04 +0300 Subject: [PATCH 15/26] zap logger updates --- core/job_manager_test.go | 10 +++++++++- core/logger/default.go | 16 +++++++++++++--- core/logger/nil.go | 10 +++++++++- core/sentry.go | 4 ++-- core/sentry_test.go | 2 +- core/util/testutil/buffer_logger.go | 13 ++++++++++--- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/core/job_manager_test.go b/core/job_manager_test.go index 6de4fc5..ae3fd82 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -95,7 +95,15 @@ func (n *callbackLogger) WithGroup(name string) logger.Logger { return n } -func (n *callbackLogger) ForAccount(handler, conn, acc any) logger.Logger { +func (n *callbackLogger) ForHandler(handler any) logger.Logger { + return n +} + +func (n *callbackLogger) ForConnection(conn any) logger.Logger { + return n +} + +func (n *callbackLogger) ForAccount(acc any) logger.Logger { return n } diff --git a/core/logger/default.go b/core/logger/default.go index dc92980..62f866d 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -20,7 +20,9 @@ type Logger interface { DPanic(msg string, fields ...zap.Field) Panic(msg string, fields ...zap.Field) Fatal(msg string, fields ...zap.Field) - ForAccount(handler, conn, acc any) Logger + ForHandler(handler any) Logger + ForConnection(conn any) Logger + ForAccount(acc any) Logger Sync() error } @@ -42,8 +44,16 @@ func (l *Default) WithLazy(fields ...zap.Field) Logger { return l.WithLazy(fields...).(Logger) } -func (l *Default) ForAccount(handler, conn, acc any) Logger { - return l.WithLazy(zap.Any(HandlerAttr, handler), zap.Any(ConnectionAttr, conn), zap.Any(AccountAttr, acc)) +func (l *Default) ForHandler(handler any) Logger { + return l.WithLazy(zap.Any(HandlerAttr, handler)) +} + +func (l *Default) ForConnection(conn any) Logger { + return l.WithLazy(zap.Any(ConnectionAttr, conn)) +} + +func (l *Default) ForAccount(acc any) Logger { + return l.WithLazy(zap.Any(AccountAttr, acc)) } func AnyZapFields(args []interface{}) []zap.Field { diff --git a/core/logger/nil.go b/core/logger/nil.go index 74d0257..282a13d 100644 --- a/core/logger/nil.go +++ b/core/logger/nil.go @@ -43,7 +43,15 @@ func (l *Nil) Panic(msg string, fields ...zap.Field) {} func (l *Nil) Fatal(msg string, fields ...zap.Field) {} -func (l *Nil) ForAccount(handler, conn, acc any) Logger { +func (l *Nil) ForHandler(handler any) Logger { + return l +} + +func (l *Nil) ForConnection(conn any) Logger { + return l +} + +func (l *Nil) ForAccount(acc any) Logger { return l } diff --git a/core/sentry.go b/core/sentry.go index 27b2057..2e3330c 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -150,7 +150,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.Logger { connectionID := "{no connection ID}" accountID := "{no account ID}" if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" { - return s.Logger.ForAccount("Sentry", connectionID, accountID) + return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID) } for tag := range s.tagsFromContext(c) { @@ -162,7 +162,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.Logger { } } - return s.Logger.ForAccount("Sentry", connectionID, accountID) + return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID) } // tagsSetterMiddleware sets event tags into Sentry events. diff --git a/core/sentry_test.go b/core/sentry_test.go index fe574f2..b919d86 100644 --- a/core/sentry_test.go +++ b/core/sentry_test.go @@ -277,7 +277,7 @@ func (s *SentryTest) TestSentry_CaptureException() { func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() { ctx, _ := s.ginCtxMock() - log := testutil.NewBufferedLogger().ForAccount("component", "conn", "acc") + log := testutil.NewBufferedLogger().ForHandler("component").ForConnection("conn").ForAccount("acc") ctx.Set("logger", log) s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx)) diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index a7d0723..08e887b 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -57,9 +57,16 @@ func (l *BufferLogger) WithLazy(fields ...zapcore.Field) logger.Logger { } } -func (l *BufferLogger) ForAccount(handler, conn, acc any) logger.Logger { - return l.WithLazy( - zap.Any(logger.HandlerAttr, handler), zap.Any(logger.ConnectionAttr, conn), zap.Any(logger.AccountAttr, acc)) +func (l *BufferLogger) ForHandler(handler any) logger.Logger { + return l.WithLazy(zap.Any(logger.HandlerAttr, handler)) +} + +func (l *BufferLogger) ForConnection(conn any) logger.Logger { + return l.WithLazy(zap.Any(logger.ConnectionAttr, conn)) +} + +func (l *BufferLogger) ForAccount(acc any) logger.Logger { + return l.WithLazy(zap.Any(logger.AccountAttr, acc)) } // Read bytes from the logger buffer. io.Reader implementation. From 3afa198be231f6aaa79f32830c633047ed0a98a5 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 19 Jan 2024 13:20:16 +0300 Subject: [PATCH 16/26] remove slog completely --- core/util/httputil/http_client_builder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/util/httputil/http_client_builder.go b/core/util/httputil/http_client_builder.go index 3d8bf3a..a27dd1c 100644 --- a/core/util/httputil/http_client_builder.go +++ b/core/util/httputil/http_client_builder.go @@ -5,7 +5,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "log/slog" + "go.uber.org/zap" "net" "net/http" "net/url" @@ -230,7 +230,7 @@ func (b *HTTPClientBuilder) buildMocks() error { } if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { - b.log("Mock address has been set", slog.String("address", net.JoinHostPort(b.mockHost, b.mockPort))) + b.log("Mock address has been set", zap.String("address", net.JoinHostPort(b.mockHost, b.mockPort))) b.log("Mocked domains: ") for _, domain := range b.mockedDomains { From aead3854e1dfe7b94e0751538dbac692b69d465c Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 19 Jan 2024 14:48:18 +0300 Subject: [PATCH 17/26] fix stack overflow --- core/logger/default.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/logger/default.go b/core/logger/default.go index 62f866d..b79f104 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -37,11 +37,11 @@ func NewDefault(debug bool) Logger { } func (l *Default) With(fields ...zap.Field) Logger { - return l.With(fields...).(Logger) + return l.clone(l.Logger.With(fields...)) } func (l *Default) WithLazy(fields ...zap.Field) Logger { - return l.WithLazy(fields...).(Logger) + return l.clone(l.Logger.WithLazy(fields...)) } func (l *Default) ForHandler(handler any) Logger { @@ -56,6 +56,10 @@ func (l *Default) ForAccount(acc any) Logger { return l.WithLazy(zap.Any(AccountAttr, acc)) } +func (l *Default) clone(log *zap.Logger) Logger { + return &Default{Logger: log} +} + func AnyZapFields(args []interface{}) []zap.Field { fields := make([]zap.Field, len(args)) for i := 0; i < len(fields); i++ { From f6c319752eb115992e5b2f0a0153ebbe42fbd627 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 10 Apr 2024 14:06:41 +0300 Subject: [PATCH 18/26] json logging support, uppercase level --- core/config/config.go | 6 ++ core/config/config_test.go | 5 ++ core/engine.go | 9 ++- core/job_manager_test.go | 2 +- core/logger/default.go | 4 +- core/logger/zap.go | 58 +++++++++++++++++-- .../util/httputil/http_client_builder_test.go | 2 +- core/util/testutil/buffer_logger.go | 2 +- core/util/testutil/buffer_logger_test.go | 6 +- core/util/utils_test.go | 2 +- 10 files changed, 81 insertions(+), 15 deletions(-) diff --git a/core/config/config.go b/core/config/config.go index e49edcc..ea3ee72 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -14,6 +14,7 @@ type Configuration interface { GetVersion() string GetSentryDSN() string GetLogLevel() logging.Level + GetLogFormat() string GetHTTPConfig() HTTPServerConfig GetZabbixConfig() ZabbixConfig GetDBConfig() DatabaseConfig @@ -44,6 +45,7 @@ type Config struct { Database DatabaseConfig `yaml:"database"` UpdateInterval int `yaml:"update_interval"` LogLevel logging.Level `yaml:"log_level"` + LogFormat string `yaml:"log_format"` Debug bool `yaml:"debug"` } @@ -153,6 +155,10 @@ func (c Config) GetLogLevel() logging.Level { return c.LogLevel } +func (c Config) GetLogFormat() string { + return c.LogFormat +} + // GetTransportInfo transport basic data. func (c Config) GetTransportInfo() InfoInterface { return c.TransportInfo diff --git a/core/config/config_test.go b/core/config/config_test.go index 0b143d4..2fa569c 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -38,6 +38,7 @@ transport_info: sentry_dsn: dsn string log_level: 5 +log_format: console debug: true update_interval: 24 @@ -90,6 +91,10 @@ func (c *ConfigTest) Test_GetLogLevel() { assert.Equal(c.T(), logging.Level(5), c.config.GetLogLevel()) } +func (c *ConfigTest) Test_GetLogFormat() { + assert.Equal(c.T(), "console", c.config.GetLogFormat()) +} + func (c *ConfigTest) Test_IsDebug() { assert.Equal(c.T(), true, c.config.IsDebug()) } diff --git a/core/engine.go b/core/engine.go index 8c318b0..6887ad4 100644 --- a/core/engine.go +++ b/core/engine.go @@ -13,7 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" - "github.com/retailcrm/zabbix-metrics-collector" + metrics "github.com/retailcrm/zabbix-metrics-collector" "go.uber.org/zap" "golang.org/x/text/language" @@ -141,9 +141,14 @@ func (e *Engine) Prepare() *Engine { e.Localizer.Preload(e.PreloadLanguages) } + logFormat := "json" + if format := e.Config.GetLogFormat(); format != "" { + logFormat = format + } + e.CreateDB(e.Config.GetDBConfig()) e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0) - e.SetLogger(logger.NewDefault(e.Config.IsDebug())) + e.SetLogger(logger.NewDefault(logFormat, e.Config.IsDebug())) e.Sentry.Localizer = &e.Localizer e.Utils.Logger = e.Logger() e.Sentry.Logger = e.Logger() diff --git a/core/job_manager_test.go b/core/job_manager_test.go index ae3fd82..b6cce5a 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -436,7 +436,7 @@ func (t *JobManagerTest) WaitForJob() bool { func (t *JobManagerTest) Test_SetLogger() { t.manager.logger = nil - t.manager.SetLogger(logger.NewDefault(true)) + t.manager.SetLogger(logger.NewDefault("console", true)) assert.IsType(t.T(), &logger.Default{}, t.manager.logger) t.manager.SetLogger(nil) diff --git a/core/logger/default.go b/core/logger/default.go index b79f104..8b1bb1f 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -30,9 +30,9 @@ type Default struct { *zap.Logger } -func NewDefault(debug bool) Logger { +func NewDefault(format string, debug bool) Logger { return &Default{ - Logger: NewZap(debug), + Logger: NewZap(format, debug), } } diff --git a/core/logger/zap.go b/core/logger/zap.go index 69db51e..52618a1 100644 --- a/core/logger/zap.go +++ b/core/logger/zap.go @@ -1,13 +1,25 @@ package logger import ( + "fmt" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) -func NewZap(debug bool) *zap.Logger { +func NewZap(format string, debug bool) *zap.Logger { + switch format { + case "json": + return NewZapJSON(debug) + case "console": + return NewZapConsole(debug) + default: + panic(fmt.Sprintf("unknown logger format: %s", format)) + } +} + +func NewZapConsole(debug bool) *zap.Logger { level := zapcore.InfoLevel if debug { level = zapcore.DebugLevel @@ -16,7 +28,7 @@ func NewZap(debug bool) *zap.Logger { Level: zap.NewAtomicLevelAt(level), Development: debug, Encoding: "console", - EncoderConfig: EncoderConfig(), + EncoderConfig: EncoderConfigConsole(), OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, }.Build() @@ -26,7 +38,7 @@ func NewZap(debug bool) *zap.Logger { return log } -func EncoderConfig() zapcore.EncoderConfig { +func EncoderConfigConsole() zapcore.EncoderConfig { return zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", @@ -37,7 +49,7 @@ func EncoderConfig() zapcore.EncoderConfig { StacktraceKey: "", LineEnding: "\n", EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { - encoder.AppendString("level=" + level.String()) + encoder.AppendString("level=" + level.CapitalString()) }, EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { encoder.AppendString("time=" + t.Format(time.RFC3339)) @@ -50,3 +62,41 @@ func EncoderConfig() zapcore.EncoderConfig { ConsoleSeparator: " ", } } + +func NewZapJSON(debug bool) *zap.Logger { + level := zapcore.InfoLevel + if debug { + level = zapcore.DebugLevel + } + log, err := zap.Config{ + Level: zap.NewAtomicLevelAt(level), + Development: debug, + Encoding: "json", + EncoderConfig: EncoderConfigJSON(), + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + }.Build() + if err != nil { + panic(err) + } + return log +} + +func EncoderConfigJSON() zapcore.EncoderConfig { + return zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "timestamp", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + StacktraceKey: "", + LineEnding: "\n", + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.RFC3339TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeName: zapcore.FullNameEncoder, + ConsoleSeparator: " ", + } +} diff --git a/core/util/httputil/http_client_builder_test.go b/core/util/httputil/http_client_builder_test.go index f2e9356..36d5931 100644 --- a/core/util/httputil/http_client_builder_test.go +++ b/core/util/httputil/http_client_builder_test.go @@ -142,7 +142,7 @@ func (t *HTTPClientBuilderTest) Test_WithLogger() { builder.WithLogger(nil) assert.Nil(t.T(), builder.logger) - log := logger.NewDefault(true) + log := logger.NewDefault("console", true) builder.WithLogger(log) assert.NotNil(t.T(), builder.logger) } diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index 08e887b..ff8ddf8 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -37,7 +37,7 @@ func NewBufferedLogger() BufferedLogger { bl.Logger = zap.New( zapcore.NewCore( zapcore.NewConsoleEncoder( - logger.EncoderConfig()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) + logger.EncoderConfigConsole()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) return bl } diff --git a/core/util/testutil/buffer_logger_test.go b/core/util/testutil/buffer_logger_test.go index dd4865c..f144082 100644 --- a/core/util/testutil/buffer_logger_test.go +++ b/core/util/testutil/buffer_logger_test.go @@ -29,17 +29,17 @@ func (t *BufferLoggerTest) Test_Read() { data, err := io.ReadAll(t.logger) t.Require().NoError(err) - t.Assert().Contains(string(data), "level=debug test") + t.Assert().Contains(string(data), "level=DEBUG test") } func (t *BufferLoggerTest) Test_Bytes() { t.logger.Debug("test") - t.Assert().Contains(string(t.logger.Bytes()), "level=debug test") + t.Assert().Contains(string(t.logger.Bytes()), "level=DEBUG test") } func (t *BufferLoggerTest) Test_String() { t.logger.Debug("test") - t.Assert().Contains(t.logger.String(), "level=debug test") + t.Assert().Contains(t.logger.String(), "level=DEBUG test") } func (t *BufferLoggerTest) TestRace() { diff --git a/core/util/utils_test.go b/core/util/utils_test.go index ffa2374..c1d577c 100644 --- a/core/util/utils_test.go +++ b/core/util/utils_test.go @@ -38,7 +38,7 @@ func mgClient() *v1.MgClient { } func (u *UtilsTest) SetupSuite() { - logger := logger.NewDefault(true) + logger := logger.NewDefault("console", true) awsConfig := config.AWS{ AccessKeyID: "access key id (will be removed)", SecretAccessKey: "secret access key", From 7c784c83109ab6c2195419850293898ce66da740 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 17 Apr 2024 11:42:31 +0300 Subject: [PATCH 19/26] change format to something like symfony monolog --- core/logger/zap.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/logger/zap.go b/core/logger/zap.go index 52618a1..53d598b 100644 --- a/core/logger/zap.go +++ b/core/logger/zap.go @@ -42,17 +42,17 @@ func EncoderConfigConsole() zapcore.EncoderConfig { return zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", - TimeKey: "timestamp", + TimeKey: "datetime", NameKey: "logger", CallerKey: "caller", FunctionKey: zapcore.OmitKey, StacktraceKey: "", LineEnding: "\n", EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { - encoder.AppendString("level=" + level.CapitalString()) + encoder.AppendString("level_name=" + level.CapitalString()) }, EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { - encoder.AppendString("time=" + t.Format(time.RFC3339)) + encoder.AppendString("datetime=" + t.Format(time.RFC3339)) }, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) { @@ -85,8 +85,8 @@ func NewZapJSON(debug bool) *zap.Logger { func EncoderConfigJSON() zapcore.EncoderConfig { return zapcore.EncoderConfig{ MessageKey: "message", - LevelKey: "level", - TimeKey: "timestamp", + LevelKey: "level_name", + TimeKey: "datetime", NameKey: "logger", CallerKey: "caller", FunctionKey: zapcore.OmitKey, From 28b73ae09f99fa255dfd7e00dd3f2056237ab94f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 7 Jun 2024 17:55:45 +0300 Subject: [PATCH 20/26] fix tests, update for logging --- core/healthcheck/processor_test.go | 82 ++- core/job_manager_test.go | 2 +- core/logger/api_client_adapter.go | 22 +- core/logger/api_client_adapter_test.go | 40 ++ core/logger/attrs.go | 66 +- core/logger/buffer_logger_test.go | 263 ++++++++ core/logger/bufferpool.go | 29 + core/logger/default.go | 26 + core/logger/json_with_context_encoder.go | 638 ++++++++++++++++++ core/logger/mg_transport_client_adapter.go | 45 +- .../mg_transport_client_adapter_test.go | 43 ++ core/logger/nil.go | 2 + core/logger/pool.go | 57 ++ core/logger/writer_adapter.go | 1 + core/logger/zabbix_collector_adapter.go | 17 +- core/logger/zap.go | 2 +- .../util/httputil/http_client_builder_test.go | 2 +- core/util/testutil/buffer_logger.go | 4 +- core/util/testutil/buffer_logger_test.go | 9 +- core/util/testutil/json_record_scanner.go | 51 ++ .../util/testutil/json_record_scanner_test.go | 107 +++ core/util/utils_test.go | 2 +- go.mod | 5 +- go.sum | 2 + 24 files changed, 1470 insertions(+), 47 deletions(-) create mode 100644 core/logger/api_client_adapter_test.go create mode 100644 core/logger/buffer_logger_test.go create mode 100644 core/logger/bufferpool.go create mode 100644 core/logger/json_with_context_encoder.go create mode 100644 core/logger/mg_transport_client_adapter_test.go create mode 100644 core/logger/pool.go create mode 100644 core/util/testutil/json_record_scanner.go create mode 100644 core/util/testutil/json_record_scanner_test.go diff --git a/core/healthcheck/processor_test.go b/core/healthcheck/processor_test.go index 0492454..e990629 100644 --- a/core/healthcheck/processor_test.go +++ b/core/healthcheck/processor_test.go @@ -45,7 +45,7 @@ func (t *CounterProcessorTest) localizer() NotifyMessageLocalizer { } func (t *CounterProcessorTest) new( - nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, testutil.BufferedLogger) { + nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, *testutil.JSONRecordScanner) { loc := t.localizer() if len(noLocalizer) > 0 && noLocalizer[0] { loc = nil @@ -61,7 +61,7 @@ func (t *CounterProcessorTest) new( FailureThreshold: DefaultFailureThreshold, MinRequests: DefaultMinRequests, Debug: true, - }, log + }, testutil.NewJSONRecordScanner(log) } func (t *CounterProcessorTest) notifier(err ...error) *notifierMock { @@ -95,8 +95,12 @@ func (t *CounterProcessorTest) Test_FailureProcessed() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "skipping counter because its failure is already processed") - t.Assert().Contains(log.String(), `"counterId": 1`) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "skipping counter because its failure is already processed") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) } func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { @@ -108,8 +112,12 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), `"counterId": 1`) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "cannot find connection data for counter") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) } func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { @@ -123,10 +131,14 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot send notification for counter") - t.Assert().Contains(log.String(), `"counterId": 1`) - t.Assert().Contains(log.String(), `"error": "http status code: 500"`) - t.Assert().Contains(log.String(), `"failureMessage": "error message"`) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "cannot send notification for counter") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) + t.Assert().Equal("http status code: 500", logs[0].Context["error"]) + t.Assert().Equal("error message", logs[0].Context["failureMessage"]) t.Assert().Equal(t.apiURL, n.apiURL) t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal("error message", n.message) @@ -143,7 +155,10 @@ func (t *CounterProcessorTest) Test_CounterFailed_SentNotification() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Empty(log.String()) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 0) t.Assert().Equal(t.apiURL, n.apiURL) t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal("error message", n.message) @@ -159,8 +174,13 @@ func (t *CounterProcessorTest) Test_TooFewRequests() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), - fmt.Sprintf(`skipping counter because it has too few requests {"counterId": %d, "minRequests": %d}`, 1, DefaultMinRequests)) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "skipping counter because it has too few requests") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) + t.Assert().Equal(float64(DefaultMinRequests), logs[0].Context["minRequests"]) } func (t *CounterProcessorTest) Test_ThresholdNotPassed() { @@ -175,7 +195,10 @@ func (t *CounterProcessorTest) Test_ThresholdNotPassed() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Empty(log.String()) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 0) t.Assert().Empty(n.message) } @@ -190,7 +213,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_AlreadyProcessed() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Empty(log.String()) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 0) t.Assert().Empty(n.message) } @@ -205,8 +231,12 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), "cannot find connection data for counter") - t.Assert().Contains(log.String(), `"counterId": 1`) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "cannot find connection data for counter") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) t.Assert().Empty(n.message) } @@ -224,7 +254,13 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Contains(log.String(), `cannot send notification for counter {"counterId": 1, "error": "unknown error", "failureMessage": ""}`) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 1) + t.Assert().Contains(logs[0].Message, "cannot send notification for counter") + t.Assert().Equal(float64(1), logs[0].Context["counterId"]) + t.Assert().Equal("unknown error", logs[0].Context["error"]) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message) } @@ -241,7 +277,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent() { p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Empty(log.String()) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 0) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message) } @@ -258,7 +297,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent_NoLocalizer p.Process(1, c) c.AssertExpectations(t.T()) - t.Assert().Empty(log.String()) + + logs, err := log.ScanAll() + t.Require().NoError(err) + t.Require().Len(logs, 0) t.Assert().Equal(`default error`, n.message) } diff --git a/core/job_manager_test.go b/core/job_manager_test.go index b6cce5a..40ed2a6 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -436,7 +436,7 @@ func (t *JobManagerTest) WaitForJob() bool { func (t *JobManagerTest) Test_SetLogger() { t.manager.logger = nil - t.manager.SetLogger(logger.NewDefault("console", true)) + t.manager.SetLogger(logger.NewDefault("json", true)) assert.IsType(t.T(), &logger.Default{}, t.manager.logger) t.manager.SetLogger(nil) diff --git a/core/logger/api_client_adapter.go b/core/logger/api_client_adapter.go index 7951336..1cb7d4c 100644 --- a/core/logger/api_client_adapter.go +++ b/core/logger/api_client_adapter.go @@ -2,10 +2,16 @@ package logger import ( "fmt" + "go.uber.org/zap" retailcrm "github.com/retailcrm/api-client-go/v2" ) +const ( + apiDebugLogReq = "API Request: %s %s" + apiDebugLogResp = "API Response: %s" +) + type apiClientAdapter struct { logger Logger } @@ -17,5 +23,19 @@ func APIClientAdapter(logger Logger) retailcrm.BasicLogger { // Printf data in the log using Debug method. func (l *apiClientAdapter) Printf(format string, v ...interface{}) { - l.logger.Debug(fmt.Sprintf(format, v...)) + switch format { + case apiDebugLogReq: + var url, key string + if len(v) > 0 { + url = fmt.Sprint(v[0]) + } + if len(v) > 1 { + key = fmt.Sprint(v[1]) + } + l.logger.Debug("API Request", zap.String("url", url), zap.String("key", key)) + case apiDebugLogResp: + l.logger.Debug("API Response", Body(v[0])) + default: + l.logger.Debug(fmt.Sprintf(format, v...)) + } } diff --git a/core/logger/api_client_adapter_test.go b/core/logger/api_client_adapter_test.go new file mode 100644 index 0000000..4ad51e7 --- /dev/null +++ b/core/logger/api_client_adapter_test.go @@ -0,0 +1,40 @@ +package logger + +import ( + "github.com/h2non/gock" + retailcrm "github.com/retailcrm/api-client-go/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) + +func TestAPIClientAdapter(t *testing.T) { + log := newJSONBufferedLogger() + client := retailcrm.New("https://example.com", "test_key").WithLogger(APIClientAdapter(log.Logger())) + client.Debug = true + + defer gock.Off() + gock.New("https://example.com"). + Get("/api/credentials"). + Reply(http.StatusOK). + JSON(retailcrm.CredentialResponse{Success: true}) + + _, _, err := client.APICredentials() + require.NoError(t, err) + + entries, err := log.ScanAll() + require.NoError(t, err) + require.Len(t, entries, 2) + + assert.Equal(t, "DEBUG", entries[0].LevelName) + assert.True(t, entries[0].DateTime.Valid) + assert.Equal(t, "API Request", entries[0].Message) + assert.Equal(t, "test_key", entries[0].Context["key"]) + assert.Equal(t, "https://example.com/api/credentials", entries[0].Context["url"]) + + assert.Equal(t, "DEBUG", entries[1].LevelName) + assert.True(t, entries[1].DateTime.Valid) + assert.Equal(t, "API Response", entries[1].Message) + assert.Equal(t, map[string]interface{}{"success": true}, entries[1].Context["body"]) +} diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 17bd746..73de78d 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -2,24 +2,44 @@ package logger import ( "fmt" + json "github.com/goccy/go-json" + "io" "net/http" "go.uber.org/zap" ) -const ( - HandlerAttr = "handler" - ConnectionAttr = "connection" - AccountAttr = "account" - CounterIDAttr = "counterId" - ErrorAttr = "error" - FailureMessageAttr = "failureMessage" - BodyAttr = "body" - HTTPMethodAttr = "method" - HTTPStatusAttr = "statusCode" - HTTPStatusNameAttr = "statusName" -) +// HandlerAttr represents the attribute name for the handler. +const HandlerAttr = "handler" +// ConnectionAttr represents the attribute name for the connection. +const ConnectionAttr = "connection" + +// AccountAttr represents the attribute name for the account. +const AccountAttr = "account" + +// CounterIDAttr represents the attribute name for the counter ID. +const CounterIDAttr = "counterId" + +// ErrorAttr represents the attribute name for an error. +const ErrorAttr = "error" + +// FailureMessageAttr represents the attribute name for a failure message. +const FailureMessageAttr = "failureMessage" + +// BodyAttr represents the attribute name for the request body. +const BodyAttr = "body" + +// HTTPMethodAttr represents the attribute name for the HTTP method. +const HTTPMethodAttr = "method" + +// HTTPStatusAttr represents the attribute name for the HTTP status code. +const HTTPStatusAttr = "statusCode" + +// HTTPStatusNameAttr represents the attribute name for the HTTP status name. +const HTTPStatusNameAttr = "statusName" + +// Err returns a zap.Field with the given error value. func Err(err any) zap.Field { if err == nil { return zap.String(ErrorAttr, "") @@ -27,24 +47,46 @@ func Err(err any) zap.Field { return zap.Any(ErrorAttr, err) } +// Handler returns a zap.Field with the given handler name. func Handler(name string) zap.Field { return zap.String(HandlerAttr, name) } +// HTTPStatusCode returns a zap.Field with the given HTTP status code. func HTTPStatusCode(code int) zap.Field { return zap.Int(HTTPStatusAttr, code) } +// HTTPStatusName returns a zap.Field with the given HTTP status name. func HTTPStatusName(code int) zap.Field { return zap.String(HTTPStatusNameAttr, http.StatusText(code)) } +// Body returns a zap.Field with the given request body value. func Body(val any) zap.Field { switch item := val.(type) { case string: + var m map[string]interface{} + if err := json.Unmarshal([]byte(item), &m); err == nil { + return zap.Any(BodyAttr, m) + } return zap.String(BodyAttr, item) case []byte: + var m interface{} + if err := json.Unmarshal(item, &m); err == nil { + return zap.Any(BodyAttr, m) + } return zap.String(BodyAttr, string(item)) + case io.Reader: + data, err := io.ReadAll(item) + if err != nil { + return zap.String(BodyAttr, fmt.Sprintf("%#v", val)) + } + var m interface{} + if err := json.Unmarshal(data, &m); err == nil { + return zap.Any(BodyAttr, m) + } + return zap.String(BodyAttr, string(data)) default: return zap.String(BodyAttr, fmt.Sprintf("%#v", val)) } diff --git a/core/logger/buffer_logger_test.go b/core/logger/buffer_logger_test.go new file mode 100644 index 0000000..7c0b193 --- /dev/null +++ b/core/logger/buffer_logger_test.go @@ -0,0 +1,263 @@ +package logger + +import ( + "bufio" + "bytes" + "encoding/json" + "github.com/guregu/null/v5" + "io" + "os" + "sync" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type logRecord struct { + LevelName string `json:"level_name"` + DateTime null.Time `json:"datetime"` + Caller string `json:"caller"` + Message string `json:"message"` + Handler string `json:"handler,omitempty"` + Connection string `json:"connection,omitempty"` + Account string `json:"account,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +type jSONRecordScanner struct { + scan *bufio.Scanner + buf *bufferLogger +} + +func newJSONBufferedLogger() *jSONRecordScanner { + buf := newBufferLogger() + return &jSONRecordScanner{scan: bufio.NewScanner(buf), buf: buf} +} + +func (s *jSONRecordScanner) ScanAll() ([]logRecord, error) { + var entries []logRecord + for s.scan.Scan() { + entry := logRecord{} + if err := json.Unmarshal(s.scan.Bytes(), &entry); err != nil { + return entries, err + } + entries = append(entries, entry) + } + return entries, nil +} + +func (s *jSONRecordScanner) Logger() Logger { + return s.buf +} + +// bufferLogger is an implementation of the BufferedLogger. +type bufferLogger struct { + Default + buf lockableBuffer +} + +// NewBufferedLogger returns new BufferedLogger instance. +func newBufferLogger() *bufferLogger { + bl := &bufferLogger{} + bl.Logger = zap.New( + zapcore.NewCore( + NewJSONWithContextEncoder( + EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) + return bl +} + +func (l *bufferLogger) With(fields ...zapcore.Field) Logger { + return &bufferLogger{ + Default: Default{ + Logger: l.Logger.With(fields...), + }, + } +} + +func (l *bufferLogger) WithLazy(fields ...zapcore.Field) Logger { + return &bufferLogger{ + Default: Default{ + Logger: l.Logger.WithLazy(fields...), + }, + } +} + +func (l *bufferLogger) ForHandler(handler any) Logger { + return l.WithLazy(zap.Any(HandlerAttr, handler)) +} + +func (l *bufferLogger) ForConnection(conn any) Logger { + return l.WithLazy(zap.Any(ConnectionAttr, conn)) +} + +func (l *bufferLogger) ForAccount(acc any) Logger { + return l.WithLazy(zap.Any(AccountAttr, acc)) +} + +// Read bytes from the logger buffer. io.Reader implementation. +func (l *bufferLogger) Read(p []byte) (n int, err error) { + return l.buf.Read(p) +} + +// String contents of the logger buffer. fmt.Stringer implementation. +func (l *bufferLogger) String() string { + return l.buf.String() +} + +// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents. +func (l *bufferLogger) Bytes() []byte { + return l.buf.Bytes() +} + +// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents. +func (l *bufferLogger) Reset() { + l.buf.Reset() +} + +type lockableBuffer struct { + buf bytes.Buffer + rw sync.RWMutex +} + +func (b *lockableBuffer) Bytes() []byte { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Bytes() +} + +func (b *lockableBuffer) AvailableBuffer() []byte { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.AvailableBuffer() +} + +func (b *lockableBuffer) String() string { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.String() +} + +func (b *lockableBuffer) Len() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Len() +} + +func (b *lockableBuffer) Cap() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Cap() +} + +func (b *lockableBuffer) Available() int { + defer b.rw.RUnlock() + b.rw.RLock() + return b.buf.Available() +} + +func (b *lockableBuffer) Truncate(n int) { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Truncate(n) +} + +func (b *lockableBuffer) Reset() { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Reset() +} + +func (b *lockableBuffer) Grow(n int) { + defer b.rw.Unlock() + b.rw.Lock() + b.buf.Grow(n) +} + +func (b *lockableBuffer) Write(p []byte) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Write(p) +} + +func (b *lockableBuffer) WriteString(s string) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteString(s) +} + +func (b *lockableBuffer) ReadFrom(r io.Reader) (n int64, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadFrom(r) +} + +func (b *lockableBuffer) WriteTo(w io.Writer) (n int64, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteTo(w) +} + +func (b *lockableBuffer) WriteByte(c byte) error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteByte(c) +} + +func (b *lockableBuffer) WriteRune(r rune) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.WriteRune(r) +} + +func (b *lockableBuffer) Read(p []byte) (n int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Read(p) +} + +func (b *lockableBuffer) Next(n int) []byte { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.Next(n) +} + +func (b *lockableBuffer) ReadByte() (byte, error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadByte() +} + +func (b *lockableBuffer) ReadRune() (r rune, size int, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadRune() +} + +func (b *lockableBuffer) UnreadRune() error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.UnreadRune() +} + +func (b *lockableBuffer) UnreadByte() error { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.UnreadByte() +} + +func (b *lockableBuffer) ReadBytes(delim byte) (line []byte, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadBytes(delim) +} + +func (b *lockableBuffer) ReadString(delim byte) (line string, err error) { + defer b.rw.Unlock() + b.rw.Lock() + return b.buf.ReadString(delim) +} + +// Sync is a no-op. +func (b *lockableBuffer) Sync() error { + return nil +} diff --git a/core/logger/bufferpool.go b/core/logger/bufferpool.go new file mode 100644 index 0000000..e0a4e8b --- /dev/null +++ b/core/logger/bufferpool.go @@ -0,0 +1,29 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package logger + +import "go.uber.org/zap/buffer" + +var ( + _pool = buffer.NewPool() + // GetBufferPool retrieves a buffer from the pool, creating one if necessary. + GetBufferPool = _pool.Get +) diff --git a/core/logger/default.go b/core/logger/default.go index 8b1bb1f..8ec6fc7 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -7,59 +7,85 @@ import ( "go.uber.org/zap/zapcore" ) +// Logger is a logging interface. type Logger interface { + // With adds fields to the logger and returns a new logger with those fields. With(fields ...zap.Field) Logger + // WithLazy adds fields to the logger lazily and returns a new logger with those fields. WithLazy(fields ...zap.Field) Logger + // Level returns the logging level of the logger. Level() zapcore.Level + // Check checks if the log message meets the given level. Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry + // Log logs a message with the given level and fields. Log(lvl zapcore.Level, msg string, fields ...zap.Field) + // Debug logs a debug-level message with the given fields. Debug(msg string, fields ...zap.Field) + // Info logs an info-level message with the given fields. Info(msg string, fields ...zap.Field) + // Warn logs a warning-level message with the given fields. Warn(msg string, fields ...zap.Field) + // Error logs an error-level message with the given fields. Error(msg string, fields ...zap.Field) + // DPanic logs a debug-panic-level message with the given fields and panics if the logger's panic level is set to a non-zero value. DPanic(msg string, fields ...zap.Field) + // Panic logs a panic-level message with the given fields and panics immediately. Panic(msg string, fields ...zap.Field) + // Fatal logs a fatal-level message with the given fields, then calls os.Exit(1). Fatal(msg string, fields ...zap.Field) + // ForHandler returns a new logger that is associated with the given handler. ForHandler(handler any) Logger + // ForConnection returns a new logger that is associated with the given connection. ForConnection(conn any) Logger + // ForAccount returns a new logger that is associated with the given account. ForAccount(acc any) Logger + // Sync returns an error if there's a problem writing log messages to disk, or nil if all writes were successful. Sync() error } +// Default is a default logger implementation. type Default struct { *zap.Logger } +// NewDefault creates a new default logger with the given format and debug level. func NewDefault(format string, debug bool) Logger { return &Default{ Logger: NewZap(format, debug), } } +// With adds fields to the logger and returns a new logger with those fields. func (l *Default) With(fields ...zap.Field) Logger { return l.clone(l.Logger.With(fields...)) } +// WithLazy adds fields to the logger lazily and returns a new logger with those fields. func (l *Default) WithLazy(fields ...zap.Field) Logger { return l.clone(l.Logger.WithLazy(fields...)) } +// ForHandler returns a new logger that is associated with the given handler. func (l *Default) ForHandler(handler any) Logger { return l.WithLazy(zap.Any(HandlerAttr, handler)) } +// ForConnection returns a new logger that is associated with the given connection. func (l *Default) ForConnection(conn any) Logger { return l.WithLazy(zap.Any(ConnectionAttr, conn)) } +// ForAccount returns a new logger that is associated with the given account. func (l *Default) ForAccount(acc any) Logger { return l.WithLazy(zap.Any(AccountAttr, acc)) } +// clone creates a copy of the given logger. func (l *Default) clone(log *zap.Logger) Logger { return &Default{Logger: log} } +// AnyZapFields converts an array of values to zap fields. func AnyZapFields(args []interface{}) []zap.Field { fields := make([]zap.Field, len(args)) for i := 0; i < len(fields); i++ { diff --git a/core/logger/json_with_context_encoder.go b/core/logger/json_with_context_encoder.go new file mode 100644 index 0000000..cb377e7 --- /dev/null +++ b/core/logger/json_with_context_encoder.go @@ -0,0 +1,638 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package logger + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "io" + "math" + "time" + "unicode/utf8" + + "go.uber.org/zap/buffer" +) + +// For JSON-escaping; see jsonWithContextEncoder.safeAddString below. +const _hex = "0123456789abcdef" + +var _jsonWithContextPool = NewPool(func() *jsonWithContextEncoder { + return &jsonWithContextEncoder{} +}) + +func init() { + err := zap.RegisterEncoder("json-with-context", func(config zapcore.EncoderConfig) (zapcore.Encoder, error) { + return NewJSONWithContextEncoder(config), nil + }) + if err != nil { + panic(err) + } +} + +func putJSONWithContextEncoder(enc *jsonWithContextEncoder) { + if enc.reflectBuf != nil { + enc.reflectBuf.Free() + } + enc.EncoderConfig = nil + enc.buf = nil + enc.spaced = false + enc.openNamespaces = 0 + enc.reflectBuf = nil + enc.reflectEnc = nil + _jsonWithContextPool.Put(enc) +} + +type jsonWithContextEncoder struct { + *zapcore.EncoderConfig + buf *buffer.Buffer + spaced bool // include spaces after colons and commas + openNamespaces int + + // for encoding generic values by reflection + reflectBuf *buffer.Buffer + reflectEnc zapcore.ReflectedEncoder +} + +// NewJSONWithContextEncoder creates a fast, low-allocation JSON encoder. The encoder +// appropriately escapes all field keys and values. +// +// Note that the encoder doesn't deduplicate keys, so it's possible to produce +// a message like +// +// {"foo":"bar","foo":"baz"} +// +// This is permitted by the JSON specification, but not encouraged. Many +// libraries will ignore duplicate key-value pairs (typically keeping the last +// pair) when unmarshaling, but users should attempt to avoid adding duplicate +// keys. +func NewJSONWithContextEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder { + return newJSONWithContextEncoder(cfg, false) +} + +func newJSONWithContextEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonWithContextEncoder { + if cfg.SkipLineEnding { + cfg.LineEnding = "" + } else if cfg.LineEnding == "" { + cfg.LineEnding = zapcore.DefaultLineEnding + } + + // If no EncoderConfig.NewReflectedEncoder is provided by the user, then use default + if cfg.NewReflectedEncoder == nil { + cfg.NewReflectedEncoder = defaultReflectedEncoder + } + + return &jsonWithContextEncoder{ + EncoderConfig: &cfg, + buf: GetBufferPool(), + spaced: spaced, + } +} + +func defaultReflectedEncoder(w io.Writer) zapcore.ReflectedEncoder { + enc := json.NewEncoder(w) + // For consistency with our custom JSON encoder. + enc.SetEscapeHTML(false) + return enc +} + +func (enc *jsonWithContextEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error { + enc.addKey(key) + return enc.AppendArray(arr) +} + +func (enc *jsonWithContextEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error { + enc.addKey(key) + return enc.AppendObject(obj) +} + +func (enc *jsonWithContextEncoder) AddBinary(key string, val []byte) { + enc.AddString(key, base64.StdEncoding.EncodeToString(val)) +} + +func (enc *jsonWithContextEncoder) AddByteString(key string, val []byte) { + enc.addKey(key) + enc.AppendByteString(val) +} + +func (enc *jsonWithContextEncoder) AddBool(key string, val bool) { + enc.addKey(key) + enc.AppendBool(val) +} + +func (enc *jsonWithContextEncoder) AddComplex128(key string, val complex128) { + enc.addKey(key) + enc.AppendComplex128(val) +} + +func (enc *jsonWithContextEncoder) AddComplex64(key string, val complex64) { + enc.addKey(key) + enc.AppendComplex64(val) +} + +func (enc *jsonWithContextEncoder) AddDuration(key string, val time.Duration) { + enc.addKey(key) + enc.AppendDuration(val) +} + +func (enc *jsonWithContextEncoder) AddFloat64(key string, val float64) { + enc.addKey(key) + enc.AppendFloat64(val) +} + +func (enc *jsonWithContextEncoder) AddFloat32(key string, val float32) { + enc.addKey(key) + enc.AppendFloat32(val) +} + +func (enc *jsonWithContextEncoder) AddInt64(key string, val int64) { + enc.addKey(key) + enc.AppendInt64(val) +} + +func (enc *jsonWithContextEncoder) resetReflectBuf() { + if enc.reflectBuf == nil { + enc.reflectBuf = GetBufferPool() + enc.reflectEnc = enc.NewReflectedEncoder(enc.reflectBuf) + } else { + enc.reflectBuf.Reset() + } +} + +var nullLiteralBytes = []byte("null") + +// Only invoke the standard JSON encoder if there is actually something to +// encode; otherwise write JSON null literal directly. +func (enc *jsonWithContextEncoder) encodeReflected(obj interface{}) ([]byte, error) { + if obj == nil { + return nullLiteralBytes, nil + } + enc.resetReflectBuf() + if err := enc.reflectEnc.Encode(obj); err != nil { + return nil, err + } + enc.reflectBuf.TrimNewline() + return enc.reflectBuf.Bytes(), nil +} + +func (enc *jsonWithContextEncoder) AddReflected(key string, obj interface{}) error { + valueBytes, err := enc.encodeReflected(obj) + if err != nil { + return err + } + enc.addKey(key) + _, err = enc.buf.Write(valueBytes) + return err +} + +func (enc *jsonWithContextEncoder) OpenNamespace(key string) { + enc.addKey(key) + enc.buf.AppendByte('{') + enc.openNamespaces++ +} + +func (enc *jsonWithContextEncoder) AddString(key, val string) { + enc.addKey(key) + enc.AppendString(val) +} + +func (enc *jsonWithContextEncoder) AddTime(key string, val time.Time) { + enc.addKey(key) + enc.AppendTime(val) +} + +func (enc *jsonWithContextEncoder) AddUint64(key string, val uint64) { + enc.addKey(key) + enc.AppendUint64(val) +} + +func (enc *jsonWithContextEncoder) AppendArray(arr zapcore.ArrayMarshaler) error { + enc.addElementSeparator() + enc.buf.AppendByte('[') + err := arr.MarshalLogArray(enc) + enc.buf.AppendByte(']') + return err +} + +func (enc *jsonWithContextEncoder) AppendObject(obj zapcore.ObjectMarshaler) error { + // Close ONLY new openNamespaces that are created during + // AppendObject(). + old := enc.openNamespaces + enc.openNamespaces = 0 + enc.addElementSeparator() + enc.buf.AppendByte('{') + err := obj.MarshalLogObject(enc) + enc.buf.AppendByte('}') + enc.closeOpenNamespaces() + enc.openNamespaces = old + return err +} + +func (enc *jsonWithContextEncoder) AppendBool(val bool) { + enc.addElementSeparator() + enc.buf.AppendBool(val) +} + +func (enc *jsonWithContextEncoder) AppendByteString(val []byte) { + enc.addElementSeparator() + enc.buf.AppendByte('"') + enc.safeAddByteString(val) + enc.buf.AppendByte('"') +} + +// appendComplex appends the encoded form of the provided complex128 value. +// precision specifies the encoding precision for the real and imaginary +// components of the complex number. +func (enc *jsonWithContextEncoder) appendComplex(val complex128, precision int) { + enc.addElementSeparator() + // Cast to a platform-independent, fixed-size type. + r, i := float64(real(val)), float64(imag(val)) + enc.buf.AppendByte('"') + // Because we're always in a quoted string, we can use strconv without + // special-casing NaN and +/-Inf. + enc.buf.AppendFloat(r, precision) + // If imaginary part is less than 0, minus (-) sign is added by default + // by AppendFloat. + if i >= 0 { + enc.buf.AppendByte('+') + } + enc.buf.AppendFloat(i, precision) + enc.buf.AppendByte('i') + enc.buf.AppendByte('"') +} + +func (enc *jsonWithContextEncoder) AppendDuration(val time.Duration) { + cur := enc.buf.Len() + if e := enc.EncodeDuration; e != nil { + e(val, enc) + } + if cur == enc.buf.Len() { + // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep + // JSON valid. + enc.AppendInt64(int64(val)) + } +} + +func (enc *jsonWithContextEncoder) AppendInt64(val int64) { + enc.addElementSeparator() + enc.buf.AppendInt(val) +} + +func (enc *jsonWithContextEncoder) AppendReflected(val interface{}) error { + valueBytes, err := enc.encodeReflected(val) + if err != nil { + return err + } + enc.addElementSeparator() + _, err = enc.buf.Write(valueBytes) + return err +} + +func (enc *jsonWithContextEncoder) AppendString(val string) { + enc.addElementSeparator() + enc.buf.AppendByte('"') + enc.safeAddString(val) + enc.buf.AppendByte('"') +} + +func (enc *jsonWithContextEncoder) AppendTimeLayout(time time.Time, layout string) { + enc.addElementSeparator() + enc.buf.AppendByte('"') + enc.buf.AppendTime(time, layout) + enc.buf.AppendByte('"') +} + +func (enc *jsonWithContextEncoder) AppendTime(val time.Time) { + cur := enc.buf.Len() + if e := enc.EncodeTime; e != nil { + e(val, enc) + } + if cur == enc.buf.Len() { + // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep + // output JSON valid. + enc.AppendInt64(val.UnixNano()) + } +} + +func (enc *jsonWithContextEncoder) AppendUint64(val uint64) { + enc.addElementSeparator() + enc.buf.AppendUint(val) +} + +func (enc *jsonWithContextEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) } +func (enc *jsonWithContextEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) } +func (enc *jsonWithContextEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) } +func (enc *jsonWithContextEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) } +func (enc *jsonWithContextEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) } +func (enc *jsonWithContextEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) } +func (enc *jsonWithContextEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) } +func (enc *jsonWithContextEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) } +func (enc *jsonWithContextEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) } +func (enc *jsonWithContextEncoder) AppendComplex64(v complex64) { enc.appendComplex(complex128(v), 32) } +func (enc *jsonWithContextEncoder) AppendComplex128(v complex128) { + enc.appendComplex(complex128(v), 64) +} +func (enc *jsonWithContextEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) } +func (enc *jsonWithContextEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) } +func (enc *jsonWithContextEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) } +func (enc *jsonWithContextEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) } +func (enc *jsonWithContextEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) } +func (enc *jsonWithContextEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) } +func (enc *jsonWithContextEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) } +func (enc *jsonWithContextEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) } +func (enc *jsonWithContextEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) } +func (enc *jsonWithContextEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) } +func (enc *jsonWithContextEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) } + +func (enc *jsonWithContextEncoder) Clone() zapcore.Encoder { + clone := enc.clone() + clone.buf.Write(enc.buf.Bytes()) + return clone +} + +func (enc *jsonWithContextEncoder) clone() *jsonWithContextEncoder { + clone := _jsonWithContextPool.Get() + clone.EncoderConfig = enc.EncoderConfig + clone.spaced = enc.spaced + clone.openNamespaces = enc.openNamespaces + clone.buf = GetBufferPool() + return clone +} + +func (enc *jsonWithContextEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + final := enc.clone() + final.buf.AppendByte('{') + + if final.LevelKey != "" && final.EncodeLevel != nil { + final.addKey(final.LevelKey) + cur := final.buf.Len() + final.EncodeLevel(ent.Level, final) + if cur == final.buf.Len() { + // User-supplied EncodeLevel was a no-op. Fall back to strings to keep + // output JSON valid. + final.AppendString(ent.Level.String()) + } + } + if final.TimeKey != "" { + final.AddTime(final.TimeKey, ent.Time) + } + if ent.LoggerName != "" && final.NameKey != "" { + final.addKey(final.NameKey) + cur := final.buf.Len() + nameEncoder := final.EncodeName + + // if no name encoder provided, fall back to FullNameEncoder for backwards + // compatibility + if nameEncoder == nil { + nameEncoder = zapcore.FullNameEncoder + } + + nameEncoder(ent.LoggerName, final) + if cur == final.buf.Len() { + // User-supplied EncodeName was a no-op. Fall back to strings to + // keep output JSON valid. + final.AppendString(ent.LoggerName) + } + } + if ent.Caller.Defined { + if final.CallerKey != "" { + final.addKey(final.CallerKey) + cur := final.buf.Len() + final.EncodeCaller(ent.Caller, final) + if cur == final.buf.Len() { + // User-supplied EncodeCaller was a no-op. Fall back to strings to + // keep output JSON valid. + final.AppendString(ent.Caller.String()) + } + } + if final.FunctionKey != "" { + final.addKey(final.FunctionKey) + final.AppendString(ent.Caller.Function) + } + } + if final.MessageKey != "" { + final.addKey(enc.MessageKey) + final.AppendString(ent.Message) + } + if enc.buf.Len() > 0 { + final.addElementSeparator() + final.buf.Write(enc.buf.Bytes()) + } + addFields(final, fields) + final.closeOpenNamespaces() + if ent.Stack != "" && final.StacktraceKey != "" { + final.AddString(final.StacktraceKey, ent.Stack) + } + final.buf.AppendByte('}') + final.buf.AppendString(final.LineEnding) + + ret := final.buf + putJSONWithContextEncoder(final) + return ret, nil +} + +func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) { + m := make(map[string]interface{}) + hasEntries := false + for _, f := range fields { + switch f.Key { + case HandlerAttr, ConnectionAttr, AccountAttr: + f.AddTo(enc) + default: + hasEntries = true + if f.Interface != nil { + switch t := f.Interface.(type) { + case fmt.Stringer: + m[f.Key] = t.String() + case fmt.GoStringer: + m[f.Key] = t.GoString() + case error: + m[f.Key] = t.Error() + default: + m[f.Key] = f.Interface + } + continue + } + if f.String != "" { + m[f.Key] = f.String + continue + } + m[f.Key] = f.Integer + } + } + if hasEntries { + zap.Any("context", m).AddTo(enc) + } +} + +func (enc *jsonWithContextEncoder) truncate() { + enc.buf.Reset() +} + +func (enc *jsonWithContextEncoder) closeOpenNamespaces() { + for i := 0; i < enc.openNamespaces; i++ { + enc.buf.AppendByte('}') + } + enc.openNamespaces = 0 +} + +func (enc *jsonWithContextEncoder) addKey(key string) { + enc.addElementSeparator() + enc.buf.AppendByte('"') + enc.safeAddString(key) + enc.buf.AppendByte('"') + enc.buf.AppendByte(':') + if enc.spaced { + enc.buf.AppendByte(' ') + } +} + +func (enc *jsonWithContextEncoder) addElementSeparator() { + last := enc.buf.Len() - 1 + if last < 0 { + return + } + switch enc.buf.Bytes()[last] { + case '{', '[', ':', ',', ' ': + return + default: + enc.buf.AppendByte(',') + if enc.spaced { + enc.buf.AppendByte(' ') + } + } +} + +func (enc *jsonWithContextEncoder) appendFloat(val float64, bitSize int) { + enc.addElementSeparator() + switch { + case math.IsNaN(val): + enc.buf.AppendString(`"NaN"`) + case math.IsInf(val, 1): + enc.buf.AppendString(`"+Inf"`) + case math.IsInf(val, -1): + enc.buf.AppendString(`"-Inf"`) + default: + enc.buf.AppendFloat(val, bitSize) + } +} + +// safeAddString JSON-escapes a string and appends it to the internal buffer. +// Unlike the standard library's encoder, it doesn't attempt to protect the +// user from browser vulnerabilities or JSONP-related problems. +func (enc *jsonWithContextEncoder) safeAddString(s string) { + safeAppendStringLike( + (*buffer.Buffer).AppendString, + utf8.DecodeRuneInString, + enc.buf, + s, + ) +} + +// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte. +func (enc *jsonWithContextEncoder) safeAddByteString(s []byte) { + safeAppendStringLike( + (*buffer.Buffer).AppendBytes, + utf8.DecodeRune, + enc.buf, + s, + ) +} + +// safeAppendStringLike is a generic implementation of safeAddString and safeAddByteString. +// It appends a string or byte slice to the buffer, escaping all special characters. +func safeAppendStringLike[S []byte | string]( + // appendTo appends this string-like object to the buffer. + appendTo func(*buffer.Buffer, S), + // decodeRune decodes the next rune from the string-like object + // and returns its value and width in bytes. + decodeRune func(S) (rune, int), + buf *buffer.Buffer, + s S, +) { + // The encoding logic below works by skipping over characters + // that can be safely copied as-is, + // until a character is found that needs special handling. + // At that point, we copy everything we've seen so far, + // and then handle that special character. + // + // last is the index of the last byte that was copied to the buffer. + last := 0 + for i := 0; i < len(s); { + if s[i] >= utf8.RuneSelf { + // Character >= RuneSelf may be part of a multi-byte rune. + // They need to be decoded before we can decide how to handle them. + r, size := decodeRune(s[i:]) + if r != utf8.RuneError || size != 1 { + // No special handling required. + // Skip over this rune and continue. + i += size + continue + } + + // Invalid UTF-8 sequence. + // Replace it with the Unicode replacement character. + appendTo(buf, s[last:i]) + buf.AppendString(`\ufffd`) + + i++ + last = i + } else { + // Character < RuneSelf is a single-byte UTF-8 rune. + if s[i] >= 0x20 && s[i] != '\\' && s[i] != '"' { + // No escaping necessary. + // Skip over this character and continue. + i++ + continue + } + + // This character needs to be escaped. + appendTo(buf, s[last:i]) + switch s[i] { + case '\\', '"': + buf.AppendByte('\\') + buf.AppendByte(s[i]) + case '\n': + buf.AppendByte('\\') + buf.AppendByte('n') + case '\r': + buf.AppendByte('\\') + buf.AppendByte('r') + case '\t': + buf.AppendByte('\\') + buf.AppendByte('t') + default: + // Encode bytes < 0x20, except for the escape sequences above. + buf.AppendString(`\u00`) + buf.AppendByte(_hex[s[i]>>4]) + buf.AppendByte(_hex[s[i]&0xF]) + } + + i++ + last = i + } + } + + // add remaining + appendTo(buf, s[last:]) +} diff --git a/core/logger/mg_transport_client_adapter.go b/core/logger/mg_transport_client_adapter.go index 917056f..345b5b3 100644 --- a/core/logger/mg_transport_client_adapter.go +++ b/core/logger/mg_transport_client_adapter.go @@ -2,18 +2,57 @@ package logger import ( "fmt" - v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "go.uber.org/zap" +) + +const ( + mgDebugLogReq = "MG TRANSPORT API Request: %s %s %s %v" + mgDebugLogReqFile = "MG TRANSPORT API Request: %s %s %s [file data]" + mgDebugLogResp = "MG TRANSPORT API Response: %s" ) type mgTransportClientAdapter struct { log Logger } -func MGTransportClientAdapter(log Logger) v1.DebugLogger { +// MGTransportClientAdapter constructs an adapter that will log MG requests and responses. +func MGTransportClientAdapter(log Logger) v1.BasicLogger { return &mgTransportClientAdapter{log: log} } +// Debugf writes a message with Debug level. func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) { - m.log.Debug(fmt.Sprintf(msg, args...)) + var body interface{} + switch msg { + case mgDebugLogReqFile: + body = "[file data]" + fallthrough + case mgDebugLogReq: + var method, uri, token string + if len(args) > 0 { + method = fmt.Sprint(args[0]) + } + if len(args) > 1 { + uri = fmt.Sprint(args[1]) + } + if len(args) > 2 { + token = fmt.Sprint(args[2]) + } + if len(args) > 3 { + body = args[3] + } + m.log.Debug("MG TRANSPORT API Request", + zap.String(HTTPMethodAttr, method), zap.String("url", uri), + zap.String("token", token), Body(body)) + case mgDebugLogResp: + m.log.Debug("MG TRANSPORT API Response", Body(args[0])) + default: + m.log.Debug(fmt.Sprintf(msg, args...)) + } +} + +// Printf is a v1.BasicLogger implementation. +func (m *mgTransportClientAdapter) Printf(msg string, args ...interface{}) { + m.Debugf(msg, args...) } diff --git a/core/logger/mg_transport_client_adapter_test.go b/core/logger/mg_transport_client_adapter_test.go new file mode 100644 index 0000000..262e389 --- /dev/null +++ b/core/logger/mg_transport_client_adapter_test.go @@ -0,0 +1,43 @@ +package logger + +import ( + "github.com/h2non/gock" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) + +func TestMGTransportClientAdapter(t *testing.T) { + httpClient := &http.Client{} + log := newJSONBufferedLogger() + client := v1.NewWithClient("https://mg.dev", "test_token", httpClient). + WithLogger(MGTransportClientAdapter(log.Logger())) + client.Debug = true + + defer gock.Off() + gock.New("https://mg.dev"). + Get("/api/transport/v1/channels"). + Reply(http.StatusOK). + JSON([]v1.ChannelListItem{{ID: 123}}) + + _, _, err := client.TransportChannels(v1.Channels{}) + require.NoError(t, err) + + entries, err := log.ScanAll() + require.NoError(t, err) + require.Len(t, entries, 2) + + assert.Equal(t, "DEBUG", entries[0].LevelName) + assert.True(t, entries[0].DateTime.Valid) + assert.Equal(t, "MG TRANSPORT API Request", entries[0].Message) + assert.Equal(t, http.MethodGet, entries[0].Context["method"]) + assert.Equal(t, "test_token", entries[0].Context["token"]) + assert.Equal(t, "https://mg.dev/api/transport/v1/channels?", entries[0].Context["url"]) + + assert.Equal(t, "DEBUG", entries[1].LevelName) + assert.True(t, entries[1].DateTime.Valid) + assert.Equal(t, "MG TRANSPORT API Response", entries[1].Message) + assert.Equal(t, float64(123), entries[1].Context["body"].([]interface{})[0].(map[string]interface{})["id"]) +} diff --git a/core/logger/nil.go b/core/logger/nil.go index 282a13d..f6d0f95 100644 --- a/core/logger/nil.go +++ b/core/logger/nil.go @@ -5,8 +5,10 @@ import ( "go.uber.org/zap/zapcore" ) +// Nil logger doesn't do anything. type Nil struct{} +// NewNil constructs new *Nil. func NewNil() Logger { return &Nil{} } diff --git a/core/logger/pool.go b/core/logger/pool.go new file mode 100644 index 0000000..51695dc --- /dev/null +++ b/core/logger/pool.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package logger + +import ( + "sync" +) + +// A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed +// object pooling. +// +// Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will +// not be detected, so all internal pool use must take care to only store +// pointer types. +type Pool[T any] struct { + pool sync.Pool +} + +// NewPool returns a new [Pool] for T, and will use fn to construct new Ts when +// the pool is empty. +func NewPool[T any](fn func() T) *Pool[T] { + return &Pool[T]{ + pool: sync.Pool{ + New: func() any { + return fn() + }, + }, + } +} + +// Get gets a T from the pool, or creates a new one if the pool is empty. +func (p *Pool[T]) Get() T { + return p.pool.Get().(T) +} + +// Put returns x into the pool. +func (p *Pool[T]) Put(x T) { + p.pool.Put(x) +} diff --git a/core/logger/writer_adapter.go b/core/logger/writer_adapter.go index e80459e..cceca9e 100644 --- a/core/logger/writer_adapter.go +++ b/core/logger/writer_adapter.go @@ -11,6 +11,7 @@ type writerAdapter struct { level zapcore.Level } +// WriterAdapter returns an io.Writer that can be used to write log messages. Message level is preconfigured. func WriterAdapter(log Logger, level zapcore.Level) io.Writer { return &writerAdapter{log: log, level: level} } diff --git a/core/logger/zabbix_collector_adapter.go b/core/logger/zabbix_collector_adapter.go index 0dc9067..e81345e 100644 --- a/core/logger/zabbix_collector_adapter.go +++ b/core/logger/zabbix_collector_adapter.go @@ -11,9 +11,24 @@ type zabbixCollectorAdapter struct { } func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) { - a.log.Error(fmt.Sprintf(format, args...)) + baseMsg := "cannot send metrics to Zabbix" + switch format { + case "cannot send metrics to Zabbix: %v": + baseMsg = "cannot stop collector" + fallthrough + case "cannot stop collector: %s": + var err interface{} + if len(args) > 0 { + err = args[0] + } + a.log.Error(baseMsg, Err(err)) + default: + a.log.Error(fmt.Sprintf(format, args...)) + } } +// ZabbixCollectorAdapter works as a logger adapter for Zabbix metrics collector. +// It can extract error messages from Zabbix collector and convert them to structured format. func ZabbixCollectorAdapter(log Logger) metrics.ErrorLogger { return &zabbixCollectorAdapter{log: log} } diff --git a/core/logger/zap.go b/core/logger/zap.go index 53d598b..1b8730b 100644 --- a/core/logger/zap.go +++ b/core/logger/zap.go @@ -71,7 +71,7 @@ func NewZapJSON(debug bool) *zap.Logger { log, err := zap.Config{ Level: zap.NewAtomicLevelAt(level), Development: debug, - Encoding: "json", + Encoding: "json-with-context", EncoderConfig: EncoderConfigJSON(), OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, diff --git a/core/util/httputil/http_client_builder_test.go b/core/util/httputil/http_client_builder_test.go index 36d5931..862783a 100644 --- a/core/util/httputil/http_client_builder_test.go +++ b/core/util/httputil/http_client_builder_test.go @@ -142,7 +142,7 @@ func (t *HTTPClientBuilderTest) Test_WithLogger() { builder.WithLogger(nil) assert.Nil(t.T(), builder.logger) - log := logger.NewDefault("console", true) + log := logger.NewDefault("json", true) builder.WithLogger(log) assert.NotNil(t.T(), builder.logger) } diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index ff8ddf8..9df5871 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -36,8 +36,8 @@ func NewBufferedLogger() BufferedLogger { bl := &BufferLogger{} bl.Logger = zap.New( zapcore.NewCore( - zapcore.NewConsoleEncoder( - logger.EncoderConfigConsole()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) + logger.NewJSONWithContextEncoder( + logger.EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel)) return bl } diff --git a/core/util/testutil/buffer_logger_test.go b/core/util/testutil/buffer_logger_test.go index f144082..8d0771d 100644 --- a/core/util/testutil/buffer_logger_test.go +++ b/core/util/testutil/buffer_logger_test.go @@ -29,17 +29,20 @@ func (t *BufferLoggerTest) Test_Read() { data, err := io.ReadAll(t.logger) t.Require().NoError(err) - t.Assert().Contains(string(data), "level=DEBUG test") + t.Assert().Contains(string(data), "\"level_name\":\"DEBUG\"") + t.Assert().Contains(string(data), "\"message\":\"test\"") } func (t *BufferLoggerTest) Test_Bytes() { t.logger.Debug("test") - t.Assert().Contains(string(t.logger.Bytes()), "level=DEBUG test") + t.Assert().Contains(string(t.logger.Bytes()), "\"level_name\":\"DEBUG\"") + t.Assert().Contains(string(t.logger.Bytes()), "\"message\":\"test\"") } func (t *BufferLoggerTest) Test_String() { t.logger.Debug("test") - t.Assert().Contains(t.logger.String(), "level=DEBUG test") + t.Assert().Contains(t.logger.String(), "\"level_name\":\"DEBUG\"") + t.Assert().Contains(t.logger.String(), "\"message\":\"test\"") } func (t *BufferLoggerTest) TestRace() { diff --git a/core/util/testutil/json_record_scanner.go b/core/util/testutil/json_record_scanner.go new file mode 100644 index 0000000..2a422c1 --- /dev/null +++ b/core/util/testutil/json_record_scanner.go @@ -0,0 +1,51 @@ +package testutil + +import ( + "bufio" + "encoding/json" + "github.com/guregu/null/v5" + "io" +) + +type LogRecord struct { + LevelName string `json:"level_name"` + DateTime null.Time `json:"datetime"` + Caller string `json:"caller"` + Message string `json:"message"` + Handler string `json:"handler,omitempty"` + Connection string `json:"connection,omitempty"` + Account string `json:"account,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +type JSONRecordScanner struct { + r *bufio.Scanner + e LogRecord +} + +func NewJSONRecordScanner(entryProvider io.Reader) *JSONRecordScanner { + return &JSONRecordScanner{r: bufio.NewScanner(entryProvider)} +} + +func (s *JSONRecordScanner) Scan() error { + if s.r.Scan() { + return json.Unmarshal(s.r.Bytes(), &s.e) + } + return io.EOF +} + +func (s *JSONRecordScanner) ScanAll() ([]LogRecord, error) { + var entries []LogRecord + for s.r.Scan() { + entry := LogRecord{} + if err := json.Unmarshal(s.r.Bytes(), &entry); err != nil { + return entries, err + } + entries = append(entries, entry) + } + return entries, nil +} + +func (s *JSONRecordScanner) Entry() LogRecord { + return s.e +} diff --git a/core/util/testutil/json_record_scanner_test.go b/core/util/testutil/json_record_scanner_test.go new file mode 100644 index 0000000..4cf4fea --- /dev/null +++ b/core/util/testutil/json_record_scanner_test.go @@ -0,0 +1,107 @@ +package testutil + +import ( + "bytes" + "github.com/stretchr/testify/suite" + "io" + "strings" + "testing" + "time" +) + +type JSONRecordScannerTest struct { + suite.Suite +} + +func TestJSONRecordScanner(t *testing.T) { + suite.Run(t, new(JSONRecordScannerTest)) +} + +func (t *JSONRecordScannerTest) new(lines []string) *JSONRecordScanner { + return NewJSONRecordScanner(bytes.NewReader([]byte(strings.Join(lines, "\n")))) +} + +func (t *JSONRecordScannerTest) newPredefined() *JSONRecordScanner { + return t.new([]string{strings.ReplaceAll(`{ + "level_name": "ERROR", + "datetime": "2024-06-07T13:49:17+03:00", + "caller": "handlers/account_middleware.go:147", + "message": "Cannot add account", + "handler": "handlers.addAccount", + "connection": "https://fake-uri.retailcrm.pro", + "account": "@username", + "context": { + "body": "[]string{\"integration_read\", \"integration_write\"}", + "statusCode": 500 + } +}`, "\n", "")}) +} + +func (t *JSONRecordScannerTest) assertPredefined(record LogRecord) { + ts, err := time.Parse(time.RFC3339, "2024-06-07T13:49:17+03:00") + t.Require().NoError(err) + t.Assert().True(record.DateTime.Valid) + t.Assert().Equal(ts, record.DateTime.Time) + t.Assert().Equal("ERROR", record.LevelName) + t.Assert().Equal("handlers/account_middleware.go:147", record.Caller) + t.Assert().Equal("Cannot add account", record.Message) + t.Assert().Equal("handlers.addAccount", record.Handler) + t.Assert().Equal("https://fake-uri.retailcrm.pro", record.Connection) + t.Assert().Equal("@username", record.Account) + t.Assert().Equal("[]string{\"integration_read\", \"integration_write\"}", record.Context["body"]) + t.Assert().Equal(float64(500), record.Context["statusCode"]) +} + +func (t *JSONRecordScannerTest) TestScan_NotJSON() { + rs := t.new([]string{"this is", "not json"}) + t.Assert().Error(rs.Scan()) +} + +func (t *JSONRecordScannerTest) TestScan_PartialJSON() { + rs := t.new([]string{"{}", "not json"}) + t.Assert().NoError(rs.Scan()) + t.Assert().Error(rs.Scan()) +} + +func (t *JSONRecordScannerTest) TestScan_JSON() { + rs := t.new([]string{"{}", "{}"}) + t.Assert().NoError(rs.Scan()) + t.Assert().NoError(rs.Scan()) + t.Assert().ErrorIs(rs.Scan(), io.EOF) +} + +func (t *JSONRecordScannerTest) TestScan_JSONRecord() { + rs := t.newPredefined() + t.Assert().NoError(rs.Scan()) + t.Assert().ErrorIs(rs.Scan(), io.EOF) + t.assertPredefined(rs.Entry()) +} + +func (t *JSONRecordScannerTest) TestScanAll_NotJSON() { + rs := t.new([]string{"this is", "not json"}) + records, err := rs.ScanAll() + t.Assert().Error(err) + t.Assert().Empty(records) +} + +func (t *JSONRecordScannerTest) TestScanAll_PartialJSON() { + rs := t.new([]string{"{}", "not json"}) + records, err := rs.ScanAll() + t.Assert().Error(err) + t.Assert().Len(records, 1) +} + +func (t *JSONRecordScannerTest) TestScanAll_JSON() { + rs := t.new([]string{"{}", "{}"}) + records, err := rs.ScanAll() + t.Assert().NoError(err) + t.Assert().Len(records, 2) +} + +func (t *JSONRecordScannerTest) TestScanAll_JSONRecord() { + rs := t.newPredefined() + records, err := rs.ScanAll() + t.Assert().NoError(err) + t.Assert().Len(records, 1) + t.assertPredefined(records[0]) +} diff --git a/core/util/utils_test.go b/core/util/utils_test.go index c1d577c..5372b52 100644 --- a/core/util/utils_test.go +++ b/core/util/utils_test.go @@ -38,7 +38,7 @@ func mgClient() *v1.MgClient { } func (u *UtilsTest) SetupSuite() { - logger := logger.NewDefault("console", true) + logger := logger.NewDefault("json", true) awsConfig := config.AWS{ AccessKeyID: "access key id (will be removed)", SecretAccessKey: "secret access key", diff --git a/go.mod b/go.mod index b458d9d..ea7bd71 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/retailcrm/mg-transport-core/v2 -go 1.21 +go 1.21.4 + +toolchain go1.22.0 require ( github.com/DATA-DOG/go-sqlmock v1.3.3 @@ -17,6 +19,7 @@ require ( github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.0 + github.com/guregu/null/v5 v5.0.0 github.com/h2non/gock v1.2.0 github.com/jessevdk/go-flags v1.4.0 github.com/jinzhu/gorm v1.9.11 diff --git a/go.sum b/go.sum index 3c610f9..5cf0ba1 100644 --- a/go.sum +++ b/go.sum @@ -238,6 +238,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw= +github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= From 6081756d39da0668a5f9ff99a2448bec41d39b57 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 7 Jun 2024 17:58:37 +0300 Subject: [PATCH 21/26] update test matrix --- .github/workflows/ci.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36d4a5e..58371b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.18', '1.19', '1.20', '1.21', '1.22', 'stable'] + go-version: ['1.22', 'stable'] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v3 diff --git a/go.mod b/go.mod index ea7bd71..4c29cc8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/retailcrm/mg-transport-core/v2 -go 1.21.4 +go 1.22 toolchain go1.22.0 From d233fd4cf115c7c2e559fe53fb434ed8ee01083b Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 7 Jun 2024 17:59:39 +0300 Subject: [PATCH 22/26] badge update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bb3cdb..c500d45 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Coverage](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/mg-transport-core) [![GitHub release](https://img.shields.io/github/release/retailcrm/mg-transport-core.svg?logo=github&logoColor=white)](https://github.com/retailcrm/mg-transport-core/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core) -[![GoLang version](https://img.shields.io/badge/go->=1.12-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/) +[![GoLang version](https://img.shields.io/badge/go->=1.22-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/) [![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core) This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports. From 983dbe722936d661bc11dcf26b44add2e594bbf3 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 14 Jun 2024 17:42:42 +0300 Subject: [PATCH 23/26] tests, more docs --- core/logger/api_client_adapter_test.go | 2 +- core/logger/attrs_test.go | 155 ++++++++++++++++++ core/logger/buffer_logger_test.go | 6 +- core/logger/default_test.go | 89 ++++++++++ core/logger/gin_test.go | 37 +++++ .../mg_transport_client_adapter_test.go | 2 +- core/logger/pool_test.go | 18 ++ core/logger/writer_adapter_test.go | 24 +++ core/logger/zabbix_collector_adapter.go | 6 +- core/logger/zabbix_collector_adapter_test.go | 25 +++ core/util/testutil/buffer_logger.go | 10 ++ 11 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 core/logger/attrs_test.go create mode 100644 core/logger/default_test.go create mode 100644 core/logger/gin_test.go create mode 100644 core/logger/pool_test.go create mode 100644 core/logger/writer_adapter_test.go create mode 100644 core/logger/zabbix_collector_adapter_test.go diff --git a/core/logger/api_client_adapter_test.go b/core/logger/api_client_adapter_test.go index 4ad51e7..eb2e6ca 100644 --- a/core/logger/api_client_adapter_test.go +++ b/core/logger/api_client_adapter_test.go @@ -10,7 +10,7 @@ import ( ) func TestAPIClientAdapter(t *testing.T) { - log := newJSONBufferedLogger() + log := newJSONBufferedLogger(nil) client := retailcrm.New("https://example.com", "test_key").WithLogger(APIClientAdapter(log.Logger())) client.Debug = true diff --git a/core/logger/attrs_test.go b/core/logger/attrs_test.go new file mode 100644 index 0000000..c3fdcf5 --- /dev/null +++ b/core/logger/attrs_test.go @@ -0,0 +1,155 @@ +package logger + +import ( + "bytes" + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "io" + "net/http" + "testing" +) + +func TestErr(t *testing.T) { + var cases = []struct { + source interface{} + expected string + }{ + { + source: nil, + expected: "", + }, + { + source: errors.New("untimely error"), + expected: "untimely error", + }, + } + + for _, c := range cases { + val := Err(c.source) + assert.Equal(t, c.expected, func() string { + if val.String != "" { + return val.String + } + if val.Interface != nil { + return fmt.Sprintf("%s", val.Interface) + } + return "" + }()) + assert.Equal(t, ErrorAttr, val.Key) + } +} + +func TestHandler(t *testing.T) { + val := Handler("handlerName") + assert.Equal(t, HandlerAttr, val.Key) + assert.Equal(t, "handlerName", val.String) +} + +func TestHTTPStatusCode(t *testing.T) { + val := HTTPStatusCode(http.StatusOK) + assert.Equal(t, HTTPStatusAttr, val.Key) + assert.Equal(t, http.StatusOK, int(val.Integer)) +} + +func TestHTTPStatusName(t *testing.T) { + val := HTTPStatusName(http.StatusOK) + assert.Equal(t, HTTPStatusNameAttr, val.Key) + assert.Equal(t, http.StatusText(http.StatusOK), val.String) +} + +func TestBody(t *testing.T) { + var cases = []struct { + input interface{} + result interface{} + }{ + { + input: "", + result: nil, + }, + { + input: nil, + result: nil, + }, + { + input: "ooga booga", + result: "ooga booga", + }, + { + input: `{"success":true}`, + result: map[string]interface{}{"success": true}, + }, + { + input: []byte{}, + result: nil, + }, + { + input: nil, + result: nil, + }, + { + input: []byte("ooga booga"), + result: "ooga booga", + }, + { + input: []byte(`{"success":true}`), + result: map[string]interface{}{"success": true}, + }, + { + input: newReaderMock(func(p []byte) (n int, err error) { + return 0, io.EOF + }), + result: nil, + }, + { + input: newReaderMockData([]byte{}), + result: nil, + }, + { + input: newReaderMockData([]byte("ooga booga")), + result: "ooga booga", + }, + + { + input: newReaderMockData([]byte(`{"success":true}`)), + result: map[string]interface{}{"success": true}, + }, + } + for _, c := range cases { + val := Body(c.input) + assert.Equal(t, BodyAttr, val.Key) + + switch assertion := c.result.(type) { + case string: + assert.Equal(t, assertion, val.String) + case int: + assert.Equal(t, assertion, int(val.Integer)) + default: + assert.Equal(t, c.result, val.Interface) + } + } +} + +type readerMock struct { + mock.Mock +} + +func newReaderMock(cb func(p []byte) (n int, err error)) io.Reader { + r := &readerMock{} + r.On("Read", mock.Anything).Return(cb) + return r +} + +func newReaderMockData(data []byte) io.Reader { + return newReaderMock(bytes.NewReader(data).Read) +} + +func (m *readerMock) Read(p []byte) (n int, err error) { + args := m.Called(p) + out := args.Get(0) + if cb, ok := out.(func(p []byte) (n int, err error)); ok { + return cb(p) + } + return args.Int(0), args.Error(1) +} diff --git a/core/logger/buffer_logger_test.go b/core/logger/buffer_logger_test.go index 7c0b193..a36e5db 100644 --- a/core/logger/buffer_logger_test.go +++ b/core/logger/buffer_logger_test.go @@ -29,8 +29,10 @@ type jSONRecordScanner struct { buf *bufferLogger } -func newJSONBufferedLogger() *jSONRecordScanner { - buf := newBufferLogger() +func newJSONBufferedLogger(buf *bufferLogger) *jSONRecordScanner { + if buf == nil { + buf = newBufferLogger() + } return &jSONRecordScanner{scan: bufio.NewScanner(buf), buf: buf} } diff --git a/core/logger/default_test.go b/core/logger/default_test.go new file mode 100644 index 0000000..acd4f47 --- /dev/null +++ b/core/logger/default_test.go @@ -0,0 +1,89 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + "testing" +) + +type TestDefaultSuite struct { + suite.Suite +} + +func TestDefault(t *testing.T) { + suite.Run(t, new(TestDefaultSuite)) +} + +func (s *TestDefaultSuite) TestNewDefault_OK() { + jsonLog := NewDefault("json", false) + consoleLog := NewDefault("console", true) + + s.Assert().NotNil(jsonLog) + s.Assert().NotNil(consoleLog) +} + +func (s *TestDefaultSuite) TestNewDefault_Panic() { + s.Assert().PanicsWithValue("unknown logger format: rar", func() { + NewDefault("rar", false) + }) +} + +func (s *TestDefaultSuite) TestWith() { + log := newBufferLogger() + log.With(zap.String(HandlerAttr, "Handler")).Info("test") + items, err := newJSONBufferedLogger(log).ScanAll() + + s.Require().NoError(err) + s.Require().Len(items, 1) + s.Assert().Equal("Handler", items[0].Handler) +} + +func (s *TestDefaultSuite) TestWithLazy() { + log := newBufferLogger() + log.WithLazy(zap.String(HandlerAttr, "Handler")).Info("test") + items, err := newJSONBufferedLogger(log).ScanAll() + + s.Require().NoError(err) + s.Require().Len(items, 1) + s.Assert().Equal("Handler", items[0].Handler) +} + +func (s *TestDefaultSuite) TestForHandler() { + log := newBufferLogger() + log.ForHandler("Handler").Info("test") + items, err := newJSONBufferedLogger(log).ScanAll() + + s.Require().NoError(err) + s.Require().Len(items, 1) + s.Assert().Equal("Handler", items[0].Handler) +} + +func (s *TestDefaultSuite) TestForConnection() { + log := newBufferLogger() + log.ForConnection("connection").Info("test") + items, err := newJSONBufferedLogger(log).ScanAll() + + s.Require().NoError(err) + s.Require().Len(items, 1) + s.Assert().Equal("connection", items[0].Connection) +} + +func (s *TestDefaultSuite) TestForAccount() { + log := newBufferLogger() + log.ForAccount("account").Info("test") + items, err := newJSONBufferedLogger(log).ScanAll() + + s.Require().NoError(err) + s.Require().Len(items, 1) + s.Assert().Equal("account", items[0].Account) +} + +func TestAnyZapFields(t *testing.T) { + fields := AnyZapFields([]interface{}{zap.String("k0", "v0"), "ooga", "booga"}) + require.Len(t, fields, 3) + assert.Equal(t, zap.String("k0", "v0"), fields[0]) + assert.Equal(t, zap.String("arg1", "ooga"), fields[1]) + assert.Equal(t, zap.String("arg2", "booga"), fields[2]) +} diff --git a/core/logger/gin_test.go b/core/logger/gin_test.go new file mode 100644 index 0000000..3c04067 --- /dev/null +++ b/core/logger/gin_test.go @@ -0,0 +1,37 @@ +package logger + +import ( + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGinMiddleware(t *testing.T) { + log := newBufferLogger() + rr := httptest.NewRecorder() + r := gin.New() + r.Use(GinMiddleware(log)) + r.GET("/mine", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{}) + }) + r.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/mine", nil)) + + require.Equal(t, http.StatusOK, rr.Code) + items, err := newJSONBufferedLogger(log).ScanAll() + require.NoError(t, err) + require.Len(t, items, 1) + require.NotEmpty(t, items[0].Context) + assert.NotEmpty(t, items[0].Context["startTime"]) + assert.NotEmpty(t, items[0].Context["endTime"]) + assert.True(t, func() bool { + _, ok := items[0].Context["latency"] + return ok + }()) + assert.NotEmpty(t, items[0].Context["remoteAddress"]) + assert.NotEmpty(t, items[0].Context[HTTPMethodAttr]) + assert.NotEmpty(t, items[0].Context["path"]) + assert.NotEmpty(t, items[0].Context["bodySize"]) +} diff --git a/core/logger/mg_transport_client_adapter_test.go b/core/logger/mg_transport_client_adapter_test.go index 262e389..e88aec0 100644 --- a/core/logger/mg_transport_client_adapter_test.go +++ b/core/logger/mg_transport_client_adapter_test.go @@ -11,7 +11,7 @@ import ( func TestMGTransportClientAdapter(t *testing.T) { httpClient := &http.Client{} - log := newJSONBufferedLogger() + log := newJSONBufferedLogger(nil) client := v1.NewWithClient("https://mg.dev", "test_token", httpClient). WithLogger(MGTransportClientAdapter(log.Logger())) client.Debug = true diff --git a/core/logger/pool_test.go b/core/logger/pool_test.go new file mode 100644 index 0000000..5509847 --- /dev/null +++ b/core/logger/pool_test.go @@ -0,0 +1,18 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPool(t *testing.T) { + p := NewPool[*uint8](func() *uint8 { + item := uint8(22) + return &item + }) + + val := p.Get() + assert.Equal(t, uint8(22), *val) + assert.Equal(t, uint8(22), *p.Get()) + p.Put(val) +} diff --git a/core/logger/writer_adapter_test.go b/core/logger/writer_adapter_test.go new file mode 100644 index 0000000..8007e02 --- /dev/null +++ b/core/logger/writer_adapter_test.go @@ -0,0 +1,24 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "testing" +) + +func TestWriterAdapter(t *testing.T) { + log := newBufferLogger() + adapter := WriterAdapter(log, zap.InfoLevel) + + msg := []byte("hello world") + total, err := adapter.Write(msg) + require.NoError(t, err) + require.Equal(t, total, len(msg)) + + items, err := newJSONBufferedLogger(log).ScanAll() + require.NoError(t, err) + require.Len(t, items, 1) + assert.Equal(t, "hello world", items[0].Message) + assert.Equal(t, "INFO", items[0].LevelName) +} diff --git a/core/logger/zabbix_collector_adapter.go b/core/logger/zabbix_collector_adapter.go index e81345e..f33face 100644 --- a/core/logger/zabbix_collector_adapter.go +++ b/core/logger/zabbix_collector_adapter.go @@ -13,10 +13,10 @@ type zabbixCollectorAdapter struct { func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) { baseMsg := "cannot send metrics to Zabbix" switch format { - case "cannot send metrics to Zabbix: %v": - baseMsg = "cannot stop collector" - fallthrough case "cannot stop collector: %s": + baseMsg = "cannot stop Zabbix collector" + fallthrough + case "cannot send metrics to Zabbix: %v": var err interface{} if len(args) > 0 { err = args[0] diff --git a/core/logger/zabbix_collector_adapter_test.go b/core/logger/zabbix_collector_adapter_test.go new file mode 100644 index 0000000..bd3633d --- /dev/null +++ b/core/logger/zabbix_collector_adapter_test.go @@ -0,0 +1,25 @@ +package logger + +import ( + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestZabbixCollectorAdapter(t *testing.T) { + log := newBufferLogger() + adapter := ZabbixCollectorAdapter(log) + adapter.Errorf("highly unexpected error: %s", "unexpected error") + adapter.Errorf("cannot stop collector: %s", "app error") + adapter.Errorf("cannot send metrics to Zabbix: %v", errors.New("send error")) + + items, err := newJSONBufferedLogger(log).ScanAll() + require.NoError(t, err) + require.Len(t, items, 3) + assert.Equal(t, "highly unexpected error: unexpected error", items[0].Message) + assert.Equal(t, "cannot stop Zabbix collector", items[1].Message) + assert.Equal(t, "app error", items[1].Context[ErrorAttr]) + assert.Equal(t, "cannot send metrics to Zabbix", items[2].Message) + assert.Equal(t, "send error", items[2].Context[ErrorAttr]) +} diff --git a/core/util/testutil/buffer_logger.go b/core/util/testutil/buffer_logger.go index 9df5871..5659ede 100644 --- a/core/util/testutil/buffer_logger.go +++ b/core/util/testutil/buffer_logger.go @@ -26,6 +26,16 @@ type BufferedLogger interface { } // BufferLogger is an implementation of the BufferedLogger. +// +// BufferLogger can be used in tests to match specific log messages. It uses JSON by default (hardcoded for now). +// It implements fmt.Stringer and provides an adapter to the underlying buffer, which means it can also return +// Bytes(), can be used like io.Reader and can be cleaned using Reset() method. +// +// Usage: +// +// log := NewBufferedLogger() +// // Some other code that works with logger. +// fmt.Println(log.String()) type BufferLogger struct { logger.Default buf LockableBuffer From 90e9de051a42a33a57d068bdf5ca8f45db1fb21e Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 14 Jun 2024 18:10:48 +0300 Subject: [PATCH 24/26] linter fixes --- cmd/transport-core-tool/main.go | 3 +- core/config/config.go | 4 +- core/config/config_test.go | 3 +- core/db/migration_generator.go | 2 +- core/db/migration_generator_test.go | 3 +- core/db/models/user.go | 11 ++-- core/doc.go | 12 ++++- core/domains.go | 4 +- core/engine.go | 8 +-- core/engine_test.go | 4 +- core/healthcheck/counter.go | 2 +- core/healthcheck/iface.go | 2 +- core/job_manager_test.go | 17 +++--- core/localizer.go | 38 ++++++++----- core/localizer_test.go | 7 ++- core/logger/api_client_adapter.go | 1 + core/logger/api_client_adapter_test.go | 5 +- core/logger/attrs.go | 3 +- core/logger/attrs_test.go | 5 +- core/logger/buffer_logger_test.go | 3 +- core/logger/default.go | 3 +- core/logger/default_test.go | 3 +- core/logger/gin_test.go | 7 +-- core/logger/json_with_context_encoder.go | 6 ++- core/logger/mg_transport_client_adapter.go | 8 +-- .../mg_transport_client_adapter_test.go | 10 ++-- core/logger/nil.go | 28 +++++----- core/logger/pool_test.go | 3 +- core/logger/writer_adapter_test.go | 3 +- core/logger/zabbix_collector_adapter_test.go | 3 +- core/middleware/csrf.go | 46 ++++++++-------- core/middleware/csrf_test.go | 7 ++- core/sentry.go | 4 +- core/sentry_test.go | 4 +- core/stacktrace/error_test.go | 8 +-- core/stacktrace/stack.go | 54 +++++++++---------- core/template_test.go | 5 +- core/util/errorutil/err_collector.go | 47 ++++++++-------- core/util/errorutil/handler_errors.go | 15 ++++-- core/util/httputil/http_client_builder.go | 5 +- .../util/httputil/http_client_builder_test.go | 10 ++-- core/util/testutil/gock.go | 2 +- core/util/testutil/gorm.go | 10 ++-- core/util/testutil/json_record_scanner.go | 3 +- .../util/testutil/json_record_scanner_test.go | 3 +- core/util/testutil/translations_extractor.go | 7 ++- .../testutil/translations_extractor_test.go | 3 +- core/util/utils.go | 3 +- core/util/utils_test.go | 3 +- 49 files changed, 251 insertions(+), 199 deletions(-) diff --git a/cmd/transport-core-tool/main.go b/cmd/transport-core-tool/main.go index 2fdcfd1..7787f72 100644 --- a/cmd/transport-core-tool/main.go +++ b/cmd/transport-core-tool/main.go @@ -32,8 +32,7 @@ func main() { if _, err := parser.Parse(); err != nil { if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint os.Exit(0) - } else { - os.Exit(1) } + os.Exit(1) } } diff --git a/core/config/config.go b/core/config/config.go index ea3ee72..d236003 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -1,7 +1,7 @@ package config import ( - "io/ioutil" + "os" "path/filepath" "time" @@ -132,7 +132,7 @@ func (c *Config) GetConfigData(path string) []byte { panic(err) } - source, err := ioutil.ReadFile(path) + source, err := os.ReadFile(path) if err != nil { panic(err) } diff --git a/core/config/config_test.go b/core/config/config_test.go index 2fa569c..8498ae0 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "io/ioutil" "os" "path" "testing" @@ -54,7 +53,7 @@ config_aws: bucket: bucket folder_name: folder content_type: image/jpeg`) - err := ioutil.WriteFile(testConfigFile, c.data, os.ModePerm) + err := os.WriteFile(testConfigFile, c.data, os.ModePerm) require.Nil(c.T(), err) c.config = NewConfig(testConfigFile) diff --git a/core/db/migration_generator.go b/core/db/migration_generator.go index b078ad0..79b9967 100644 --- a/core/db/migration_generator.go +++ b/core/db/migration_generator.go @@ -51,7 +51,7 @@ func (x *NewMigrationCommand) FileExists(filename string) bool { } // Execute migration generator command. -func (x *NewMigrationCommand) Execute(args []string) error { +func (x *NewMigrationCommand) Execute(_ []string) error { tpl, err := template.New("migration").Parse(migrationTemplate) if err != nil { return fmt.Errorf("fatal: cannot parse base migration template: %w", err) diff --git a/core/db/migration_generator_test.go b/core/db/migration_generator_test.go index 1d86ac5..03723ac 100644 --- a/core/db/migration_generator_test.go +++ b/core/db/migration_generator_test.go @@ -2,7 +2,6 @@ package db import ( "fmt" - "io/ioutil" "log" "math/rand" "os" @@ -36,7 +35,7 @@ func (s *MigrationGeneratorSuite) Test_FileExists() { func (s *MigrationGeneratorSuite) Test_Execute() { found := false assert.NoError(s.T(), s.command.Execute([]string{})) - files, err := ioutil.ReadDir(s.command.Directory) + files, err := os.ReadDir(s.command.Directory) if err != nil { log.Fatal(err) } diff --git a/core/db/models/user.go b/core/db/models/user.go index 5a7b4b0..be82382 100644 --- a/core/db/models/user.go +++ b/core/db/models/user.go @@ -14,11 +14,14 @@ type User struct { // TableName will return table name for User // It will not work if User is not embedded, but mapped as another type -// type MyUser User // will not work +// +// type MyUser User // will not work +// // but -// type MyUser struct { // will work -// User -// } +// +// type MyUser struct { // will work +// User +// } func (User) TableName() string { return "mg_user" } diff --git a/core/doc.go b/core/doc.go index b1916a4..4bbe25e 100644 --- a/core/doc.go +++ b/core/doc.go @@ -2,6 +2,7 @@ Package core provides different functions like error-reporting, logging, localization, etc. to make it easier to create transports. Usage: + package main import ( @@ -36,14 +37,19 @@ Usage: } } -Resource embedding +# Resource embedding packr can be used to provide resource embedding, see: + https://github.com/gobuffalo/packr/tree/master/v2 + In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library. You can find instruction here: + https://github.com/gobuffalo/packr/tree/master/v2#library-installation + Example of usage: + package main import ( @@ -104,10 +110,12 @@ Example of usage: } } -Migration generator +# Migration generator This library contains helper tool for transports. You can install it via go: + $ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool + Currently, it only can generate new migrations for your transport. Copyright (c) 2019 RetailDriver LLC. Usage of this source code is governed by a MIT license. diff --git a/core/domains.go b/core/domains.go index 2b14d63..46e1529 100644 --- a/core/domains.go +++ b/core/domains.go @@ -2,7 +2,7 @@ package core import ( "encoding/json" - "io/ioutil" + "io" "net/http" ) @@ -40,7 +40,7 @@ func getDomainsByStore(store string) []Domain { return nil } - respBody, readErr := ioutil.ReadAll(resp.Body) + respBody, readErr := io.ReadAll(resp.Body) if readErr != nil { return nil diff --git a/core/engine.go b/core/engine.go index 6887ad4..72dfd48 100644 --- a/core/engine.go +++ b/core/engine.go @@ -7,6 +7,7 @@ import ( "io/fs" "net/http" "sync" + "time" "github.com/blacked/go-zabbix" "github.com/getsentry/sentry-go" @@ -26,12 +27,14 @@ import ( "github.com/retailcrm/mg-transport-core/v2/core/logger" ) +const DefaultHTTPClientTimeout time.Duration = 30 + var boolTrue = true // DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client // if HTTP client config is not present in the configuration. var DefaultHTTPClientConfig = &config.HTTPClientConfig{ - Timeout: 30, + Timeout: DefaultHTTPClientTimeout, SSLVerification: &boolTrue, } @@ -279,10 +282,9 @@ func (e *Engine) BuildHTTPClient(certs *x509.CertPool, replaceDefault ...bool) * if err != nil { panic(err) - } else { - e.httpClient = client } + e.httpClient = client return e } diff --git a/core/engine_test.go b/core/engine_test.go index 2d89d76..ab9bbd2 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -6,7 +6,7 @@ import ( "database/sql" "fmt" "html/template" - "io/ioutil" + "io" "net/http" "net/url" "os" @@ -365,7 +365,7 @@ func (e *EngineTest) Test_GetCSRFToken() { URL: &url.URL{ RawQuery: "", }, - Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + Body: io.NopCloser(bytes.NewReader([]byte{})), Header: http.Header{"X-CSRF-Token": []string{"token"}}, }} c.Set("csrf_token", "token") diff --git a/core/healthcheck/counter.go b/core/healthcheck/counter.go index e8e1ede..86ee3a7 100644 --- a/core/healthcheck/counter.go +++ b/core/healthcheck/counter.go @@ -10,7 +10,7 @@ import ( const DefaultResetPeriod = time.Minute * 15 // AtomicCounter is a default Counter implementation. -// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and +// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and. type AtomicCounter struct { name atomic.String msg atomic.String diff --git a/core/healthcheck/iface.go b/core/healthcheck/iface.go index cf88d2d..c9c2486 100644 --- a/core/healthcheck/iface.go +++ b/core/healthcheck/iface.go @@ -1,7 +1,7 @@ package healthcheck var ( - // compile-time checks to ensure that implementations are compatible with the interface + // compile-time checks to ensure that implementations are compatible with the interface. _ = Storage(&SyncMapStorage{}) _ = Counter(&AtomicCounter{}) _ = Processor(CounterProcessor{}) diff --git a/core/job_manager_test.go b/core/job_manager_test.go index 40ed2a6..ac3ce2a 100644 --- a/core/job_manager_test.go +++ b/core/job_manager_test.go @@ -44,7 +44,7 @@ type callbackLogger struct { fn callbackLoggerFunc } -func (n *callbackLogger) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { +func (n *callbackLogger) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry { return &zapcore.CheckedEntry{} } @@ -75,7 +75,7 @@ func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger { cl.fields = fields return cl } - cl.fields = append(existing, fields...) + cl.fields = append(existing, fields...) // nolint:gocritic return cl } @@ -91,19 +91,19 @@ func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger { return n.cloneWithFields(args) } -func (n *callbackLogger) WithGroup(name string) logger.Logger { +func (n *callbackLogger) WithGroup(_ string) logger.Logger { return n } -func (n *callbackLogger) ForHandler(handler any) logger.Logger { +func (n *callbackLogger) ForHandler(_ any) logger.Logger { return n } -func (n *callbackLogger) ForConnection(conn any) logger.Logger { +func (n *callbackLogger) ForConnection(_ any) logger.Logger { return n } -func (n *callbackLogger) ForAccount(acc any) logger.Logger { +func (n *callbackLogger) ForAccount(_ any) logger.Logger { return n } @@ -288,11 +288,11 @@ func (t *JobTest) oncePanicJob() { } func (t *JobTest) regularJob() { - rand.Seed(time.Now().UnixNano()) + r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec t.job = &Job{ Command: func(log logger.Logger) error { t.executedChan <- true - t.randomNumber <- rand.Int() // nolint:gosec + t.randomNumber <- r.Int() // nolint:gosec return nil }, ErrorHandler: t.testErrorHandler(), @@ -303,7 +303,6 @@ func (t *JobTest) regularJob() { } func (t *JobTest) regularSyncJob() { - rand.Seed(time.Now().UnixNano()) t.job = &Job{ Command: func(log logger.Logger) error { t.syncBool = true diff --git a/core/localizer.go b/core/localizer.go index 5dbfcf7..355d86d 100644 --- a/core/localizer.go +++ b/core/localizer.go @@ -3,7 +3,7 @@ package core import ( "html/template" "io/fs" - "io/ioutil" + "os" "path" "sync" @@ -90,7 +90,8 @@ type CloneableLocalizer interface { // NewLocalizer returns localizer instance with specified parameters. // Usage: -// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations") +// +// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations") func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface { localizer := &Localizer{ i18nStorage: &sync.Map{}, @@ -106,7 +107,9 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat // NewLocalizerFS returns localizer instance with specified parameters. // Usage: -// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS) +// +// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS) +// // TODO This code should be covered with tests. func NewLocalizerFS( locale language.Tag, matcher language.Matcher, translationsFS fs.FS, @@ -161,9 +164,10 @@ func (l *Localizer) Clone() CloneableLocalizer { // Because of that all Localizer instances from this middleware will share *same* mutex. This mutex is used to wrap // i18n.Bundle methods (those aren't goroutine-safe to use). // Usage: -// engine := gin.New() -// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations") -// engine.Use(localizer.LocalizationMiddleware()) +// +// engine := gin.New() +// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations") +// engine.Use(localizer.LocalizationMiddleware()) func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc { return func(c *gin.Context) { clone := l.Clone().(LocaleControls) @@ -174,15 +178,21 @@ func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc { // LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans // Usage in code: -// engine := gin.New() -// engine.FuncMap = localizer.LocalizationFuncMap() +// +// engine := gin.New() +// engine.FuncMap = localizer.LocalizationFuncMap() +// // or (with multitemplate) -// renderer := multitemplate.NewRenderer() -// funcMap := localizer.LocalizationFuncMap() -// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html") +// +// renderer := multitemplate.NewRenderer() +// funcMap := localizer.LocalizationFuncMap() +// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html") +// // funcMap must be passed for every .AddFromFilesFuncs call // Usage in templates: -//

{{"need_login_msg" | trans}} +// +//

{{"need_login_msg" | trans}} +// // You can borrow FuncMap from this method and add your functions to it. func (l *Localizer) LocalizationFuncMap() template.FuncMap { return template.FuncMap{ @@ -196,7 +206,7 @@ func (l *Localizer) LocalizationFuncMap() template.FuncMap { parts = append(parts, "") } - partsMap := make(map[string]interface{}, len(parts)/2) + partsMap := make(map[string]interface{}, len(parts)/2) // nolint:gomnd for i := 0; i < len(parts)-1; i += 2 { partsMap[parts[i]] = parts[i+1] @@ -239,7 +249,7 @@ func (l *Localizer) loadTranslationsToBundle(i18nBundle *i18n.Bundle) { // LoadTranslations will load all translation files from translations directory. func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error { - files, err := ioutil.ReadDir(l.TranslationsPath) + files, err := os.ReadDir(l.TranslationsPath) if err != nil { return err } diff --git a/core/localizer_test.go b/core/localizer_test.go index 57c5cc8..35050fa 100644 --- a/core/localizer_test.go +++ b/core/localizer_test.go @@ -1,7 +1,6 @@ package core import ( - "io/ioutil" "math/rand" "net/http" "net/http/httptest" @@ -40,7 +39,7 @@ func createTestLangFiles(t *testing.T) { } if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) { - err = ioutil.WriteFile(fileName, data, os.ModePerm) + err = os.WriteFile(fileName, data, os.ModePerm) require.Nil(t, err) } } @@ -103,7 +102,7 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() { func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { var wg sync.WaitGroup - rand.Seed(time.Now().UnixNano()) + r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec l.localizer.Preload(DefaultLanguages) langMsgMap := map[language.Tag]string{ language.English: "Test message", @@ -122,7 +121,7 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { wg.Add(1) go func(m map[language.Tag]string, wg *sync.WaitGroup) { var tag language.Tag - switch rand.Intn(3-1) + 1 { // nolint:gosec + switch r.Intn(3-1) + 1 { // nolint:gosec case 1: tag = language.English case 2: diff --git a/core/logger/api_client_adapter.go b/core/logger/api_client_adapter.go index 1cb7d4c..8d88117 100644 --- a/core/logger/api_client_adapter.go +++ b/core/logger/api_client_adapter.go @@ -2,6 +2,7 @@ package logger import ( "fmt" + "go.uber.org/zap" retailcrm "github.com/retailcrm/api-client-go/v2" diff --git a/core/logger/api_client_adapter_test.go b/core/logger/api_client_adapter_test.go index eb2e6ca..4a4d6bc 100644 --- a/core/logger/api_client_adapter_test.go +++ b/core/logger/api_client_adapter_test.go @@ -1,12 +1,13 @@ package logger import ( + "net/http" + "testing" + "github.com/h2non/gock" retailcrm "github.com/retailcrm/api-client-go/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/http" - "testing" ) func TestAPIClientAdapter(t *testing.T) { diff --git a/core/logger/attrs.go b/core/logger/attrs.go index 73de78d..06ec6e0 100644 --- a/core/logger/attrs.go +++ b/core/logger/attrs.go @@ -2,10 +2,11 @@ package logger import ( "fmt" - json "github.com/goccy/go-json" "io" "net/http" + json "github.com/goccy/go-json" + "go.uber.org/zap" ) diff --git a/core/logger/attrs_test.go b/core/logger/attrs_test.go index c3fdcf5..2bb519d 100644 --- a/core/logger/attrs_test.go +++ b/core/logger/attrs_test.go @@ -4,11 +4,12 @@ import ( "bytes" "errors" "fmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "io" "net/http" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestErr(t *testing.T) { diff --git a/core/logger/buffer_logger_test.go b/core/logger/buffer_logger_test.go index a36e5db..0c6115e 100644 --- a/core/logger/buffer_logger_test.go +++ b/core/logger/buffer_logger_test.go @@ -4,11 +4,12 @@ import ( "bufio" "bytes" "encoding/json" - "github.com/guregu/null/v5" "io" "os" "sync" + "github.com/guregu/null/v5" + "go.uber.org/zap" "go.uber.org/zap/zapcore" ) diff --git a/core/logger/default.go b/core/logger/default.go index 8ec6fc7..017b4b5 100644 --- a/core/logger/default.go +++ b/core/logger/default.go @@ -27,7 +27,8 @@ type Logger interface { Warn(msg string, fields ...zap.Field) // Error logs an error-level message with the given fields. Error(msg string, fields ...zap.Field) - // DPanic logs a debug-panic-level message with the given fields and panics if the logger's panic level is set to a non-zero value. + // DPanic logs a debug-panic-level message with the given fields and panics + // if the logger's panic level is set to a non-zero value. DPanic(msg string, fields ...zap.Field) // Panic logs a panic-level message with the given fields and panics immediately. Panic(msg string, fields ...zap.Field) diff --git a/core/logger/default_test.go b/core/logger/default_test.go index acd4f47..7301568 100644 --- a/core/logger/default_test.go +++ b/core/logger/default_test.go @@ -1,11 +1,12 @@ package logger import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.uber.org/zap" - "testing" ) type TestDefaultSuite struct { diff --git a/core/logger/gin_test.go b/core/logger/gin_test.go index 3c04067..3cf6cf7 100644 --- a/core/logger/gin_test.go +++ b/core/logger/gin_test.go @@ -1,12 +1,13 @@ package logger import ( - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGinMiddleware(t *testing.T) { diff --git a/core/logger/json_with_context_encoder.go b/core/logger/json_with_context_encoder.go index cb377e7..cc3a8a1 100644 --- a/core/logger/json_with_context_encoder.go +++ b/core/logger/json_with_context_encoder.go @@ -18,19 +18,21 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +// nolint package logger import ( "encoding/base64" "encoding/json" "fmt" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "io" "math" "time" "unicode/utf8" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/buffer" ) diff --git a/core/logger/mg_transport_client_adapter.go b/core/logger/mg_transport_client_adapter.go index 345b5b3..0b8569b 100644 --- a/core/logger/mg_transport_client_adapter.go +++ b/core/logger/mg_transport_client_adapter.go @@ -2,8 +2,10 @@ package logger import ( "fmt" - v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "go.uber.org/zap" + + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" ) const ( @@ -36,10 +38,10 @@ func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) { if len(args) > 1 { uri = fmt.Sprint(args[1]) } - if len(args) > 2 { + if len(args) > 2 { // nolint:gomnd token = fmt.Sprint(args[2]) } - if len(args) > 3 { + if len(args) > 3 { // nolint:gomnd body = args[3] } m.log.Debug("MG TRANSPORT API Request", diff --git a/core/logger/mg_transport_client_adapter_test.go b/core/logger/mg_transport_client_adapter_test.go index e88aec0..f3ece35 100644 --- a/core/logger/mg_transport_client_adapter_test.go +++ b/core/logger/mg_transport_client_adapter_test.go @@ -1,12 +1,14 @@ package logger import ( - "github.com/h2non/gock" - v1 "github.com/retailcrm/mg-transport-api-client-go/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "net/http" "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" ) func TestMGTransportClientAdapter(t *testing.T) { diff --git a/core/logger/nil.go b/core/logger/nil.go index f6d0f95..bd348ed 100644 --- a/core/logger/nil.go +++ b/core/logger/nil.go @@ -13,11 +13,11 @@ func NewNil() Logger { return &Nil{} } -func (l *Nil) With(fields ...zap.Field) Logger { +func (l *Nil) With(_ ...zap.Field) Logger { return l } -func (l *Nil) WithLazy(fields ...zap.Field) Logger { +func (l *Nil) WithLazy(_ ...zap.Field) Logger { return l } @@ -25,35 +25,35 @@ func (l *Nil) Level() zapcore.Level { return zapcore.DebugLevel } -func (l *Nil) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { +func (l *Nil) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry { return &zapcore.CheckedEntry{} } -func (l *Nil) Log(lvl zapcore.Level, msg string, fields ...zap.Field) {} +func (l *Nil) Log(_ zapcore.Level, _ string, _ ...zap.Field) {} -func (l *Nil) Debug(msg string, fields ...zap.Field) {} +func (l *Nil) Debug(_ string, _ ...zap.Field) {} -func (l *Nil) Info(msg string, fields ...zap.Field) {} +func (l *Nil) Info(_ string, _ ...zap.Field) {} -func (l *Nil) Warn(msg string, fields ...zap.Field) {} +func (l *Nil) Warn(_ string, _ ...zap.Field) {} -func (l *Nil) Error(msg string, fields ...zap.Field) {} +func (l *Nil) Error(_ string, _ ...zap.Field) {} -func (l *Nil) DPanic(msg string, fields ...zap.Field) {} +func (l *Nil) DPanic(_ string, _ ...zap.Field) {} -func (l *Nil) Panic(msg string, fields ...zap.Field) {} +func (l *Nil) Panic(_ string, _ ...zap.Field) {} -func (l *Nil) Fatal(msg string, fields ...zap.Field) {} +func (l *Nil) Fatal(_ string, _ ...zap.Field) {} -func (l *Nil) ForHandler(handler any) Logger { +func (l *Nil) ForHandler(_ any) Logger { return l } -func (l *Nil) ForConnection(conn any) Logger { +func (l *Nil) ForConnection(_ any) Logger { return l } -func (l *Nil) ForAccount(acc any) Logger { +func (l *Nil) ForAccount(_ any) Logger { return l } diff --git a/core/logger/pool_test.go b/core/logger/pool_test.go index 5509847..a73da78 100644 --- a/core/logger/pool_test.go +++ b/core/logger/pool_test.go @@ -1,8 +1,9 @@ package logger import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestPool(t *testing.T) { diff --git a/core/logger/writer_adapter_test.go b/core/logger/writer_adapter_test.go index 8007e02..68e34d2 100644 --- a/core/logger/writer_adapter_test.go +++ b/core/logger/writer_adapter_test.go @@ -1,10 +1,11 @@ package logger import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" - "testing" ) func TestWriterAdapter(t *testing.T) { diff --git a/core/logger/zabbix_collector_adapter_test.go b/core/logger/zabbix_collector_adapter_test.go index bd3633d..08f9dfb 100644 --- a/core/logger/zabbix_collector_adapter_test.go +++ b/core/logger/zabbix_collector_adapter_test.go @@ -2,9 +2,10 @@ package logger import ( "errors" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) func TestZabbixCollectorAdapter(t *testing.T) { diff --git a/core/middleware/csrf.go b/core/middleware/csrf.go index 3fe4716..f8b59b0 100644 --- a/core/middleware/csrf.go +++ b/core/middleware/csrf.go @@ -6,7 +6,6 @@ import ( "crypto/sha1" "encoding/base64" "io" - "io/ioutil" "math/rand" "time" @@ -57,10 +56,10 @@ var DefaultCSRFTokenGetter = func(c *gin.Context) string { } else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 { return t } else if c.Request.Body != nil { - data, _ := ioutil.ReadAll(c.Request.Body) - c.Request.Body = ioutil.NopCloser(bytes.NewReader(data)) + data, _ := io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewReader(data)) t := r.FormValue("csrf_token") - c.Request.Body = ioutil.NopCloser(bytes.NewReader(data)) + c.Request.Body = io.NopCloser(bytes.NewReader(data)) if len(t) > 0 { return t @@ -91,17 +90,20 @@ type CSRF struct { // csrfTokenGetter will be used to obtain token. // // Usage (with random salt): -// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) { -// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"}) -// }, core.DefaultCSRFTokenGetter) +// +// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) { +// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"}) +// }, core.DefaultCSRFTokenGetter) // // Note for csrfTokenGetter: if you want to read token from request body (for example, from form field) // - don't forget to restore Body data! // // Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that: -// if t := r.FormValue("csrf_token"); len(t) > 0 { -// return t -// } +// +// if t := r.FormValue("csrf_token"); len(t) > 0 { +// return t +// } +// // will close body - and all next middlewares won't be able to read body at all! // // Use DefaultCSRFTokenGetter as example to implement your own token getter. @@ -185,11 +187,11 @@ func (x *CSRF) generateSalt() string { // pseudoRandomString generates pseudo-random string with specified length. func (x *CSRF) pseudoRandomString(length int) string { - rand.Seed(time.Now().UnixNano()) + r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec data := make([]byte, length) for i := 0; i < length; i++ { // it is supposed to use pseudo-random data. - data[i] = byte(65 + rand.Intn(90-65)) // nolint:gosec,gomnd + data[i] = byte(65 + r.Intn(90-65)) // nolint:gosec,gomnd } return string(data) @@ -209,15 +211,16 @@ func (x *CSRF) CSRFFromContext(c *gin.Context) string { // GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token // Usage: -// engine := gin.New() -// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer) -// engine.Use(csrf.GenerateCSRFMiddleware()) +// +// engine := gin.New() +// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer) +// engine.Use(csrf.GenerateCSRFMiddleware()) func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc { return func(c *gin.Context) { session, _ := x.store.Get(c.Request, x.sessionName) - if i, ok := session.Values["csrf_token"]; ok { - if i, ok := i.(string); !ok || i == "" { + if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif + if i, ok := i.(string); !ok || i == "" { // nolint:nestif if x.fillToken(session, c) != nil { x.abortFunc(c, CSRFErrorCannotStoreTokenInSession) c.Abort() @@ -243,8 +246,9 @@ func (x *CSRF) fillToken(s *sessions.Session, c *gin.Context) error { // VerifyCSRFMiddleware verifies CSRF token // Usage: -// engine := gin.New() -// engine.Use(csrf.VerifyCSRFMiddleware()) +// +// engine := gin.New() +// engine.Use(csrf.VerifyCSRFMiddleware()) func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc { return func(c *gin.Context) { if x.strInSlice(ignoredMethods, c.Request.Method) { @@ -254,9 +258,9 @@ func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc { var token string session, _ := x.store.Get(c.Request, x.sessionName) - if i, ok := session.Values["csrf_token"]; ok { + if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif var v string - if v, ok = i.(string); !ok || v == "" { + if v, ok = i.(string); !ok || v == "" { // nolint:nestif if !ok { x.abortFunc(c, CSRFErrorIncorrectTokenType) } else if v == "" { diff --git a/core/middleware/csrf_test.go b/core/middleware/csrf_test.go index 5f7b831..6d501a7 100644 --- a/core/middleware/csrf_test.go +++ b/core/middleware/csrf_test.go @@ -3,7 +3,6 @@ package middleware import ( "bytes" "io" - "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -32,7 +31,7 @@ func TestCSRF_DefaultCSRFTokenGetter_Empty(t *testing.T) { URL: &url.URL{ RawQuery: "", }, - Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + Body: io.NopCloser(bytes.NewReader([]byte(""))), }} assert.Empty(t, DefaultCSRFTokenGetter(c)) @@ -85,14 +84,14 @@ func TestCSRF_DefaultCSRFTokenGetter_Form(t *testing.T) { RawQuery: "", }, Header: headers, - Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + Body: io.NopCloser(bytes.NewReader([]byte(""))), }} c.Request.PostForm = url.Values{"csrf_token": {"token"}} assert.NotEmpty(t, DefaultCSRFTokenGetter(c)) assert.Equal(t, "token", DefaultCSRFTokenGetter(c)) - _, err := ioutil.ReadAll(c.Request.Body) + _, err := io.ReadAll(c.Request.Body) assert.NoError(t, err) } diff --git a/core/sentry.go b/core/sentry.go index 2e3330c..9f6763f 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -23,6 +23,8 @@ import ( "github.com/gin-gonic/gin" ) +const recoveryMiddlewareSkipFrames = 3 + // ErrorHandlerFunc will handle errors. type ErrorHandlerFunc func(recovery interface{}, c *gin.Context) @@ -249,7 +251,7 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint } if l != nil { // TODO: Check if we can output stacktraces with prefix data like before if we really need it. - stack := stacktrace.FormattedStack(3, "trace: ") + stack := stacktrace.FormattedStack(recoveryMiddlewareSkipFrames, "trace: ") formattedErr := logger.Err(err) httpRequest, _ := httputil.DumpRequest(c.Request, false) headers := strings.Split(string(httpRequest), "\r\n") diff --git a/core/sentry_test.go b/core/sentry_test.go index b919d86..2857725 100644 --- a/core/sentry_test.go +++ b/core/sentry_test.go @@ -31,12 +31,12 @@ type sentryMockTransport struct { sending sync.RWMutex } -func (s *sentryMockTransport) Flush(timeout time.Duration) bool { +func (s *sentryMockTransport) Flush(_ time.Duration) bool { // noop return true } -func (s *sentryMockTransport) Configure(options sentry.ClientOptions) { +func (s *sentryMockTransport) Configure(_ sentry.ClientOptions) { // noop } diff --git a/core/stacktrace/error_test.go b/core/stacktrace/error_test.go index fdc205e..0ff8a51 100644 --- a/core/stacktrace/error_test.go +++ b/core/stacktrace/error_test.go @@ -18,7 +18,7 @@ func TestError(t *testing.T) { func (t *ErrorTest) TestAppendToError() { err := errors.New("test error") - _, ok := err.(StackTraced) + _, ok := err.(StackTraced) // nolint:errorlint t.Assert().False(ok) @@ -28,16 +28,16 @@ func (t *ErrorTest) TestAppendToError() { t.Assert().Nil(AppendToError(nil)) t.Assert().Implements((*StackTraced)(nil), withTrace) t.Assert().Implements((*StackTraced)(nil), twiceTrace) - t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace()) + t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace()) // nolint:errorlint } func (t *ErrorTest) TestCauseUnwrap() { err := errors.New("test error") wrapped := AppendToError(err) - t.Assert().Equal(err, wrapped.(*withStack).Cause()) + t.Assert().Equal(err, wrapped.(*withStack).Cause()) // nolint:errorlint t.Assert().Equal(err, errors.Unwrap(wrapped)) - t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped)) + t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped)) // nolint:errorlint } func (t *ErrorTest) TestFormat() { diff --git a/core/stacktrace/stack.go b/core/stacktrace/stack.go index b6262a2..d66d6e9 100644 --- a/core/stacktrace/stack.go +++ b/core/stacktrace/stack.go @@ -5,7 +5,7 @@ import ( "bytes" "fmt" "io" - "io/ioutil" + "os" "path" "runtime" "strconv" @@ -63,34 +63,34 @@ func (f Frame) name() string { // Format formats the frame according to the fmt.Formatter interface. // -// %s source file -// %d source line -// %n function name -// %v equivalent to %s:%d +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) -// %+v equivalent to %+s:%d +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { switch verb { case 's': switch { case s.Flag('+'): - io.WriteString(s, f.name()) - io.WriteString(s, "\n\t") - io.WriteString(s, f.file()) + _, _ = io.WriteString(s, f.name()) + _, _ = io.WriteString(s, "\n\t") + _, _ = io.WriteString(s, f.file()) default: - io.WriteString(s, path.Base(f.file())) + _, _ = io.WriteString(s, path.Base(f.file())) } case 'd': - io.WriteString(s, strconv.Itoa(f.line())) + _, _ = io.WriteString(s, strconv.Itoa(f.line())) case 'n': - io.WriteString(s, funcname(f.name())) + _, _ = io.WriteString(s, funcname(f.name())) case 'v': f.Format(s, 's') - io.WriteString(s, ":") + _, _ = io.WriteString(s, ":") f.Format(s, 'd') } } @@ -110,23 +110,23 @@ type StackTrace []Frame // Format formats the stack of Frames according to the fmt.Formatter interface. // -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+v Prints filename, function, and line number for each Frame in the stack. +// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': switch { case s.Flag('+'): for _, f := range st { - io.WriteString(s, "\n") + _, _ = io.WriteString(s, "\n") f.Format(s, verb) } case s.Flag('#'): - fmt.Fprintf(s, "%#v", []Frame(st)) + _, _ = fmt.Fprintf(s, "%#v", []Frame(st)) default: st.formatSlice(s, verb) } @@ -138,14 +138,14 @@ func (st StackTrace) Format(s fmt.State, verb rune) { // formatSlice will format this StackTrace into the given buffer as a slice of // Frame, only valid when called with '%s' or '%v'. func (st StackTrace) formatSlice(s fmt.State, verb rune) { - io.WriteString(s, "[") + _, _ = io.WriteString(s, "[") for i, f := range st { if i > 0 { - io.WriteString(s, " ") + _, _ = io.WriteString(s, " ") } f.Format(s, verb) } - io.WriteString(s, "]") + _, _ = io.WriteString(s, "]") } // stack represents a stack of program counters. @@ -159,7 +159,7 @@ func (s *stack) Format(st fmt.State, verb rune) { if verb == 'v' && st.Flag('+') { for _, pc := range *s { f := Frame(pc) - fmt.Fprintf(st, "\n%+v", f) + _, _ = fmt.Fprintf(st, "\n%+v", f) } } } @@ -185,16 +185,16 @@ func FormattedStack(skip int, prefix string) []byte { break } // Print this much at least. If we can't find the source, it won't show. - fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc) + _, _ = fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc) if file != lastFile { - data, err := ioutil.ReadFile(file) + data, err := os.ReadFile(file) if err != nil { continue } lines = bytes.Split(data, []byte{'\n'}) lastFile = file } - fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line)) + _, _ = fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line)) } return buf.Bytes() } diff --git a/core/template_test.go b/core/template_test.go index 21ab439..48a618a 100644 --- a/core/template_test.go +++ b/core/template_test.go @@ -3,7 +3,6 @@ package core import ( "fmt" "html/template" - "io/ioutil" "os" "path" "testing" @@ -37,8 +36,8 @@ func (t *TemplateTest) initTestData() { require.Nil(t.T(), err) data1 := []byte(`data {{template "body" .}}`) data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`) - err1 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm) - err2 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm) + err1 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm) + err2 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm) require.Nil(t.T(), err1) require.Nil(t.T(), err2) } diff --git a/core/util/errorutil/err_collector.go b/core/util/errorutil/err_collector.go index bc6b669..d68c90e 100644 --- a/core/util/errorutil/err_collector.go +++ b/core/util/errorutil/err_collector.go @@ -18,36 +18,41 @@ import ( // because AsError() returns nil if there are no errors in the list. // // Example: -// err := errorutil.NewCollector(). -// Do(errors.New("error 1")). -// Do(errors.New("error 2"), errors.New("error 3")) -// // Will print error message. -// fmt.Println(err) +// +// err := errorutil.NewCollector(). +// Do(errors.New("error 1")). +// Do(errors.New("error 2"), errors.New("error 3")) +// // Will print error message. +// fmt.Println(err) // // This code will produce something like this: -// #1 err at /home/user/main.go:62: error 1 -// #2 err at /home/user/main.go:63: error 2 -// #3 err at /home/user/main.go:64: error 3 +// +// #1 err at /home/user/main.go:62: error 1 +// #2 err at /home/user/main.go:63: error 2 +// #3 err at /home/user/main.go:64: error 3 // // You can also iterate over the error to use their data instead of using predefined message: -// err := errorutil.NewCollector(). -// Do(errors.New("error 1")). -// Do(errors.New("error 2"), errors.New("error 3")) // -// for err := range c.Iterate() { -// fmt.Printf("Error at %s:%d: %v\n", err.File, err.Line, err) -// } +// err := errorutil.NewCollector(). +// Do(errors.New("error 1")). +// Do(errors.New("error 2"), errors.New("error 3")) +// +// for err := range c.Iterate() { +// fmt.Printf("Error at %s:%d: %v\n", err.File, err.Line, err) +// } // // This code will produce output that looks like this: -// Error at /home/user/main.go:164: error 0 -// Error at /home/user/main.go:164: error 1 -// Error at /home/user/main.go:164: error 2 +// +// Error at /home/user/main.go:164: error 0 +// Error at /home/user/main.go:164: error 1 +// Error at /home/user/main.go:164: error 2 // // Example with GORM migration (Collector is returned as an error here). -// return errorutil.NewCollector().Do( -// db.CreateTable(models.Account{}, models.Connection{}).Error, -// db.Table("account").AddUniqueIndex("account_key", "channel").Error, -// ).AsError() +// +// return errorutil.NewCollector().Do( +// db.CreateTable(models.Account{}, models.Connection{}).Error, +// db.Table("account").AddUniqueIndex("account_key", "channel").Error, +// ).AsError() type Collector struct { errors *errList } diff --git a/core/util/errorutil/handler_errors.go b/core/util/errorutil/handler_errors.go index a04a42b..d0bd50f 100644 --- a/core/util/errorutil/handler_errors.go +++ b/core/util/errorutil/handler_errors.go @@ -14,7 +14,8 @@ type ListResponse struct { // GetErrorResponse returns ErrorResponse with specified status code // Usage (with gin): -// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money")) +// +// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money")) func GetErrorResponse(statusCode int, err string) (int, interface{}) { return statusCode, Response{ Error: err, @@ -23,28 +24,32 @@ func GetErrorResponse(statusCode int, err string) (int, interface{}) { // BadRequest returns ErrorResponse with code 400 // Usage (with gin): -// context.JSON(BadRequest("invalid data")) +// +// context.JSON(BadRequest("invalid data")) func BadRequest(err string) (int, interface{}) { return GetErrorResponse(http.StatusBadRequest, err) } // Unauthorized returns ErrorResponse with code 401 // Usage (with gin): -// context.JSON(Unauthorized("invalid credentials")) +// +// context.JSON(Unauthorized("invalid credentials")) func Unauthorized(err string) (int, interface{}) { return GetErrorResponse(http.StatusUnauthorized, err) } // Forbidden returns ErrorResponse with code 403 // Usage (with gin): -// context.JSON(Forbidden("forbidden")) +// +// context.JSON(Forbidden("forbidden")) func Forbidden(err string) (int, interface{}) { return GetErrorResponse(http.StatusForbidden, err) } // InternalServerError returns ErrorResponse with code 500 // Usage (with gin): -// context.JSON(BadRequest("invalid data")) +// +// context.JSON(BadRequest("invalid data")) func InternalServerError(err string) (int, interface{}) { return GetErrorResponse(http.StatusInternalServerError, err) } diff --git a/core/util/httputil/http_client_builder.go b/core/util/httputil/http_client_builder.go index a27dd1c..e750077 100644 --- a/core/util/httputil/http_client_builder.go +++ b/core/util/httputil/http_client_builder.go @@ -5,12 +5,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "go.uber.org/zap" "net" "net/http" "net/url" "time" + "go.uber.org/zap" + "github.com/pkg/errors" "github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/logger" @@ -229,7 +230,7 @@ func (b *HTTPClientBuilder) buildMocks() error { return errors.New("dialer must be built first") } - if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { + if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { // nolint:nestif b.log("Mock address has been set", zap.String("address", net.JoinHostPort(b.mockHost, b.mockPort))) b.log("Mocked domains: ") diff --git a/core/util/httputil/http_client_builder_test.go b/core/util/httputil/http_client_builder_test.go index 862783a..9e35422 100644 --- a/core/util/httputil/http_client_builder_test.go +++ b/core/util/httputil/http_client_builder_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" "net" "net/http" @@ -209,6 +208,7 @@ x5porosgI2RgOTTwmiYOcYQTS2650jYydHhK16Gu2b3UKernO16mAWXNDWfvS2bk nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB -----END CERTIFICATE-----` + // nolint:gosec keyFileData := `-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC 5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn @@ -237,9 +237,9 @@ weywTxDl/OD5ybNkZIRKsIXciFYG1VCGO2HNGN9qJcV+nJ63kyrIBauwUkuEhiN5 uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc= -----END RSA PRIVATE KEY-----` - certFile, err := ioutil.TempFile("/tmp", "cert_") + certFile, err := os.CreateTemp("/tmp", "cert_") require.NoError(t.T(), err, "cannot create temp cert file") - keyFile, err := ioutil.TempFile("/tmp", "key_") + keyFile, err := os.CreateTemp("/tmp", "key_") require.NoError(t.T(), err, "cannot create temp key file") _, err = certFile.WriteString(certFileData) @@ -252,7 +252,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc= errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file") mux := &http.ServeMux{} - srv := &http.Server{Addr: mockServerAddr, Handler: mux} + srv := &http.Server{Addr: mockServerAddr, Handler: mux, ReadHeaderTimeout: defaultDialerTimeout} mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) _, _ = io.WriteString(w, "ok") @@ -307,7 +307,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc= defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) + data, err := io.ReadAll(resp.Body) require.NoError(t.T(), err, "error while reading body") assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code") diff --git a/core/util/testutil/gock.go b/core/util/testutil/gock.go index 130daf3..7505ace 100644 --- a/core/util/testutil/gock.go +++ b/core/util/testutil/gock.go @@ -46,7 +46,7 @@ func printRequestData(t UnmatchedRequestsTestingT, r *http.Request) { } } - if r.Body == nil { + if r.Body == nil { // nolint:nestif t.Log("No body is present.") } else { data, err := io.ReadAll(r.Body) diff --git a/core/util/testutil/gorm.go b/core/util/testutil/gorm.go index b53217f..0c01e11 100644 --- a/core/util/testutil/gorm.go +++ b/core/util/testutil/gorm.go @@ -11,11 +11,11 @@ import ( // remove all the entities created after the hook was set up. // You can use it like this: // -// func TestSomething(t *testing.T){ -// db, _ := gorm.Open(...) -// cleaner := DeleteCreatedEntities(db) -// defer cleaner() -// }. +// func TestSomething(t *testing.T){ +// db, _ := gorm.Open(...) +// cleaner := DeleteCreatedEntities(db) +// defer cleaner() +// }. func DeleteCreatedEntities(db *gorm.DB) func() { // nolint type entity struct { key interface{} diff --git a/core/util/testutil/json_record_scanner.go b/core/util/testutil/json_record_scanner.go index 2a422c1..6887fc2 100644 --- a/core/util/testutil/json_record_scanner.go +++ b/core/util/testutil/json_record_scanner.go @@ -3,8 +3,9 @@ package testutil import ( "bufio" "encoding/json" - "github.com/guregu/null/v5" "io" + + "github.com/guregu/null/v5" ) type LogRecord struct { diff --git a/core/util/testutil/json_record_scanner_test.go b/core/util/testutil/json_record_scanner_test.go index 4cf4fea..d8fd64d 100644 --- a/core/util/testutil/json_record_scanner_test.go +++ b/core/util/testutil/json_record_scanner_test.go @@ -2,11 +2,12 @@ package testutil import ( "bytes" - "github.com/stretchr/testify/suite" "io" "strings" "testing" "time" + + "github.com/stretchr/testify/suite" ) type JSONRecordScannerTest struct { diff --git a/core/util/testutil/translations_extractor.go b/core/util/testutil/translations_extractor.go index 4abb1a8..bf63cbe 100644 --- a/core/util/testutil/translations_extractor.go +++ b/core/util/testutil/translations_extractor.go @@ -3,7 +3,6 @@ package testutil import ( "errors" "io/fs" - "io/ioutil" "os" "path/filepath" "sort" @@ -63,8 +62,8 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf err error ) - if info, err = os.Stat(fileName); err == nil { - if !info.IsDir() { + if info, err = os.Stat(fileName); err == nil { // nolint:nestif + if !info.IsDir() { // nolint:nestif var ( path string source []byte @@ -75,7 +74,7 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf return dataMap, err } - if source, err = ioutil.ReadFile(path); err != nil { + if source, err = os.ReadFile(path); err != nil { return dataMap, err } diff --git a/core/util/testutil/translations_extractor_test.go b/core/util/testutil/translations_extractor_test.go index 11de554..43552be 100644 --- a/core/util/testutil/translations_extractor_test.go +++ b/core/util/testutil/translations_extractor_test.go @@ -1,7 +1,6 @@ package testutil import ( - "io/ioutil" "os" "reflect" "sort" @@ -40,7 +39,7 @@ func (t *TranslationsExtractorTest) SetupSuite() { data, _ := yaml.Marshal(translation) // It's not regular temporary file. Little hack in order to test translations extractor. // nolint:gosec - errWrite := ioutil.WriteFile("/tmp/translate.en.yml", data, os.ModePerm) + errWrite := os.WriteFile("/tmp/translate.en.yml", data, os.ModePerm) require.NoError(t.T(), errWrite) t.extractor = NewTranslationsExtractor("translate.{}.yml") diff --git a/core/util/utils.go b/core/util/utils.go index 27e6e71..71bbac6 100644 --- a/core/util/utils.go +++ b/core/util/utils.go @@ -20,9 +20,10 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/gin-gonic/gin" retailcrm "github.com/retailcrm/api-client-go/v2" - v1 "github.com/retailcrm/mg-transport-api-client-go/v1" "github.com/retailcrm/mg-transport-core/v2/core/config" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/util/errorutil" diff --git a/core/util/utils_test.go b/core/util/utils_test.go index 5372b52..cc041e9 100644 --- a/core/util/utils_test.go +++ b/core/util/utils_test.go @@ -11,11 +11,12 @@ import ( "github.com/h2non/gock" retailcrm "github.com/retailcrm/api-client-go/v2" - v1 "github.com/retailcrm/mg-transport-api-client-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/logger" From ca47e757bc07c5958fac95777ab585f34d43012b Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 14 Jun 2024 18:17:54 +0300 Subject: [PATCH 25/26] fix race & prettier test output --- .github/workflows/ci.yml | 4 +++- core/localizer_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58371b7..893c74e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,9 @@ jobs: - name: Get dependencies run: go mod tidy - name: Tests - run: go test ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic + run: | + go install gotest.tools/gotestsum@latest + gotestsum --format testdox ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic - name: Coverage run: | go install github.com/axw/gocov/gocov@latest diff --git a/core/localizer_test.go b/core/localizer_test.go index 35050fa..4190974 100644 --- a/core/localizer_test.go +++ b/core/localizer_test.go @@ -102,7 +102,6 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() { func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { var wg sync.WaitGroup - r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec l.localizer.Preload(DefaultLanguages) langMsgMap := map[language.Tag]string{ language.English: "Test message", @@ -120,6 +119,7 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { for i := 0; i < 1000; i++ { wg.Add(1) go func(m map[language.Tag]string, wg *sync.WaitGroup) { + r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(i))) // nolint:gosec var tag language.Tag switch r.Intn(3-1) + 1 { // nolint:gosec case 1: From 7533ee4423a102e0e868d4550c9232522bd6540f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 14 Jun 2024 18:20:19 +0300 Subject: [PATCH 26/26] fix lint --- core/localizer_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/localizer_test.go b/core/localizer_test.go index 4190974..ec41cb8 100644 --- a/core/localizer_test.go +++ b/core/localizer_test.go @@ -118,6 +118,7 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { for i := 0; i < 1000; i++ { wg.Add(1) + i := i go func(m map[language.Tag]string, wg *sync.WaitGroup) { r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(i))) // nolint:gosec var tag language.Tag