New logging format

This commit is contained in:
Pavel 2024-06-14 18:22:18 +03:00 committed by GitHub
commit 36d32de72f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 2848 additions and 1404 deletions

View File

@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go-version: ['1.18', '1.19', '1.20', '1.21', '1.22', 'stable'] go-version: ['1.22', 'stable']
steps: steps:
- name: Set up Go ${{ matrix.go-version }} - name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v3 uses: actions/setup-go@v3
@ -46,7 +46,9 @@ jobs:
- name: Get dependencies - name: Get dependencies
run: go mod tidy run: go mod tidy
- name: Tests - 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 - name: Coverage
run: | run: |
go install github.com/axw/gocov/gocov@latest go install github.com/axw/gocov/gocov@latest

View File

@ -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) [![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) [![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) [![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) [![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. This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.

View File

@ -32,8 +32,7 @@ func main() {
if _, err := parser.Parse(); err != nil { if _, err := parser.Parse(); err != nil {
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint
os.Exit(0) os.Exit(0)
} else { }
os.Exit(1) os.Exit(1)
} }
} }
}

View File

@ -1,7 +1,7 @@
package config package config
import ( import (
"io/ioutil" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -14,6 +14,7 @@ type Configuration interface {
GetVersion() string GetVersion() string
GetSentryDSN() string GetSentryDSN() string
GetLogLevel() logging.Level GetLogLevel() logging.Level
GetLogFormat() string
GetHTTPConfig() HTTPServerConfig GetHTTPConfig() HTTPServerConfig
GetZabbixConfig() ZabbixConfig GetZabbixConfig() ZabbixConfig
GetDBConfig() DatabaseConfig GetDBConfig() DatabaseConfig
@ -44,6 +45,7 @@ type Config struct {
Database DatabaseConfig `yaml:"database"` Database DatabaseConfig `yaml:"database"`
UpdateInterval int `yaml:"update_interval"` UpdateInterval int `yaml:"update_interval"`
LogLevel logging.Level `yaml:"log_level"` LogLevel logging.Level `yaml:"log_level"`
LogFormat string `yaml:"log_format"`
Debug bool `yaml:"debug"` Debug bool `yaml:"debug"`
} }
@ -130,7 +132,7 @@ func (c *Config) GetConfigData(path string) []byte {
panic(err) panic(err)
} }
source, err := ioutil.ReadFile(path) source, err := os.ReadFile(path)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -153,6 +155,10 @@ func (c Config) GetLogLevel() logging.Level {
return c.LogLevel return c.LogLevel
} }
func (c Config) GetLogFormat() string {
return c.LogFormat
}
// GetTransportInfo transport basic data. // GetTransportInfo transport basic data.
func (c Config) GetTransportInfo() InfoInterface { func (c Config) GetTransportInfo() InfoInterface {
return c.TransportInfo return c.TransportInfo

View File

@ -1,7 +1,6 @@
package config package config
import ( import (
"io/ioutil"
"os" "os"
"path" "path"
"testing" "testing"
@ -38,6 +37,7 @@ transport_info:
sentry_dsn: dsn string sentry_dsn: dsn string
log_level: 5 log_level: 5
log_format: console
debug: true debug: true
update_interval: 24 update_interval: 24
@ -53,7 +53,7 @@ config_aws:
bucket: bucket bucket: bucket
folder_name: folder folder_name: folder
content_type: image/jpeg`) 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) require.Nil(c.T(), err)
c.config = NewConfig(testConfigFile) c.config = NewConfig(testConfigFile)
@ -90,6 +90,10 @@ func (c *ConfigTest) Test_GetLogLevel() {
assert.Equal(c.T(), logging.Level(5), c.config.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() { func (c *ConfigTest) Test_IsDebug() {
assert.Equal(c.T(), true, c.config.IsDebug()) assert.Equal(c.T(), true, c.config.IsDebug())
} }

View File

@ -51,7 +51,7 @@ func (x *NewMigrationCommand) FileExists(filename string) bool {
} }
// Execute migration generator command. // Execute migration generator command.
func (x *NewMigrationCommand) Execute(args []string) error { func (x *NewMigrationCommand) Execute(_ []string) error {
tpl, err := template.New("migration").Parse(migrationTemplate) tpl, err := template.New("migration").Parse(migrationTemplate)
if err != nil { if err != nil {
return fmt.Errorf("fatal: cannot parse base migration template: %w", err) return fmt.Errorf("fatal: cannot parse base migration template: %w", err)

View File

@ -2,7 +2,6 @@ package db
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"os" "os"
@ -36,7 +35,7 @@ func (s *MigrationGeneratorSuite) Test_FileExists() {
func (s *MigrationGeneratorSuite) Test_Execute() { func (s *MigrationGeneratorSuite) Test_Execute() {
found := false found := false
assert.NoError(s.T(), s.command.Execute([]string{})) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -17,3 +17,10 @@ type Connection struct {
ID int `gorm:"primary_key" json:"id"` ID int `gorm:"primary_key" json:"id"`
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
} }
func (c Connection) Address() string {
if c.PublicURL != "" {
return c.PublicURL
}
return c.URL
}

View File

@ -14,8 +14,11 @@ type User struct {
// TableName will return table name for User // TableName will return table name for User
// It will not work if User is not embedded, but mapped as another type // 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 // but
//
// type MyUser struct { // will work // type MyUser struct { // will work
// User // User
// } // }

View File

@ -2,6 +2,7 @@
Package core provides different functions like error-reporting, logging, localization, etc. Package core provides different functions like error-reporting, logging, localization, etc.
to make it easier to create transports. to make it easier to create transports.
Usage: Usage:
package main package main
import ( import (
@ -36,14 +37,19 @@ Usage:
} }
} }
Resource embedding # Resource embedding
packr can be used to provide resource embedding, see: packr can be used to provide resource embedding, see:
https://github.com/gobuffalo/packr/tree/master/v2 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. In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library.
You can find instruction here: You can find instruction here:
https://github.com/gobuffalo/packr/tree/master/v2#library-installation https://github.com/gobuffalo/packr/tree/master/v2#library-installation
Example of usage: Example of usage:
package main package main
import ( 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: 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 $ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
Currently, it only can generate new migrations for your transport. 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. Copyright (c) 2019 RetailDriver LLC. Usage of this source code is governed by a MIT license.

View File

@ -2,7 +2,7 @@ package core
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net/http" "net/http"
) )
@ -40,7 +40,7 @@ func getDomainsByStore(store string) []Domain {
return nil return nil
} }
respBody, readErr := ioutil.ReadAll(resp.Body) respBody, readErr := io.ReadAll(resp.Body)
if readErr != nil { if readErr != nil {
return nil return nil

View File

@ -7,14 +7,15 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"sync" "sync"
"time"
"github.com/blacked/go-zabbix" "github.com/blacked/go-zabbix"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/op/go-logging" metrics "github.com/retailcrm/zabbix-metrics-collector"
"github.com/retailcrm/zabbix-metrics-collector" "go.uber.org/zap"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/config"
@ -26,12 +27,14 @@ import (
"github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/logger"
) )
const DefaultHTTPClientTimeout time.Duration = 30
var boolTrue = true var boolTrue = true
// DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client // 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. // if HTTP client config is not present in the configuration.
var DefaultHTTPClientConfig = &config.HTTPClientConfig{ var DefaultHTTPClientConfig = &config.HTTPClientConfig{
Timeout: 30, Timeout: DefaultHTTPClientTimeout,
SSLVerification: &boolTrue, SSLVerification: &boolTrue,
} }
@ -65,7 +68,6 @@ type Engine struct {
logger logger.Logger logger logger.Logger
AppInfo AppInfo AppInfo AppInfo
Sessions sessions.Store Sessions sessions.Store
LogFormatter logging.Formatter
Config config.Configuration Config config.Configuration
Zabbix metrics.Transport Zabbix metrics.Transport
ginEngine *gin.Engine ginEngine *gin.Engine
@ -112,11 +114,6 @@ func (e *Engine) initGin() {
e.buildSentryConfig() e.buildSentryConfig()
e.InitSentrySDK() e.InitSentrySDK()
r.Use(e.SentryMiddlewares()...) r.Use(e.SentryMiddlewares()...)
if e.Config.IsDebug() {
r.Use(gin.Logger())
}
r.Use(e.LocalizationMiddleware()) r.Use(e.LocalizationMiddleware())
e.ginEngine = r e.ginEngine = r
} }
@ -133,9 +130,6 @@ func (e *Engine) Prepare() *Engine {
if e.DefaultError == "" { if e.DefaultError == "" {
e.DefaultError = "error" e.DefaultError = "error"
} }
if e.LogFormatter == nil {
e.LogFormatter = logger.DefaultLogFormatter()
}
if e.LocaleMatcher == nil { if e.LocaleMatcher == nil {
e.LocaleMatcher = DefaultLocalizerMatcher() e.LocaleMatcher = DefaultLocalizerMatcher()
} }
@ -150,9 +144,14 @@ func (e *Engine) Prepare() *Engine {
e.Localizer.Preload(e.PreloadLanguages) e.Localizer.Preload(e.PreloadLanguages)
} }
logFormat := "json"
if format := e.Config.GetLogFormat(); format != "" {
logFormat = format
}
e.CreateDB(e.Config.GetDBConfig()) e.CreateDB(e.Config.GetDBConfig())
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0) 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.Sentry.Localizer = &e.Localizer
e.Utils.Logger = e.Logger() e.Utils.Logger = e.Logger()
e.Sentry.Logger = e.Logger() e.Sentry.Logger = e.Logger()
@ -175,7 +174,25 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine {
} }
cfg := e.Config.GetZabbixConfig() cfg := e.Config.GetZabbixConfig()
sender := zabbix.NewSender(cfg.ServerHost, cfg.ServerPort) 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 return e
} }
@ -247,6 +264,9 @@ func (e *Engine) SetLogger(l logger.Logger) *Engine {
e.mutex.Lock() e.mutex.Lock()
defer e.mutex.Unlock() defer e.mutex.Unlock()
if !e.prepared && e.logger != nil {
return e
}
e.logger = l e.logger = l
return e return e
} }
@ -262,10 +282,9 @@ func (e *Engine) BuildHTTPClient(certs *x509.CertPool, replaceDefault ...bool) *
if err != nil { if err != nil {
panic(err) panic(err)
} else {
e.httpClient = client
} }
e.httpClient = client
return e return e
} }

View File

@ -6,7 +6,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -123,7 +123,6 @@ func (e *EngineTest) Test_Prepare() {
assert.True(e.T(), e.engine.prepared) assert.True(e.T(), e.engine.prepared)
assert.NotNil(e.T(), e.engine.Config) assert.NotNil(e.T(), e.engine.Config)
assert.NotEmpty(e.T(), e.engine.DefaultError) assert.NotEmpty(e.T(), e.engine.DefaultError)
assert.NotEmpty(e.T(), e.engine.LogFormatter)
assert.NotEmpty(e.T(), e.engine.LocaleMatcher) assert.NotEmpty(e.T(), e.engine.LocaleMatcher)
assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag)) assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag))
assert.NotNil(e.T(), e.engine.DB) assert.NotNil(e.T(), e.engine.DB)
@ -253,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() {
defer func() { defer func() {
e.engine.logger = origLogger e.engine.logger = origLogger
}() }()
e.engine.logger = &logger.StandardLogger{} e.engine.logger = logger.NewNil()
e.engine.SetLogger(nil) e.engine.SetLogger(nil)
assert.NotNil(e.T(), e.engine.logger) assert.NotNil(e.T(), e.engine.logger)
} }
@ -366,7 +365,7 @@ func (e *EngineTest) Test_GetCSRFToken() {
URL: &url.URL{ URL: &url.URL{
RawQuery: "", RawQuery: "",
}, },
Body: ioutil.NopCloser(bytes.NewReader([]byte{})), Body: io.NopCloser(bytes.NewReader([]byte{})),
Header: http.Header{"X-CSRF-Token": []string{"token"}}, Header: http.Header{"X-CSRF-Token": []string{"token"}},
}} }}
c.Set("csrf_token", "token") c.Set("csrf_token", "token")

View File

@ -10,7 +10,7 @@ import (
const DefaultResetPeriod = time.Minute * 15 const DefaultResetPeriod = time.Minute * 15
// AtomicCounter is a default Counter implementation. // 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 { type AtomicCounter struct {
name atomic.String name atomic.String
msg atomic.String msg atomic.String

View File

@ -1,7 +1,7 @@
package healthcheck package healthcheck
var ( 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{}) _ = Storage(&SyncMapStorage{})
_ = Counter(&AtomicCounter{}) _ = Counter(&AtomicCounter{})
_ = Processor(CounterProcessor{}) _ = Processor(CounterProcessor{})

View File

@ -2,6 +2,7 @@ package healthcheck
import ( import (
"github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/logger"
"go.uber.org/zap"
) )
const ( const (
@ -29,19 +30,19 @@ type CounterProcessor struct {
func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen
if counter.IsFailed() { if counter.IsFailed() {
if counter.IsFailureProcessed() { 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 return true
} }
apiURL, apiKey, _, exists := c.ConnectionDataProvider(id) apiURL, apiKey, _, exists := c.ConnectionDataProvider(id)
if !exists { 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 return true
} }
err := c.Notifier(apiURL, apiKey, counter.Message()) err := c.Notifier(apiURL, apiKey, counter.Message())
if err != nil { if err != nil {
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)", c.debugLog("cannot send notification for counter",
id, err, counter.Message()) zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
} }
counter.FailureProcessed() counter.FailureProcessed()
return true 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. // Ignore this counter for now because total count of requests is less than minimal count.
// The results may not be representative. // The results may not be representative.
if (succeeded + failed) < c.MinRequests { 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 return true
} }
@ -72,13 +74,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id) apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id)
if !exists { 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 return true
} }
err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang)) err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang))
if err != nil { if err != nil {
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)", c.debugLog("cannot send notification for counter",
id, err, counter.Message()) zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
} }
counter.CountersProcessed() counter.CountersProcessed()
return true return true
@ -96,6 +98,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string {
func (c CounterProcessor) debugLog(msg string, args ...interface{}) { func (c CounterProcessor) debugLog(msg string, args ...interface{}) {
if c.Debug { if c.Debug {
c.Logger.Debugf(msg, args...) c.Logger.Debug(msg, logger.AnyZapFields(args)...)
} }
} }

View File

@ -45,7 +45,7 @@ func (t *CounterProcessorTest) localizer() NotifyMessageLocalizer {
} }
func (t *CounterProcessorTest) new( 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() loc := t.localizer()
if len(noLocalizer) > 0 && noLocalizer[0] { if len(noLocalizer) > 0 && noLocalizer[0] {
loc = nil loc = nil
@ -61,7 +61,7 @@ func (t *CounterProcessorTest) new(
FailureThreshold: DefaultFailureThreshold, FailureThreshold: DefaultFailureThreshold,
MinRequests: DefaultMinRequests, MinRequests: DefaultMinRequests,
Debug: true, Debug: true,
}, log }, testutil.NewJSONRecordScanner(log)
} }
func (t *CounterProcessorTest) notifier(err ...error) *notifierMock { func (t *CounterProcessorTest) notifier(err ...error) *notifierMock {
@ -95,7 +95,12 @@ func (t *CounterProcessorTest) Test_FailureProcessed() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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() { func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
@ -107,7 +112,12 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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() { func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
@ -121,7 +131,14 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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.apiURL, n.apiURL)
t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal(t.apiKey, n.apiKey)
t.Assert().Equal("error message", n.message) t.Assert().Equal("error message", n.message)
@ -138,7 +155,10 @@ func (t *CounterProcessorTest) Test_CounterFailed_SentNotification() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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.apiURL, n.apiURL)
t.Assert().Equal(t.apiKey, n.apiKey) t.Assert().Equal(t.apiKey, n.apiKey)
t.Assert().Equal("error message", n.message) t.Assert().Equal("error message", n.message)
@ -154,8 +174,13 @@ func (t *CounterProcessorTest) Test_TooFewRequests() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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() { func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
@ -170,7 +195,10 @@ func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Empty(n.message)
} }
@ -185,7 +213,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_AlreadyProcessed() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Empty(n.message)
} }
@ -200,7 +231,12 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Empty(n.message)
} }
@ -218,7 +254,13 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
} }
@ -235,7 +277,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent() {
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
} }
@ -252,7 +297,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent_NoLocalizer
p.Process(1, c) p.Process(1, c)
c.AssertExpectations(t.T()) 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) t.Assert().Equal(`default error`, n.message)
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/retailcrm/mg-transport-core/v2/core/logger" "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. // JobFunc is empty func which should be executed in a parallel goroutine.
@ -46,7 +47,7 @@ type Job struct {
// SetLogger(logger). // SetLogger(logger).
// SetLogging(false) // SetLogging(false)
// _ = manager.RegisterJob("updateTokens", &Job{ // _ = manager.RegisterJob("updateTokens", &Job{
// Command: func(log logger.Logger) error { // Command: func(log logger.LoggerOld) error {
// // logic goes here... // // logic goes here...
// logger.Info("All tokens were updated successfully") // logger.Info("All tokens were updated successfully")
// return nil // 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) err := j.Command(log)
if err != nil && j.ErrorHandler != nil { if err != nil && j.ErrorHandler != nil {
j.ErrorHandler(name, err, log) j.ErrorHandler(name, err, log)
@ -153,7 +155,7 @@ func NewJobManager() *JobManager {
func DefaultJobErrorHandler() JobErrorHandler { func DefaultJobErrorHandler() JobErrorHandler {
return func(name string, err error, log logger.Logger) { return func(name string, err error, log logger.Logger) {
if err != nil && name != "" { 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 { func DefaultJobPanicHandler() JobPanicHandler {
return func(name string, recoverValue interface{}, log logger.Logger) { return func(name string, recoverValue interface{}, log logger.Logger) {
if recoverValue != nil && name != "" { 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))
} }
} }
} }

View File

@ -9,10 +9,11 @@ import (
"testing" "testing"
"time" "time"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/logger"
) )
@ -25,7 +26,7 @@ type JobTest struct {
executeErr chan error executeErr chan error
panicValue chan interface{} panicValue chan interface{}
lastLog string lastLog string
lastMsgLevel logging.Level lastMsgLevel zapcore.Level
syncBool bool syncBool bool
} }
@ -36,68 +37,94 @@ type JobManagerTest struct {
syncRunnerFlag bool 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 { type callbackLogger struct {
fields []zap.Field
fn callbackLoggerFunc fn callbackLoggerFunc
} }
func (n *callbackLogger) Fatal(args ...interface{}) { func (n *callbackLogger) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
n.fn(logging.CRITICAL, "", args...) return &zapcore.CheckedEntry{}
} }
func (n *callbackLogger) Fatalf(format string, args ...interface{}) { func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) {
n.fn(logging.CRITICAL, format, args...) n.fn(zap.PanicLevel, msg, fields...)
} }
func (n *callbackLogger) Panic(args ...interface{}) { func (n *callbackLogger) Panic(msg string, fields ...zap.Field) {
n.fn(logging.CRITICAL, "", args...) n.fn(zap.PanicLevel, msg, fields...)
}
func (n *callbackLogger) Panicf(format string, args ...interface{}) {
n.fn(logging.CRITICAL, format, args...)
} }
func (n *callbackLogger) Critical(args ...interface{}) { func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) {
n.fn(logging.CRITICAL, "", args...) n.fn(zap.FatalLevel, msg, fields...)
} }
func (n *callbackLogger) Criticalf(format string, args ...interface{}) { func (n *callbackLogger) Sync() error {
n.fn(logging.CRITICAL, format, args...) return nil
} }
func (n *callbackLogger) Error(args ...interface{}) { func (n *callbackLogger) clone() *callbackLogger {
n.fn(logging.ERROR, "", args...) return &callbackLogger{fn: n.fn, fields: n.fields}
}
func (n *callbackLogger) Errorf(format string, args ...interface{}) {
n.fn(logging.ERROR, format, args...)
} }
func (n *callbackLogger) Warning(args ...interface{}) { func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger {
n.fn(logging.WARNING, "", args...) cl := &callbackLogger{fn: n.fn, fields: n.fields}
existing := cl.fields
if len(existing) == 0 {
cl.fields = fields
return cl
} }
func (n *callbackLogger) Warningf(format string, args ...interface{}) { cl.fields = append(existing, fields...) // nolint:gocritic
n.fn(logging.WARNING, format, args...) return cl
} }
func (n *callbackLogger) Notice(args ...interface{}) { func (n *callbackLogger) Level() zapcore.Level {
n.fn(logging.NOTICE, "", args...) return zapcore.DebugLevel
}
func (n *callbackLogger) Noticef(format string, args ...interface{}) {
n.fn(logging.NOTICE, format, args...)
} }
func (n *callbackLogger) Info(args ...interface{}) { func (n *callbackLogger) With(args ...zap.Field) logger.Logger {
n.fn(logging.INFO, "", args...) return n.cloneWithFields(args)
}
func (n *callbackLogger) Infof(format string, args ...interface{}) {
n.fn(logging.INFO, format, args...)
} }
func (n *callbackLogger) Debug(args ...interface{}) { func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger {
n.fn(logging.DEBUG, "", args...) 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) { func TestJob(t *testing.T) {
@ -115,9 +142,9 @@ func TestDefaultJobErrorHandler(t *testing.T) {
fn := DefaultJobErrorHandler() fn := DefaultJobErrorHandler()
require.NotNil(t, fn) 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) 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() fn := DefaultJobPanicHandler()
require.NotNil(t, fn) 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) 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 { 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 == "" { if format == "" {
var sb strings.Builder var sb strings.Builder
sb.Grow(3 * len(args)) // nolint:gomnd sb.Grow(3 * len(args)) // nolint:gomnd
@ -159,7 +186,12 @@ func (t *JobTest) testLogger() logger.Logger {
format = strings.TrimRight(sb.String(), " ") 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 t.lastMsgLevel = level
}} }}
} }
@ -256,11 +288,11 @@ func (t *JobTest) oncePanicJob() {
} }
func (t *JobTest) regularJob() { func (t *JobTest) regularJob() {
rand.Seed(time.Now().UnixNano()) r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec
t.job = &Job{ t.job = &Job{
Command: func(log logger.Logger) error { Command: func(log logger.Logger) error {
t.executedChan <- true t.executedChan <- true
t.randomNumber <- rand.Int() // nolint:gosec t.randomNumber <- r.Int() // nolint:gosec
return nil return nil
}, },
ErrorHandler: t.testErrorHandler(), ErrorHandler: t.testErrorHandler(),
@ -271,7 +303,6 @@ func (t *JobTest) regularJob() {
} }
func (t *JobTest) regularSyncJob() { func (t *JobTest) regularSyncJob() {
rand.Seed(time.Now().UnixNano())
t.job = &Job{ t.job = &Job{
Command: func(log logger.Logger) error { Command: func(log logger.Logger) error {
t.syncBool = true t.syncBool = true
@ -404,11 +435,11 @@ func (t *JobManagerTest) WaitForJob() bool {
func (t *JobManagerTest) Test_SetLogger() { func (t *JobManagerTest) Test_SetLogger() {
t.manager.logger = nil t.manager.logger = nil
t.manager.SetLogger(logger.NewStandard("test", logging.ERROR, logger.DefaultLogFormatter())) t.manager.SetLogger(logger.NewDefault("json", true))
assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger) assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
t.manager.SetLogger(nil) 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() { func (t *JobManagerTest) Test_SetLogging() {

View File

@ -3,7 +3,7 @@ package core
import ( import (
"html/template" "html/template"
"io/fs" "io/fs"
"io/ioutil" "os"
"path" "path"
"sync" "sync"
@ -90,6 +90,7 @@ type CloneableLocalizer interface {
// NewLocalizer returns localizer instance with specified parameters. // NewLocalizer returns localizer instance with specified parameters.
// Usage: // Usage:
//
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations") // NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface { func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface {
localizer := &Localizer{ localizer := &Localizer{
@ -106,7 +107,9 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat
// NewLocalizerFS returns localizer instance with specified parameters. // NewLocalizerFS returns localizer instance with specified parameters.
// Usage: // Usage:
//
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS) // NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS)
//
// TODO This code should be covered with tests. // TODO This code should be covered with tests.
func NewLocalizerFS( func NewLocalizerFS(
locale language.Tag, matcher language.Matcher, translationsFS fs.FS, locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
@ -161,6 +164,7 @@ func (l *Localizer) Clone() CloneableLocalizer {
// Because of that all Localizer instances from this middleware will share *same* mutex. This mutex is used to wrap // 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). // i18n.Bundle methods (those aren't goroutine-safe to use).
// Usage: // Usage:
//
// engine := gin.New() // engine := gin.New()
// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations") // localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations")
// engine.Use(localizer.LocalizationMiddleware()) // engine.Use(localizer.LocalizationMiddleware())
@ -174,15 +178,21 @@ func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
// LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans // LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans
// Usage in code: // Usage in code:
//
// engine := gin.New() // engine := gin.New()
// engine.FuncMap = localizer.LocalizationFuncMap() // engine.FuncMap = localizer.LocalizationFuncMap()
//
// or (with multitemplate) // or (with multitemplate)
//
// renderer := multitemplate.NewRenderer() // renderer := multitemplate.NewRenderer()
// funcMap := localizer.LocalizationFuncMap() // funcMap := localizer.LocalizationFuncMap()
// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html") // renderer.AddFromFilesFuncs("index", funcMap, "template/index.html")
//
// funcMap must be passed for every .AddFromFilesFuncs call // funcMap must be passed for every .AddFromFilesFuncs call
// Usage in templates: // Usage in templates:
//
// <p class="info">{{"need_login_msg" | trans}} // <p class="info">{{"need_login_msg" | trans}}
//
// You can borrow FuncMap from this method and add your functions to it. // You can borrow FuncMap from this method and add your functions to it.
func (l *Localizer) LocalizationFuncMap() template.FuncMap { func (l *Localizer) LocalizationFuncMap() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
@ -196,7 +206,7 @@ func (l *Localizer) LocalizationFuncMap() template.FuncMap {
parts = append(parts, "") 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 { for i := 0; i < len(parts)-1; i += 2 {
partsMap[parts[i]] = parts[i+1] 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. // LoadTranslations will load all translation files from translations directory.
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error { func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
files, err := ioutil.ReadDir(l.TranslationsPath) files, err := os.ReadDir(l.TranslationsPath)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,6 @@
package core package core
import ( import (
"io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -40,7 +39,7 @@ func createTestLangFiles(t *testing.T) {
} }
if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) { 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) require.Nil(t, err)
} }
} }
@ -103,7 +102,6 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() { func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
var wg sync.WaitGroup var wg sync.WaitGroup
rand.Seed(time.Now().UnixNano())
l.localizer.Preload(DefaultLanguages) l.localizer.Preload(DefaultLanguages)
langMsgMap := map[language.Tag]string{ langMsgMap := map[language.Tag]string{
language.English: "Test message", language.English: "Test message",
@ -120,9 +118,11 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
wg.Add(1) wg.Add(1)
i := i
go func(m map[language.Tag]string, wg *sync.WaitGroup) { 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 var tag language.Tag
switch rand.Intn(3-1) + 1 { // nolint:gosec switch r.Intn(3-1) + 1 { // nolint:gosec
case 1: case 1:
tag = language.English tag = language.English
case 2: case 2:

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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...))
}
}

View File

@ -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"])
}

94
core/logger/attrs.go Normal file
View File

@ -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, "<nil>")
}
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))
}
}

156
core/logger/attrs_test.go Normal file
View File

@ -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: "<nil>",
},
{
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)
}

View File

@ -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
}

29
core/logger/bufferpool.go Normal file
View File

@ -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
)

100
core/logger/default.go Normal file
View File

@ -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
}

View File

@ -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])
}

37
core/logger/gin.go Normal file
View File

@ -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()),
)
}
}

38
core/logger/gin_test.go Normal file
View File

@ -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"])
}

View File

@ -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:])
}

View File

@ -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...)
}

View File

@ -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")
}

View File

@ -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...)
}

View File

@ -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"])
}

62
core/logger/nil.go Normal file
View File

@ -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
}

View File

@ -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{}
}

View File

@ -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("")
})
}

57
core/logger/pool.go Normal file
View File

@ -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)
}

19
core/logger/pool_test.go Normal file
View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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}
}

View File

@ -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])
}

102
core/logger/zap.go Normal file
View File

@ -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: " ",
}
}

View File

@ -6,7 +6,6 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/base64" "encoding/base64"
"io" "io"
"io/ioutil"
"math/rand" "math/rand"
"time" "time"
@ -57,10 +56,10 @@ var DefaultCSRFTokenGetter = func(c *gin.Context) string {
} else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 { } else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 {
return t return t
} else if c.Request.Body != nil { } else if c.Request.Body != nil {
data, _ := ioutil.ReadAll(c.Request.Body) data, _ := io.ReadAll(c.Request.Body)
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data)) c.Request.Body = io.NopCloser(bytes.NewReader(data))
t := r.FormValue("csrf_token") 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 { if len(t) > 0 {
return t return t
@ -91,6 +90,7 @@ type CSRF struct {
// csrfTokenGetter will be used to obtain token. // csrfTokenGetter will be used to obtain token.
// //
// Usage (with random salt): // Usage (with random salt):
//
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) { // core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) {
// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"}) // c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"})
// }, core.DefaultCSRFTokenGetter) // }, core.DefaultCSRFTokenGetter)
@ -99,9 +99,11 @@ type CSRF struct {
// - don't forget to restore Body data! // - don't forget to restore Body data!
// //
// Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that: // Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that:
//
// if t := r.FormValue("csrf_token"); len(t) > 0 { // if t := r.FormValue("csrf_token"); len(t) > 0 {
// return t // return t
// } // }
//
// will close body - and all next middlewares won't be able to read body at all! // 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. // 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. // pseudoRandomString generates pseudo-random string with specified length.
func (x *CSRF) pseudoRandomString(length int) string { 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) data := make([]byte, length)
for i := 0; i < length; i++ { // it is supposed to use pseudo-random data. 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) return string(data)
@ -209,6 +211,7 @@ func (x *CSRF) CSRFFromContext(c *gin.Context) string {
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token // GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
// Usage: // Usage:
//
// engine := gin.New() // engine := gin.New()
// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer) // csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer)
// engine.Use(csrf.GenerateCSRFMiddleware()) // engine.Use(csrf.GenerateCSRFMiddleware())
@ -216,8 +219,8 @@ func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
session, _ := x.store.Get(c.Request, x.sessionName) 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
if i, ok := i.(string); !ok || i == "" { if i, ok := i.(string); !ok || i == "" { // nolint:nestif
if x.fillToken(session, c) != nil { if x.fillToken(session, c) != nil {
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession) x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
c.Abort() c.Abort()
@ -243,6 +246,7 @@ func (x *CSRF) fillToken(s *sessions.Session, c *gin.Context) error {
// VerifyCSRFMiddleware verifies CSRF token // VerifyCSRFMiddleware verifies CSRF token
// Usage: // Usage:
//
// engine := gin.New() // engine := gin.New()
// engine.Use(csrf.VerifyCSRFMiddleware()) // engine.Use(csrf.VerifyCSRFMiddleware())
func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc { func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
@ -254,9 +258,9 @@ func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
var token string var token string
session, _ := x.store.Get(c.Request, x.sessionName) 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 var v string
if v, ok = i.(string); !ok || v == "" { if v, ok = i.(string); !ok || v == "" { // nolint:nestif
if !ok { if !ok {
x.abortFunc(c, CSRFErrorIncorrectTokenType) x.abortFunc(c, CSRFErrorIncorrectTokenType)
} else if v == "" { } else if v == "" {

View File

@ -3,7 +3,6 @@ package middleware
import ( import (
"bytes" "bytes"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -32,7 +31,7 @@ func TestCSRF_DefaultCSRFTokenGetter_Empty(t *testing.T) {
URL: &url.URL{ URL: &url.URL{
RawQuery: "", RawQuery: "",
}, },
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), Body: io.NopCloser(bytes.NewReader([]byte(""))),
}} }}
assert.Empty(t, DefaultCSRFTokenGetter(c)) assert.Empty(t, DefaultCSRFTokenGetter(c))
@ -85,14 +84,14 @@ func TestCSRF_DefaultCSRFTokenGetter_Form(t *testing.T) {
RawQuery: "", RawQuery: "",
}, },
Header: headers, Header: headers,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), Body: io.NopCloser(bytes.NewReader([]byte(""))),
}} }}
c.Request.PostForm = url.Values{"csrf_token": {"token"}} c.Request.PostForm = url.Values{"csrf_token": {"token"}}
assert.NotEmpty(t, DefaultCSRFTokenGetter(c)) assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
assert.Equal(t, "token", DefaultCSRFTokenGetter(c)) assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
_, err := ioutil.ReadAll(c.Request.Body) _, err := io.ReadAll(c.Request.Body)
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
"go.uber.org/zap"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/config"
@ -64,14 +65,13 @@ func NewModuleFeaturesUploader(
awsConfig.WithCredentialsProvider(customProvider), awsConfig.WithCredentialsProvider(customProvider),
) )
if err != nil { if err != nil {
log.Fatal(err) log.Error("cannot load S3 configuration", logger.Err(err))
return nil return nil
} }
client := manager.NewUploader(s3.NewFromConfig(cfg)) client := manager.NewUploader(s3.NewFromConfig(cfg))
if err != nil { if err != nil {
log.Fatal(err) log.Error("cannot load S3 configuration", logger.Err(err))
return nil return nil
} }
@ -87,18 +87,18 @@ func NewModuleFeaturesUploader(
} }
func (s *ModuleFeaturesUploader) Upload() { func (s *ModuleFeaturesUploader) Upload() {
s.log.Debugf("upload module features started...") s.log.Debug("upload module features started...")
content, err := os.ReadFile(s.featuresFilename) content, err := os.ReadFile(s.featuresFilename)
if err != nil { 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 return
} }
for _, lang := range languages { for _, lang := range languages {
translated, err := s.translate(content, lang) translated, err := s.translate(content, lang)
if err != nil { 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 continue
} }
@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() {
resp, err := s.uploadFile(html, lang.String()) resp, err := s.uploadFile(html, lang.String())
if err != nil { 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 continue
} }
@ -114,7 +114,7 @@ func (s *ModuleFeaturesUploader) Upload() {
} }
fmt.Println() 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) { func (s *ModuleFeaturesUploader) translate(content []byte, lang language.Tag) ([]byte, error) {

View File

@ -1,19 +1,18 @@
package core package core
import ( import (
"bytes"
"context" "context"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"os" "os"
"testing" "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/aws/aws-sdk-go-v2/service/s3"
"github.com/op/go-logging"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/config"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
) )
type ModuleFeaturesUploaderTest struct { type ModuleFeaturesUploaderTest struct {
@ -36,8 +35,7 @@ func (t *ModuleFeaturesUploaderTest) TearDownSuite() {
} }
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() { func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() {
logs := &bytes.Buffer{} log := testutil.NewBufferedLogger()
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt") uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
@ -50,8 +48,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeature
} }
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() { func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
logs := &bytes.Buffer{} log := testutil.NewBufferedLogger()
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt") uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
content := "test content " + t.localizer.GetLocalizedMessage("message") content := "test content " + t.localizer.GetLocalizedMessage("message")
@ -62,8 +59,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
} }
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() { func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() {
logs := &bytes.Buffer{} log := testutil.NewBufferedLogger()
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"} conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md") uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md")
content := "test content" content := "test content"

View File

@ -15,6 +15,7 @@ import (
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
sentrygin "github.com/getsentry/sentry-go/gin" sentrygin "github.com/getsentry/sentry-go/gin"
"github.com/pkg/errors" "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/logger"
"github.com/retailcrm/mg-transport-core/v2/core/stacktrace" "github.com/retailcrm/mg-transport-core/v2/core/stacktrace"
@ -22,8 +23,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// reset is borrowed directly from the gin. const recoveryMiddlewareSkipFrames = 3
const reset = "\033[0m"
// ErrorHandlerFunc will handle errors. // ErrorHandlerFunc will handle errors.
type ErrorHandlerFunc func(recovery interface{}, c *gin.Context) 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 // 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. // 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 item, ok := c.Get("logger"); ok {
if accountLogger, ok := item.(logger.AccountLogger); ok { if ctxLogger, ok := item.(logger.Logger); ok {
return accountLogger return ctxLogger
} }
} }
connectionID := "{no connection ID}" connectionID := "{no connection ID}"
accountID := "{no account ID}" accountID := "{no account ID}"
if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" { 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) { 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. // tagsSetterMiddleware sets event tags into Sentry events.
@ -201,13 +201,13 @@ func (s *Sentry) exceptionCaptureMiddleware() gin.HandlerFunc { // nolint:gocogn
for _, err := range publicErrors { for _, err := range publicErrors {
messages[index] = err.Error() messages[index] = err.Error()
s.CaptureException(c, err) s.CaptureException(c, err)
l.Error(err) l.Error(err.Error())
index++ index++
} }
for _, err := range privateErrors { for _, err := range privateErrors {
s.CaptureException(c, err) s.CaptureException(c, err)
l.Error(err) l.Error(err.Error())
} }
if privateLen > 0 || recovery != nil { if privateLen > 0 || recovery != nil {
@ -250,8 +250,9 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
} }
} }
if l != nil { if l != nil {
stack := stacktrace.FormattedStack(3, l.Prefix()+" ") // TODO: Check if we can output stacktraces with prefix data like before if we really need it.
formattedErr := fmt.Sprintf("%s %s", l.Prefix(), err) stack := stacktrace.FormattedStack(recoveryMiddlewareSkipFrames, "trace: ")
formattedErr := logger.Err(err)
httpRequest, _ := httputil.DumpRequest(c.Request, false) httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n") headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers { for idx, header := range headers {
@ -259,18 +260,17 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
if current[0] == "Authorization" { if current[0] == "Authorization" {
headers[idx] = current[0] + ": *" 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 { switch {
case brokenPipe: case brokenPipe:
l.Errorf("%s\n%s%s", formattedErr, headersToStr, reset) l.Error("error", formattedErr, headersToStr)
case gin.IsDebugging(): case gin.IsDebugging():
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", l.Error("[Recovery] panic recovered", headersToStr, formattedErr, formattedStack)
timeFormat(time.Now()), headersToStr, formattedErr, stack, reset)
default: default:
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s%s", l.Error("[Recovery] panic recovered", formattedErr, formattedStack)
timeFormat(time.Now()), formattedErr, stack, reset)
} }
} }
if brokenPipe { if brokenPipe {

View File

@ -2,7 +2,6 @@ package core
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync" "sync"
@ -32,12 +31,12 @@ type sentryMockTransport struct {
sending sync.RWMutex sending sync.RWMutex
} }
func (s *sentryMockTransport) Flush(timeout time.Duration) bool { func (s *sentryMockTransport) Flush(_ time.Duration) bool {
// noop // noop
return true return true
} }
func (s *sentryMockTransport) Configure(options sentry.ClientOptions) { func (s *sentryMockTransport) Configure(_ sentry.ClientOptions) {
// noop // noop
} }
@ -278,7 +277,7 @@ func (s *SentryTest) TestSentry_CaptureException() {
func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() { func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() {
ctx, _ := s.ginCtxMock() ctx, _ := s.ginCtxMock()
log := logger.DecorateForAccount(testutil.NewBufferedLogger(), "component", "conn", "acc") log := testutil.NewBufferedLogger().ForHandler("component").ForConnection("conn").ForAccount("acc")
ctx.Set("logger", log) ctx.Set("logger", log)
s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx)) 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(log)
s.Assert().NotNil(logNoConfig) s.Assert().NotNil(logNoConfig)
s.Assert().Implements((*logger.AccountLogger)(nil), log) s.Assert().Implements((*logger.Logger)(nil), log)
s.Assert().Implements((*logger.AccountLogger)(nil), logNoConfig) s.Assert().Implements((*logger.Logger)(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())
} }
func (s *SentryTest) TestSentry_MiddlewaresError() { func (s *SentryTest) TestSentry_MiddlewaresError() {

View File

@ -18,7 +18,7 @@ func TestError(t *testing.T) {
func (t *ErrorTest) TestAppendToError() { func (t *ErrorTest) TestAppendToError() {
err := errors.New("test error") err := errors.New("test error")
_, ok := err.(StackTraced) _, ok := err.(StackTraced) // nolint:errorlint
t.Assert().False(ok) t.Assert().False(ok)
@ -28,16 +28,16 @@ func (t *ErrorTest) TestAppendToError() {
t.Assert().Nil(AppendToError(nil)) t.Assert().Nil(AppendToError(nil))
t.Assert().Implements((*StackTraced)(nil), withTrace) t.Assert().Implements((*StackTraced)(nil), withTrace)
t.Assert().Implements((*StackTraced)(nil), twiceTrace) 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() { func (t *ErrorTest) TestCauseUnwrap() {
err := errors.New("test error") err := errors.New("test error")
wrapped := AppendToError(err) 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(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() { func (t *ErrorTest) TestFormat() {

View File

@ -5,7 +5,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil" "os"
"path" "path"
"runtime" "runtime"
"strconv" "strconv"
@ -78,19 +78,19 @@ func (f Frame) Format(s fmt.State, verb rune) {
case 's': case 's':
switch { switch {
case s.Flag('+'): case s.Flag('+'):
io.WriteString(s, f.name()) _, _ = io.WriteString(s, f.name())
io.WriteString(s, "\n\t") _, _ = io.WriteString(s, "\n\t")
io.WriteString(s, f.file()) _, _ = io.WriteString(s, f.file())
default: default:
io.WriteString(s, path.Base(f.file())) _, _ = io.WriteString(s, path.Base(f.file()))
} }
case 'd': case 'd':
io.WriteString(s, strconv.Itoa(f.line())) _, _ = io.WriteString(s, strconv.Itoa(f.line()))
case 'n': case 'n':
io.WriteString(s, funcname(f.name())) _, _ = io.WriteString(s, funcname(f.name()))
case 'v': case 'v':
f.Format(s, 's') f.Format(s, 's')
io.WriteString(s, ":") _, _ = io.WriteString(s, ":")
f.Format(s, 'd') f.Format(s, 'd')
} }
} }
@ -122,11 +122,11 @@ func (st StackTrace) Format(s fmt.State, verb rune) {
switch { switch {
case s.Flag('+'): case s.Flag('+'):
for _, f := range st { for _, f := range st {
io.WriteString(s, "\n") _, _ = io.WriteString(s, "\n")
f.Format(s, verb) f.Format(s, verb)
} }
case s.Flag('#'): case s.Flag('#'):
fmt.Fprintf(s, "%#v", []Frame(st)) _, _ = fmt.Fprintf(s, "%#v", []Frame(st))
default: default:
st.formatSlice(s, verb) 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 // formatSlice will format this StackTrace into the given buffer as a slice of
// Frame, only valid when called with '%s' or '%v'. // Frame, only valid when called with '%s' or '%v'.
func (st StackTrace) formatSlice(s fmt.State, verb rune) { func (st StackTrace) formatSlice(s fmt.State, verb rune) {
io.WriteString(s, "[") _, _ = io.WriteString(s, "[")
for i, f := range st { for i, f := range st {
if i > 0 { if i > 0 {
io.WriteString(s, " ") _, _ = io.WriteString(s, " ")
} }
f.Format(s, verb) f.Format(s, verb)
} }
io.WriteString(s, "]") _, _ = io.WriteString(s, "]")
} }
// stack represents a stack of program counters. // 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('+') { if verb == 'v' && st.Flag('+') {
for _, pc := range *s { for _, pc := range *s {
f := Frame(pc) 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 break
} }
// Print this much at least. If we can't find the source, it won't show. // 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 { if file != lastFile {
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
continue continue
} }
lines = bytes.Split(data, []byte{'\n'}) lines = bytes.Split(data, []byte{'\n'})
lastFile = file 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() return buf.Bytes()
} }

View File

@ -3,7 +3,6 @@ package core
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil"
"os" "os"
"path" "path"
"testing" "testing"
@ -37,8 +36,8 @@ func (t *TemplateTest) initTestData() {
require.Nil(t.T(), err) require.Nil(t.T(), err)
data1 := []byte(`data {{template "body" .}}`) data1 := []byte(`data {{template "body" .}}`)
data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`) data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`)
err1 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm) err1 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm)
err2 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm) err2 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm)
require.Nil(t.T(), err1) require.Nil(t.T(), err1)
require.Nil(t.T(), err2) require.Nil(t.T(), err2)
} }

View File

@ -18,6 +18,7 @@ import (
// because AsError() returns nil if there are no errors in the list. // because AsError() returns nil if there are no errors in the list.
// //
// Example: // Example:
//
// err := errorutil.NewCollector(). // err := errorutil.NewCollector().
// Do(errors.New("error 1")). // Do(errors.New("error 1")).
// Do(errors.New("error 2"), errors.New("error 3")) // Do(errors.New("error 2"), errors.New("error 3"))
@ -25,11 +26,13 @@ import (
// fmt.Println(err) // fmt.Println(err)
// //
// This code will produce something like this: // This code will produce something like this:
//
// #1 err at /home/user/main.go:62: error 1 // #1 err at /home/user/main.go:62: error 1
// #2 err at /home/user/main.go:63: error 2 // #2 err at /home/user/main.go:63: error 2
// #3 err at /home/user/main.go:64: error 3 // #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: // You can also iterate over the error to use their data instead of using predefined message:
//
// err := errorutil.NewCollector(). // err := errorutil.NewCollector().
// Do(errors.New("error 1")). // Do(errors.New("error 1")).
// Do(errors.New("error 2"), errors.New("error 3")) // Do(errors.New("error 2"), errors.New("error 3"))
@ -39,11 +42,13 @@ import (
// } // }
// //
// This code will produce output that looks like this: // 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 0
// Error at /home/user/main.go:164: error 1 // 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 2
// //
// Example with GORM migration (Collector is returned as an error here). // Example with GORM migration (Collector is returned as an error here).
//
// return errorutil.NewCollector().Do( // return errorutil.NewCollector().Do(
// db.CreateTable(models.Account{}, models.Connection{}).Error, // db.CreateTable(models.Account{}, models.Connection{}).Error,
// db.Table("account").AddUniqueIndex("account_key", "channel").Error, // db.Table("account").AddUniqueIndex("account_key", "channel").Error,

View File

@ -14,6 +14,7 @@ type ListResponse struct {
// GetErrorResponse returns ErrorResponse with specified status code // GetErrorResponse returns ErrorResponse with specified status code
// Usage (with gin): // 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{}) { func GetErrorResponse(statusCode int, err string) (int, interface{}) {
return statusCode, Response{ return statusCode, Response{
@ -23,6 +24,7 @@ func GetErrorResponse(statusCode int, err string) (int, interface{}) {
// BadRequest returns ErrorResponse with code 400 // BadRequest returns ErrorResponse with code 400
// Usage (with gin): // Usage (with gin):
//
// context.JSON(BadRequest("invalid data")) // context.JSON(BadRequest("invalid data"))
func BadRequest(err string) (int, interface{}) { func BadRequest(err string) (int, interface{}) {
return GetErrorResponse(http.StatusBadRequest, err) return GetErrorResponse(http.StatusBadRequest, err)
@ -30,6 +32,7 @@ func BadRequest(err string) (int, interface{}) {
// Unauthorized returns ErrorResponse with code 401 // Unauthorized returns ErrorResponse with code 401
// Usage (with gin): // Usage (with gin):
//
// context.JSON(Unauthorized("invalid credentials")) // context.JSON(Unauthorized("invalid credentials"))
func Unauthorized(err string) (int, interface{}) { func Unauthorized(err string) (int, interface{}) {
return GetErrorResponse(http.StatusUnauthorized, err) return GetErrorResponse(http.StatusUnauthorized, err)
@ -37,6 +40,7 @@ func Unauthorized(err string) (int, interface{}) {
// Forbidden returns ErrorResponse with code 403 // Forbidden returns ErrorResponse with code 403
// Usage (with gin): // Usage (with gin):
//
// context.JSON(Forbidden("forbidden")) // context.JSON(Forbidden("forbidden"))
func Forbidden(err string) (int, interface{}) { func Forbidden(err string) (int, interface{}) {
return GetErrorResponse(http.StatusForbidden, err) return GetErrorResponse(http.StatusForbidden, err)
@ -44,6 +48,7 @@ func Forbidden(err string) (int, interface{}) {
// InternalServerError returns ErrorResponse with code 500 // InternalServerError returns ErrorResponse with code 500
// Usage (with gin): // Usage (with gin):
//
// context.JSON(BadRequest("invalid data")) // context.JSON(BadRequest("invalid data"))
func InternalServerError(err string) (int, interface{}) { func InternalServerError(err string) (int, interface{}) {
return GetErrorResponse(http.StatusInternalServerError, err) return GetErrorResponse(http.StatusInternalServerError, err)

View File

@ -10,10 +10,10 @@ import (
"net/url" "net/url"
"time" "time"
"go.uber.org/zap"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/config"
"github.com/retailcrm/mg-transport-core/v2/core/logger" "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") return errors.New("dialer must be built first")
} }
if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { // nolint:nestif
b.logf("Mock address is \"%s\"\n", net.JoinHostPort(b.mockHost, b.mockPort)) b.log("Mock address has been set", zap.String("address", net.JoinHostPort(b.mockHost, b.mockPort)))
b.logf("Mocked domains: ") b.log("Mocked domains: ")
for _, domain := range b.mockedDomains { for _, domain := range b.mockedDomains {
b.logf(" - %s\n", domain) b.log(fmt.Sprintf(" - %s\n", domain))
} }
b.httpTransport.Proxy = nil b.httpTransport.Proxy = nil
@ -259,7 +259,7 @@ func (b *HTTPClientBuilder) buildMocks() error {
addr = net.JoinHostPort(b.mockHost, b.mockPort) 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 return nil
} }
// logf prints logs via Engine or via fmt.Printf. // log prints logs via Engine or via fmt.Println.
func (b *HTTPClientBuilder) logf(format string, args ...interface{}) { func (b *HTTPClientBuilder) log(msg string, args ...interface{}) {
if b.logging { if b.logging {
if b.logger != nil { if b.logger != nil {
b.logger.Infof(format, args...) b.logger.Info(msg, logger.AnyZapFields(args)...)
} else { } else {
fmt.Printf(format, args...) fmt.Println(append([]any{msg}, args...))
} }
} }
} }

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -16,7 +15,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -137,14 +135,14 @@ func (t *HTTPClientBuilderTest) Test_buildMocks() {
} }
func (t *HTTPClientBuilderTest) Test_WithLogger() { func (t *HTTPClientBuilderTest) Test_WithLogger() {
logger := logger.NewStandard("telegram", logging.ERROR, logger.DefaultLogFormatter())
builder := NewHTTPClientBuilder() builder := NewHTTPClientBuilder()
require.Nil(t.T(), builder.logger) require.Nil(t.T(), builder.logger)
builder.WithLogger(nil) builder.WithLogger(nil)
assert.Nil(t.T(), builder.logger) assert.Nil(t.T(), builder.logger)
builder.WithLogger(logger) log := logger.NewDefault("json", true)
builder.WithLogger(log)
assert.NotNil(t.T(), builder.logger) assert.NotNil(t.T(), builder.logger)
} }
@ -153,7 +151,7 @@ func (t *HTTPClientBuilderTest) Test_logf() {
assert.Nil(t.T(), recover()) assert.Nil(t.T(), recover())
}() }()
t.builder.logf("test %s", "string") t.builder.log(fmt.Sprintf("test %s", "string"))
} }
func (t *HTTPClientBuilderTest) Test_Build() { func (t *HTTPClientBuilderTest) Test_Build() {
@ -210,6 +208,7 @@ x5porosgI2RgOTTwmiYOcYQTS2650jYydHhK16Gu2b3UKernO16mAWXNDWfvS2bk
nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI
saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
// nolint:gosec
keyFileData := `-----BEGIN RSA PRIVATE KEY----- keyFileData := `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC
5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn 5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn
@ -238,9 +237,9 @@ weywTxDl/OD5ybNkZIRKsIXciFYG1VCGO2HNGN9qJcV+nJ63kyrIBauwUkuEhiN5
uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc= uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
-----END RSA PRIVATE KEY-----` -----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") 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") require.NoError(t.T(), err, "cannot create temp key file")
_, err = certFile.WriteString(certFileData) _, err = certFile.WriteString(certFileData)
@ -253,7 +252,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file") errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file")
mux := &http.ServeMux{} 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) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, "ok") _, _ = io.WriteString(w, "ok")
@ -308,7 +307,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
defer resp.Body.Close() 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") require.NoError(t.T(), err, "error while reading body")
assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code") assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code")

