diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 36d4a5e..893c74e 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
@@ -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/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.
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 e49edcc..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"
@@ -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"`
}
@@ -130,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)
}
@@ -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..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"
@@ -38,6 +37,7 @@ transport_info:
sentry_dsn: dsn string
log_level: 5
+log_format: console
debug: true
update_interval: 24
@@ -53,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)
@@ -90,6 +90,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/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/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/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 bd3b38a..72dfd48 100644
--- a/core/engine.go
+++ b/core/engine.go
@@ -7,14 +7,15 @@ import (
"io/fs"
"net/http"
"sync"
+ "time"
"github.com/blacked/go-zabbix"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
- "github.com/op/go-logging"
- "github.com/retailcrm/zabbix-metrics-collector"
+ metrics "github.com/retailcrm/zabbix-metrics-collector"
+ "go.uber.org/zap"
"golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config"
@@ -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,
}
@@ -62,16 +65,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
@@ -112,11 +114,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
}
@@ -133,9 +130,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 +144,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.NewStandard(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter))
+ e.SetLogger(logger.NewDefault(logFormat, e.Config.IsDebug()))
e.Sentry.Localizer = &e.Localizer
e.Utils.Logger = e.Logger()
e.Sentry.Logger = e.Logger()
@@ -175,7 +174,25 @@ 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
+}
+
+// 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
+ }
+ 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",
+ zap.String(logger.HTTPMethodAttr, httpMethod),
+ zap.String("path", absolutePath),
+ zap.String(logger.HandlerAttr, handlerName),
+ zap.Int("handlerCount", nuHandlers))
+ }
return e
}
@@ -247,6 +264,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
}
@@ -262,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 259ef92..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"
@@ -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.NewNil()
e.engine.SetLogger(nil)
assert.NotNil(e.T(), e.engine.logger)
}
@@ -366,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/healthcheck/processor.go b/core/healthcheck/processor.go
index fc11ccd..f7f4216 100644
--- a/core/healthcheck/processor.go
+++ b/core/healthcheck/processor.go
@@ -2,6 +2,7 @@ package healthcheck
import (
"github.com/retailcrm/mg-transport-core/v2/core/logger"
+ "go.uber.org/zap"
)
const (
@@ -29,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 id=%d because its failure is already processed", 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 id=%d", 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 id=%d: %s (message: %s)",
- id, err, counter.Message())
+ c.debugLog("cannot send notification for counter",
+ zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
}
counter.FailureProcessed()
return true
@@ -53,7 +54,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",
+ zap.Int(logger.CounterIDAttr, id), zap.Any("minRequests", c.MinRequests))
return true
}
@@ -72,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 id=%d", 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 id=%d: %s (message: %s)",
- id, err, counter.Message())
+ c.debugLog("cannot send notification for counter",
+ zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
}
counter.CountersProcessed()
return true
@@ -96,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.Debugf(msg, args...)
+ c.Logger.Debug(msg, logger.AnyZapFields(args)...)
}
}
diff --git a/core/healthcheck/processor_test.go b/core/healthcheck/processor_test.go
index 9774310..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,7 +95,12 @@ 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")
+
+ 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() {
@@ -107,7 +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 id=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() {
@@ -121,7 +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 id=1: http status code: 500 (message: 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)
@@ -138,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)
@@ -154,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 id=%d because it has fewer than %d requests", 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() {
@@ -170,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)
}
@@ -185,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)
}
@@ -200,7 +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 id=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)
}
@@ -218,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 id=1: unknown 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("unknown error", logs[0].Context["error"])
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
}
@@ -235,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)
}
@@ -252,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.go b/core/job_manager.go
index fac84de..4a734cc 100644
--- a/core/job_manager.go
+++ b/core/job_manager.go
@@ -7,6 +7,7 @@ import (
"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.
@@ -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
@@ -73,6 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf
}
}()
+ log = log.With(logger.Handler(name))
err := j.Command(log)
if err != nil && j.ErrorHandler != nil {
j.ErrorHandler(name, err, log)
@@ -153,7 +155,7 @@ func NewJobManager() *JobManager {
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", zap.String("job", name), logger.Err(err))
}
}
}
@@ -162,7 +164,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", zap.String("job", name), zap.Any("value", recoverValue))
}
}
}
diff --git a/core/job_manager_test.go b/core/job_manager_test.go
index 6da22de..ac3ce2a 100644
--- a/core/job_manager_test.go
+++ b/core/job_manager_test.go
@@ -9,10 +9,11 @@ import (
"testing"
"time"
- "github.com/op/go-logging"
"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"
)
@@ -25,7 +26,7 @@ type JobTest struct {
executeErr chan error
panicValue chan interface{}
lastLog string
- lastMsgLevel logging.Level
+ lastMsgLevel zapcore.Level
syncBool bool
}
@@ -36,68 +37,94 @@ type JobManagerTest struct {
syncRunnerFlag bool
}
-type callbackLoggerFunc func(level logging.Level, format string, args ...interface{})
+type callbackLoggerFunc func(level zapcore.Level, msg string, args ...zap.Field)
type callbackLogger struct {
- fn callbackLoggerFunc
+ fields []zap.Field
+ fn callbackLoggerFunc
}
-func (n *callbackLogger) Fatal(args ...interface{}) {
- n.fn(logging.CRITICAL, "", args...)
+func (n *callbackLogger) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
+ return &zapcore.CheckedEntry{}
}
-func (n *callbackLogger) Fatalf(format string, args ...interface{}) {
- n.fn(logging.CRITICAL, format, args...)
+func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) {
+ n.fn(zap.PanicLevel, msg, fields...)
}
-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) Panic(msg string, fields ...zap.Field) {
+ n.fn(zap.PanicLevel, msg, fields...)
}
-func (n *callbackLogger) Critical(args ...interface{}) {
- n.fn(logging.CRITICAL, "", args...)
+func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) {
+ n.fn(zap.FatalLevel, msg, fields...)
}
-func (n *callbackLogger) Criticalf(format string, args ...interface{}) {
- n.fn(logging.CRITICAL, format, args...)
+func (n *callbackLogger) Sync() error {
+ return nil
}
-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) clone() *callbackLogger {
+ return &callbackLogger{fn: n.fn, fields: n.fields}
}
-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) 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...) // nolint:gocritic
+ return cl
}
-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) Level() zapcore.Level {
+ return zapcore.DebugLevel
}
-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) With(args ...zap.Field) logger.Logger {
+ return n.cloneWithFields(args)
}
-func (n *callbackLogger) Debug(args ...interface{}) {
- n.fn(logging.DEBUG, "", args...)
+func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger {
+ return n.cloneWithFields(args)
}
-func (n *callbackLogger) Debugf(format string, args ...interface{}) {
- n.fn(logging.DEBUG, format, args...)
+
+func (n *callbackLogger) WithGroup(_ string) logger.Logger {
+ return n
+}
+
+func (n *callbackLogger) ForHandler(_ any) logger.Logger {
+ return n
+}
+
+func (n *callbackLogger) ForConnection(_ any) logger.Logger {
+ return n
+}
+
+func (n *callbackLogger) ForAccount(_ any) logger.Logger {
+ return n
+}
+
+func (n *callbackLogger) Log(level zapcore.Level, msg string, args ...zap.Field) {
+ n.fn(level, msg, args...)
+}
+
+func (n *callbackLogger) Debug(msg string, args ...zap.Field) {
+ n.Log(zap.DebugLevel, msg, args...)
+}
+
+func (n *callbackLogger) Info(msg string, args ...zap.Field) {
+ n.Log(zap.InfoLevel, msg, args...)
+}
+
+func (n *callbackLogger) Warn(msg string, args ...zap.Field) {
+ n.Log(zap.WarnLevel, msg, args...)
+}
+
+func (n *callbackLogger) Error(msg string, args ...zap.Field) {
+ n.Log(zap.ErrorLevel, msg, args...)
}
func TestJob(t *testing.T) {
@@ -115,9 +142,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(level zapcore.Level, s string, i ...zap.Field) {
require.Len(t, i, 2)
- assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
+ assert.Equal(t, "error=test", fmt.Sprintf("%s=%v", i[1].Key, i[1].Interface))
}})
}
@@ -128,9 +155,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(level zapcore.Level, s string, i ...zap.Field) {
require.Len(t, i, 2)
- assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
+ assert.Equal(t, "value=test", fmt.Sprintf("%s=%s", i[1].Key, i[1].Interface))
}})
}
@@ -147,7 +174,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(level zapcore.Level, format string, args ...zap.Field) {
if format == "" {
var sb strings.Builder
sb.Grow(3 * len(args)) // nolint:gomnd
@@ -159,7 +186,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
}}
}
@@ -256,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(),
@@ -271,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
@@ -404,11 +435,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.NewDefault("json", true))
+ 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/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..ec41cb8 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,6 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
var wg sync.WaitGroup
- rand.Seed(time.Now().UnixNano())
l.localizer.Preload(DefaultLanguages)
langMsgMap := map[language.Tag]string{
language.English: "Test message",
@@ -120,9 +118,11 @@ 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
- 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/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..8d88117
--- /dev/null
+++ b/core/logger/api_client_adapter.go
@@ -0,0 +1,42 @@
+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
+}
+
+// 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{}) {
+ 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..4a4d6bc
--- /dev/null
+++ b/core/logger/api_client_adapter_test.go
@@ -0,0 +1,41 @@
+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"
+)
+
+func TestAPIClientAdapter(t *testing.T) {
+ log := newJSONBufferedLogger(nil)
+ 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
new file mode 100644
index 0000000..06ec6e0
--- /dev/null
+++ b/core/logger/attrs.go
@@ -0,0 +1,94 @@
+package logger
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+
+ json "github.com/goccy/go-json"
+
+ "go.uber.org/zap"
+)
+
+// 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, "")
+ }
+ 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/attrs_test.go b/core/logger/attrs_test.go
new file mode 100644
index 0000000..2bb519d
--- /dev/null
+++ b/core/logger/attrs_test.go
@@ -0,0 +1,156 @@
+package logger
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+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
new file mode 100644
index 0000000..0c6115e
--- /dev/null
+++ b/core/logger/buffer_logger_test.go
@@ -0,0 +1,266 @@
+package logger
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "io"
+ "os"
+ "sync"
+
+ "github.com/guregu/null/v5"
+
+ "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(buf *bufferLogger) *jSONRecordScanner {
+ if buf == nil {
+ 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
new file mode 100644
index 0000000..017b4b5
--- /dev/null
+++ b/core/logger/default.go
@@ -0,0 +1,100 @@
+package logger
+
+import (
+ "strconv"
+
+ "go.uber.org/zap"
+ "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++ {
+ 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/default_test.go b/core/logger/default_test.go
new file mode 100644
index 0000000..7301568
--- /dev/null
+++ b/core/logger/default_test.go
@@ -0,0 +1,90 @@
+package logger
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/zap"
+)
+
+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.go b/core/logger/gin.go
new file mode 100644
index 0000000..5ecf3b2
--- /dev/null
+++ b/core/logger/gin.go
@@ -0,0 +1,37 @@
+package logger
+
+import (
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+// 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("request",
+ 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/gin_test.go b/core/logger/gin_test.go
new file mode 100644
index 0000000..3cf6cf7
--- /dev/null
+++ b/core/logger/gin_test.go
@@ -0,0 +1,38 @@
+package logger
+
+import (
+ "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) {
+ 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/json_with_context_encoder.go b/core/logger/json_with_context_encoder.go
new file mode 100644
index 0000000..cc3a8a1
--- /dev/null
+++ b/core/logger/json_with_context_encoder.go
@@ -0,0 +1,640 @@
+// 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.
+
+// nolint
+package logger
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "time"
+ "unicode/utf8"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+
+ "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/logger.go b/core/logger/logger.go
deleted file mode 100644
index 32445f9..0000000
--- a/core/logger/logger.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package logger
-
-import (
- "io"
- "os"
- "sync"
-
- "github.com/op/go-logging"
-)
-
-// Logger contains methods which should be present in logger implementation.
-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...)
-}
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/mg_transport_client_adapter.go b/core/logger/mg_transport_client_adapter.go
new file mode 100644
index 0000000..0b8569b
--- /dev/null
+++ b/core/logger/mg_transport_client_adapter.go
@@ -0,0 +1,60 @@
+package logger
+
+import (
+ "fmt"
+
+ "go.uber.org/zap"
+
+ v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
+)
+
+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
+}
+
+// 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{}) {
+ 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 { // nolint:gomnd
+ token = fmt.Sprint(args[2])
+ }
+ if len(args) > 3 { // nolint:gomnd
+ 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..f3ece35
--- /dev/null
+++ b/core/logger/mg_transport_client_adapter_test.go
@@ -0,0 +1,45 @@
+package logger
+
+import (
+ "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) {
+ httpClient := &http.Client{}
+ log := newJSONBufferedLogger(nil)
+ 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
new file mode 100644
index 0000000..bd348ed
--- /dev/null
+++ b/core/logger/nil.go
@@ -0,0 +1,62 @@
+package logger
+
+import (
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+// Nil logger doesn't do anything.
+type Nil struct{}
+
+// NewNil constructs new *Nil.
+func NewNil() Logger {
+ return &Nil{}
+}
+
+func (l *Nil) With(_ ...zap.Field) Logger {
+ return l
+}
+
+func (l *Nil) WithLazy(_ ...zap.Field) Logger {
+ return l
+}
+
+func (l *Nil) Level() zapcore.Level {
+ return zapcore.DebugLevel
+}
+
+func (l *Nil) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
+ return &zapcore.CheckedEntry{}
+}
+
+func (l *Nil) Log(_ zapcore.Level, _ string, _ ...zap.Field) {}
+
+func (l *Nil) Debug(_ string, _ ...zap.Field) {}
+
+func (l *Nil) Info(_ string, _ ...zap.Field) {}
+
+func (l *Nil) Warn(_ string, _ ...zap.Field) {}
+
+func (l *Nil) Error(_ string, _ ...zap.Field) {}
+
+func (l *Nil) DPanic(_ string, _ ...zap.Field) {}
+
+func (l *Nil) Panic(_ string, _ ...zap.Field) {}
+
+func (l *Nil) Fatal(_ string, _ ...zap.Field) {}
+
+func (l *Nil) ForHandler(_ any) Logger {
+ return l
+}
+
+func (l *Nil) ForConnection(_ any) Logger {
+ return l
+}
+
+func (l *Nil) ForAccount(_ any) Logger {
+ return l
+}
+
+func (l *Nil) Sync() error {
+ return nil
+}
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/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/pool_test.go b/core/logger/pool_test.go
new file mode 100644
index 0000000..a73da78
--- /dev/null
+++ b/core/logger/pool_test.go
@@ -0,0 +1,19 @@
+package logger
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+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/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/writer_adapter.go b/core/logger/writer_adapter.go
new file mode 100644
index 0000000..cceca9e
--- /dev/null
+++ b/core/logger/writer_adapter.go
@@ -0,0 +1,22 @@
+package logger
+
+import (
+ "io"
+
+ "go.uber.org/zap/zapcore"
+)
+
+type writerAdapter struct {
+ log Logger
+ 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}
+}
+
+func (w *writerAdapter) Write(p []byte) (n int, err error) {
+ w.log.Log(w.level, string(p))
+ return len(p), nil
+}
diff --git a/core/logger/writer_adapter_test.go b/core/logger/writer_adapter_test.go
new file mode 100644
index 0000000..68e34d2
--- /dev/null
+++ b/core/logger/writer_adapter_test.go
@@ -0,0 +1,25 @@
+package logger
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+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
new file mode 100644
index 0000000..f33face
--- /dev/null
+++ b/core/logger/zabbix_collector_adapter.go
@@ -0,0 +1,34 @@
+package logger
+
+import (
+ "fmt"
+
+ metrics "github.com/retailcrm/zabbix-metrics-collector"
+)
+
+type zabbixCollectorAdapter struct {
+ log Logger
+}
+
+func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) {
+ baseMsg := "cannot send metrics to Zabbix"
+ switch format {
+ 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]
+ }
+ 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/zabbix_collector_adapter_test.go b/core/logger/zabbix_collector_adapter_test.go
new file mode 100644
index 0000000..08f9dfb
--- /dev/null
+++ b/core/logger/zabbix_collector_adapter_test.go
@@ -0,0 +1,26 @@
+package logger
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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/logger/zap.go b/core/logger/zap.go
new file mode 100644
index 0000000..1b8730b
--- /dev/null
+++ b/core/logger/zap.go
@@ -0,0 +1,102 @@
+package logger
+
+import (
+ "fmt"
+ "time"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+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
+ }
+ log, err := zap.Config{
+ Level: zap.NewAtomicLevelAt(level),
+ Development: debug,
+ Encoding: "console",
+ EncoderConfig: EncoderConfigConsole(),
+ OutputPaths: []string{"stdout"},
+ ErrorOutputPaths: []string{"stderr"},
+ }.Build()
+ if err != nil {
+ panic(err)
+ }
+ return log
+}
+
+func EncoderConfigConsole() zapcore.EncoderConfig {
+ return zapcore.EncoderConfig{
+ MessageKey: "message",
+ LevelKey: "level",
+ TimeKey: "datetime",
+ NameKey: "logger",
+ CallerKey: "caller",
+ FunctionKey: zapcore.OmitKey,
+ StacktraceKey: "",
+ LineEnding: "\n",
+ EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
+ encoder.AppendString("level_name=" + level.CapitalString())
+ },
+ EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
+ encoder.AppendString("datetime=" + t.Format(time.RFC3339))
+ },
+ EncodeDuration: zapcore.StringDurationEncoder,
+ EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
+ encoder.AppendString("caller=" + caller.TrimmedPath())
+ },
+ EncodeName: zapcore.FullNameEncoder,
+ 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-with-context",
+ 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_name",
+ TimeKey: "datetime",
+ 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/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/module_features_uploader.go b/core/module_features_uploader.go
index f2979d9..d014938 100644
--- a/core/module_features_uploader.go
+++ b/core/module_features_uploader.go
@@ -12,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"
@@ -64,14 +65,13 @@ func NewModuleFeaturesUploader(
awsConfig.WithCredentialsProvider(customProvider),
)
if err != nil {
- log.Fatal(err)
+ log.Error("cannot load S3 configuration", logger.Err(err))
return nil
}
client := manager.NewUploader(s3.NewFromConfig(cfg))
-
if err != nil {
- log.Fatal(err)
+ log.Error("cannot load S3 configuration", logger.Err(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", zap.String("fileName", s.featuresFilename), logger.Err(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", 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.Errorf("cannot upload file %s: %s", lang.String(), err.Error())
+ s.log.Error("cannot upload file", zap.String("lang", lang.String()), logger.Err(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..9f6763f 100644
--- a/core/sentry.go
+++ b/core/sentry.go
@@ -15,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"
@@ -22,8 +23,7 @@ import (
"github.com/gin-gonic/gin"
)
-// reset is borrowed directly from the gin.
-const reset = "\033[0m"
+const recoveryMiddlewareSkipFrames = 3
// ErrorHandlerFunc will handle errors.
type ErrorHandlerFunc func(recovery interface{}, c *gin.Context)
@@ -142,17 +142,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.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
}
for tag := range s.tagsFromContext(c) {
@@ -164,7 +164,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger {
}
}
- return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID)
+ return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
}
// tagsSetterMiddleware sets event tags into Sentry events.
@@ -201,13 +201,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 +250,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(recoveryMiddlewareSkipFrames, "trace: ")
+ formattedErr := logger.Err(err)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
@@ -259,18 +260,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 := zap.String("headers", strings.Join(headers, "\r\n"))
+ formattedStack := zap.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..2857725 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"
@@ -32,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
}
@@ -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().ForHandler("component").ForConnection("conn").ForAccount("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/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 3e1a17b..e750077 100644
--- a/core/util/httputil/http_client_builder.go
+++ b/core/util/httputil/http_client_builder.go
@@ -10,10 +10,10 @@ import (
"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"
)
@@ -230,12 +230,12 @@ func (b *HTTPClientBuilder) buildMocks() error {
return errors.New("dialer must be built first")
}
- 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: ")
+ 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: ")
for _, domain := range b.mockedDomains {
- b.logf(" - %s\n", domain)
+ b.log(fmt.Sprintf(" - %s\n", domain))
}
b.httpTransport.Proxy = nil
@@ -259,7 +259,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 +270,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, logger.AnyZapFields(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..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"
@@ -16,7 +15,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 +135,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.NewDefault("json", true)
+ builder.WithLogger(log)
assert.NotNil(t.T(), builder.logger)
}
@@ -153,7 +151,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() {
@@ -210,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
@@ -238,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)
@@ -253,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")
@@ -308,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/buffer_logger.go b/core/util/testutil/buffer_logger.go
index 79619d6..5659ede 100644
--- a/core/util/testutil/buffer_logger.go
+++ b/core/util/testutil/buffer_logger.go
@@ -1,15 +1,13 @@
package testutil
import (
- "bytes"
"fmt"
"io"
"os"
- "sync"
-
- "github.com/op/go-logging"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
)
// ReadBuffer is implemented by the BufferLogger.
@@ -28,120 +26,75 @@ 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 {
- buf bytes.Buffer
- rw sync.RWMutex
+ logger.Default
+ buf LockableBuffer
}
// NewBufferedLogger returns new BufferedLogger instance.
func NewBufferedLogger() BufferedLogger {
- return &BufferLogger{}
+ bl := &BufferLogger{}
+ bl.Logger = zap.New(
+ zapcore.NewCore(
+ logger.NewJSONWithContextEncoder(
+ logger.EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
+ return bl
+}
+
+func (l *BufferLogger) With(fields ...zapcore.Field) logger.Logger {
+ return &BufferLogger{
+ Default: logger.Default{
+ 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) 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.
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..8d0771d 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,20 @@ 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_name\":\"DEBUG\"")
+ t.Assert().Contains(string(data), "\"message\":\"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_name\":\"DEBUG\"")
+ t.Assert().Contains(string(t.logger.Bytes()), "\"message\":\"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_name\":\"DEBUG\"")
+ t.Assert().Contains(t.logger.String(), "\"message\":\"test\"")
}
func (t *BufferLoggerTest) TestRace() {
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
new file mode 100644
index 0000000..6887fc2
--- /dev/null
+++ b/core/util/testutil/json_record_scanner.go
@@ -0,0 +1,52 @@
+package testutil
+
+import (
+ "bufio"
+ "encoding/json"
+ "io"
+
+ "github.com/guregu/null/v5"
+)
+
+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..d8fd64d
--- /dev/null
+++ b/core/util/testutil/json_record_scanner_test.go
@@ -0,0 +1,108 @@
+package testutil
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+)
+
+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/testutil/lockable_buffer.go b/core/util/testutil/lockable_buffer.go
new file mode 100644
index 0000000..8636a9a
--- /dev/null
+++ b/core/util/testutil/lockable_buffer.go
@@ -0,0 +1,155 @@
+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)
+}
+
+// Sync is a no-op.
+func (b *LockableBuffer) Sync() error {
+ return nil
+}
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 2ff8f5f..71bbac6 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,11 +18,12 @@ 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"
-
"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"
@@ -132,7 +135,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()
@@ -142,12 +145,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)
}
}
@@ -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)
+}
diff --git a/core/util/utils_test.go b/core/util/utils_test.go
index ce7a015..cc041e9 100644
--- a/core/util/utils_test.go
+++ b/core/util/utils_test.go
@@ -9,13 +9,14 @@ 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"
"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"
@@ -38,7 +39,7 @@ func mgClient() *v1.MgClient {
}
func (u *UtilsTest) SetupSuite() {
- logger := logger.NewStandard("code", logging.DEBUG, logger.DefaultLogFormatter())
+ 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 0a8c1e7..4c29cc8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,8 @@
module github.com/retailcrm/mg-transport-core/v2
-go 1.18
+go 1.22
+
+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
@@ -24,10 +27,11 @@ 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
+ 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 +81,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 6f7ec25..5cf0ba1 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=
@@ -237,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=
@@ -296,8 +299,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=
@@ -376,8 +380,8 @@ 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=
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=
@@ -441,6 +445,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=