View File

@ -1,15 +1,13 @@
package testutil package testutil
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"sync"
"github.com/op/go-logging"
"github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
// ReadBuffer is implemented by the BufferLogger. // ReadBuffer is implemented by the BufferLogger.
@ -28,120 +26,75 @@ type BufferedLogger interface {
} }
// BufferLogger is an implementation of the BufferedLogger. // 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 { type BufferLogger struct {
buf bytes.Buffer logger.Default
rw sync.RWMutex buf LockableBuffer
} }
// NewBufferedLogger returns new BufferedLogger instance. // NewBufferedLogger returns new BufferedLogger instance.
func NewBufferedLogger() BufferedLogger { 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. // Read bytes from the logger buffer. io.Reader implementation.
func (l *BufferLogger) Read(p []byte) (n int, err error) { func (l *BufferLogger) Read(p []byte) (n int, err error) {
defer l.rw.RUnlock()
l.rw.RLock()
return l.buf.Read(p) return l.buf.Read(p)
} }
// String contents of the logger buffer. fmt.Stringer implementation. // String contents of the logger buffer. fmt.Stringer implementation.
func (l *BufferLogger) String() string { func (l *BufferLogger) String() string {
defer l.rw.RUnlock()
l.rw.RLock()
return l.buf.String() return l.buf.String()
} }
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents. // Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
func (l *BufferLogger) Bytes() []byte { func (l *BufferLogger) Bytes() []byte {
defer l.rw.RUnlock()
l.rw.RLock()
return l.buf.Bytes() return l.buf.Bytes()
} }
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents. // Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
func (l *BufferLogger) Reset() { func (l *BufferLogger) Reset() {
defer l.rw.Unlock()
l.rw.Lock()
l.buf.Reset() 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...)
}

View File

@ -4,7 +4,6 @@ import (
"io" "io"
"testing" "testing"
"github.com/op/go-logging"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -30,17 +29,20 @@ func (t *BufferLoggerTest) Test_Read() {
data, err := io.ReadAll(t.logger) data, err := io.ReadAll(t.logger)
t.Require().NoError(err) 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() { func (t *BufferLoggerTest) Test_Bytes() {
t.logger.Debug("test") 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() { func (t *BufferLoggerTest) Test_String() {
t.logger.Debug("test") 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() { func (t *BufferLoggerTest) TestRace() {

View File

@ -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.") t.Log("No body is present.")
} else { } else {
data, err := io.ReadAll(r.Body) data, err := io.ReadAll(r.Body)

View File

@ -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
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -3,7 +3,6 @@ package testutil
import ( import (
"errors" "errors"
"io/fs" "io/fs"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -63,8 +62,8 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
err error err error
) )
if info, err = os.Stat(fileName); err == nil { if info, err = os.Stat(fileName); err == nil { // nolint:nestif
if !info.IsDir() { if !info.IsDir() { // nolint:nestif
var ( var (
path string path string
source []byte source []byte
@ -75,7 +74,7 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
return dataMap, err return dataMap, err
} }
if source, err = ioutil.ReadFile(path); err != nil { if source, err = os.ReadFile(path); err != nil {
return dataMap, err return dataMap, err
} }

View File

@ -1,7 +1,6 @@
package testutil package testutil
import ( import (
"io/ioutil"
"os" "os"
"reflect" "reflect"
"sort" "sort"
@ -40,7 +39,7 @@ func (t *TranslationsExtractorTest) SetupSuite() {
data, _ := yaml.Marshal(translation) data, _ := yaml.Marshal(translation)
// It's not regular temporary file. Little hack in order to test translations extractor. // It's not regular temporary file. Little hack in order to test translations extractor.
// nolint:gosec // 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) require.NoError(t.T(), errWrite)
t.extractor = NewTranslationsExtractor("translate.{}.yml") t.extractor = NewTranslationsExtractor("translate.{}.yml")

View File

@ -1,11 +1,13 @@
package util package util
import ( import (
"bytes"
// nolint:gosec // nolint:gosec
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
@ -16,11 +18,12 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gin-gonic/gin"
retailcrm "github.com/retailcrm/api-client-go/v2" retailcrm "github.com/retailcrm/api-client-go/v2"
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
"github.com/retailcrm/mg-transport-core/v2/core/config" "github.com/retailcrm/mg-transport-core/v2/core/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/logger"
"github.com/retailcrm/mg-transport-core/v2/core/util/errorutil" "github.com/retailcrm/mg-transport-core/v2/core/util/errorutil"
@ -132,7 +135,7 @@ func (u *Utils) GenerateToken() string {
func (u *Utils) GetAPIClient( func (u *Utils) GetAPIClient(
url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) { url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) {
client := retailcrm.New(url, key). client := retailcrm.New(url, key).
WithLogger(retailcrm.DebugLoggerAdapter(u.Logger)) WithLogger(logger.APIClientAdapter(u.Logger))
client.Debug = u.IsDebug client.Debug = u.IsDebug
cr, status, err := client.APICredentials() cr, status, err := client.APICredentials()
@ -142,12 +145,12 @@ func (u *Utils) GetAPIClient(
if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 { if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 {
if len(credentials) == 0 || len(cr.Scopes) > 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) return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
} }
if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 { 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) return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
} }
} }
@ -281,3 +284,15 @@ func GetCurrencySymbol(code string) string {
func FormatCurrencyValue(value float32) string { func FormatCurrencyValue(value float32) string {
return fmt.Sprintf("%.2f", value) 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)
}

View File

@ -9,13 +9,14 @@ import (
"time" "time"
"github.com/h2non/gock" "github.com/h2non/gock"
"github.com/op/go-logging"
retailcrm "github.com/retailcrm/api-client-go/v2" 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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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/config"
"github.com/retailcrm/mg-transport-core/v2/core/logger" "github.com/retailcrm/mg-transport-core/v2/core/logger"
@ -38,7 +39,7 @@ func mgClient() *v1.MgClient {
} }
func (u *UtilsTest) SetupSuite() { func (u *UtilsTest) SetupSuite() {
logger := logger.NewStandard("code", logging.DEBUG, logger.DefaultLogFormatter()) logger := logger.NewDefault("json", true)
awsConfig := config.AWS{ awsConfig := config.AWS{
AccessKeyID: "access key id (will be removed)", AccessKeyID: "access key id (will be removed)",
SecretAccessKey: "secret access key", SecretAccessKey: "secret access key",

9
go.mod
View File

@ -1,6 +1,8 @@
module github.com/retailcrm/mg-transport-core/v2 module github.com/retailcrm/mg-transport-core/v2
go 1.18 go 1.22
toolchain go1.22.0
require ( require (
github.com/DATA-DOG/go-sqlmock v1.3.3 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/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.0
github.com/guregu/null/v5 v5.0.0
github.com/h2non/gock v1.2.0 github.com/h2non/gock v1.2.0
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/jinzhu/gorm v1.9.11 github.com/jinzhu/gorm v1.9.11
@ -24,10 +27,11 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/retailcrm/api-client-go/v2 v2.1.3 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/retailcrm/zabbix-metrics-collector v1.0.0
github.com/stretchr/testify v1.8.3 github.com/stretchr/testify v1.8.3
go.uber.org/atomic v1.10.0 go.uber.org/atomic v1.10.0
go.uber.org/zap v1.26.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
gopkg.in/gormigrate.v1 v1.6.0 gopkg.in/gormigrate.v1 v1.6.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
@ -77,6 +81,7 @@ require (
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // 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/arch v0.3.0 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/net v0.23.0 // indirect

16
go.sum
View File

@ -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-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-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 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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= 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 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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/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 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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.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/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/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 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/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 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/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.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE=
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/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 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA=
github.com/retailcrm/zabbix-metrics-collector v1.0.0/go.mod h1:3Orc+gfSg1tXj89QNvOn22t0cO1i2whR/4NJUGonWJA= 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= 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.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 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 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.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 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=