mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2024-11-24 06:06:03 +03:00
New logging format
This commit is contained in:
commit
36d32de72f
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.18', '1.19', '1.20', '1.21', '1.22', 'stable']
|
||||
go-version: ['1.22', 'stable']
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v3
|
||||
@ -46,7 +46,9 @@ jobs:
|
||||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
- name: Tests
|
||||
run: go test ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic
|
||||
run: |
|
||||
go install gotest.tools/gotestsum@latest
|
||||
gotestsum --format testdox ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Coverage
|
||||
run: |
|
||||
go install github.com/axw/gocov/gocov@latest
|
||||
|
@ -3,7 +3,7 @@
|
||||
[![Coverage](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/mg-transport-core)
|
||||
[![GitHub release](https://img.shields.io/github/release/retailcrm/mg-transport-core.svg?logo=github&logoColor=white)](https://github.com/retailcrm/mg-transport-core/releases)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core)
|
||||
[![GoLang version](https://img.shields.io/badge/go->=1.12-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/)
|
||||
[![GoLang version](https://img.shields.io/badge/go->=1.22-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/)
|
||||
[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core)
|
||||
|
||||
This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
|
||||
|
@ -32,8 +32,7 @@ func main() {
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint
|
||||
os.Exit(0)
|
||||
} else {
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@ -14,6 +14,7 @@ type Configuration interface {
|
||||
GetVersion() string
|
||||
GetSentryDSN() string
|
||||
GetLogLevel() logging.Level
|
||||
GetLogFormat() string
|
||||
GetHTTPConfig() HTTPServerConfig
|
||||
GetZabbixConfig() ZabbixConfig
|
||||
GetDBConfig() DatabaseConfig
|
||||
@ -44,6 +45,7 @@ type Config struct {
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
UpdateInterval int `yaml:"update_interval"`
|
||||
LogLevel logging.Level `yaml:"log_level"`
|
||||
LogFormat string `yaml:"log_format"`
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
@ -130,7 +132,7 @@ func (c *Config) GetConfigData(path string) []byte {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
source, err := ioutil.ReadFile(path)
|
||||
source, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -153,6 +155,10 @@ func (c Config) GetLogLevel() logging.Level {
|
||||
return c.LogLevel
|
||||
}
|
||||
|
||||
func (c Config) GetLogFormat() string {
|
||||
return c.LogFormat
|
||||
}
|
||||
|
||||
// GetTransportInfo transport basic data.
|
||||
func (c Config) GetTransportInfo() InfoInterface {
|
||||
return c.TransportInfo
|
||||
|
@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@ -38,6 +37,7 @@ transport_info:
|
||||
|
||||
sentry_dsn: dsn string
|
||||
log_level: 5
|
||||
log_format: console
|
||||
debug: true
|
||||
update_interval: 24
|
||||
|
||||
@ -53,7 +53,7 @@ config_aws:
|
||||
bucket: bucket
|
||||
folder_name: folder
|
||||
content_type: image/jpeg`)
|
||||
err := ioutil.WriteFile(testConfigFile, c.data, os.ModePerm)
|
||||
err := os.WriteFile(testConfigFile, c.data, os.ModePerm)
|
||||
require.Nil(c.T(), err)
|
||||
|
||||
c.config = NewConfig(testConfigFile)
|
||||
@ -90,6 +90,10 @@ func (c *ConfigTest) Test_GetLogLevel() {
|
||||
assert.Equal(c.T(), logging.Level(5), c.config.GetLogLevel())
|
||||
}
|
||||
|
||||
func (c *ConfigTest) Test_GetLogFormat() {
|
||||
assert.Equal(c.T(), "console", c.config.GetLogFormat())
|
||||
}
|
||||
|
||||
func (c *ConfigTest) Test_IsDebug() {
|
||||
assert.Equal(c.T(), true, c.config.IsDebug())
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (x *NewMigrationCommand) FileExists(filename string) bool {
|
||||
}
|
||||
|
||||
// Execute migration generator command.
|
||||
func (x *NewMigrationCommand) Execute(args []string) error {
|
||||
func (x *NewMigrationCommand) Execute(_ []string) error {
|
||||
tpl, err := template.New("migration").Parse(migrationTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fatal: cannot parse base migration template: %w", err)
|
||||
|
@ -2,7 +2,6 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
@ -36,7 +35,7 @@ func (s *MigrationGeneratorSuite) Test_FileExists() {
|
||||
func (s *MigrationGeneratorSuite) Test_Execute() {
|
||||
found := false
|
||||
assert.NoError(s.T(), s.command.Execute([]string{}))
|
||||
files, err := ioutil.ReadDir(s.command.Directory)
|
||||
files, err := os.ReadDir(s.command.Directory)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -17,3 +17,10 @@ type Connection struct {
|
||||
ID int `gorm:"primary_key" json:"id"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
func (c Connection) Address() string {
|
||||
if c.PublicURL != "" {
|
||||
return c.PublicURL
|
||||
}
|
||||
return c.URL
|
||||
}
|
||||
|
@ -14,8 +14,11 @@ type User struct {
|
||||
|
||||
// TableName will return table name for User
|
||||
// It will not work if User is not embedded, but mapped as another type
|
||||
//
|
||||
// type MyUser User // will not work
|
||||
//
|
||||
// but
|
||||
//
|
||||
// type MyUser struct { // will work
|
||||
// User
|
||||
// }
|
||||
|
12
core/doc.go
12
core/doc.go
@ -2,6 +2,7 @@
|
||||
Package core provides different functions like error-reporting, logging, localization, etc.
|
||||
to make it easier to create transports.
|
||||
Usage:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -36,14 +37,19 @@ Usage:
|
||||
}
|
||||
}
|
||||
|
||||
Resource embedding
|
||||
# Resource embedding
|
||||
|
||||
packr can be used to provide resource embedding, see:
|
||||
|
||||
https://github.com/gobuffalo/packr/tree/master/v2
|
||||
|
||||
In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library.
|
||||
You can find instruction here:
|
||||
|
||||
https://github.com/gobuffalo/packr/tree/master/v2#library-installation
|
||||
|
||||
Example of usage:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -104,10 +110,12 @@ Example of usage:
|
||||
}
|
||||
}
|
||||
|
||||
Migration generator
|
||||
# Migration generator
|
||||
|
||||
This library contains helper tool for transports. You can install it via go:
|
||||
|
||||
$ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
|
||||
|
||||
Currently, it only can generate new migrations for your transport.
|
||||
|
||||
Copyright (c) 2019 RetailDriver LLC. Usage of this source code is governed by a MIT license.
|
||||
|
@ -2,7 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -40,7 +40,7 @@ func getDomainsByStore(store string) []Domain {
|
||||
return nil
|
||||
}
|
||||
|
||||
respBody, readErr := ioutil.ReadAll(resp.Body)
|
||||
respBody, readErr := io.ReadAll(resp.Body)
|
||||
|
||||
if readErr != nil {
|
||||
return nil
|
||||
|
@ -7,14 +7,15 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/blacked/go-zabbix"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/op/go-logging"
|
||||
"github.com/retailcrm/zabbix-metrics-collector"
|
||||
metrics "github.com/retailcrm/zabbix-metrics-collector"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
@ -26,12 +27,14 @@ import (
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
)
|
||||
|
||||
const DefaultHTTPClientTimeout time.Duration = 30
|
||||
|
||||
var boolTrue = true
|
||||
|
||||
// DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client
|
||||
// if HTTP client config is not present in the configuration.
|
||||
var DefaultHTTPClientConfig = &config.HTTPClientConfig{
|
||||
Timeout: 30,
|
||||
Timeout: DefaultHTTPClientTimeout,
|
||||
SSLVerification: &boolTrue,
|
||||
}
|
||||
|
||||
@ -65,7 +68,6 @@ type Engine struct {
|
||||
logger logger.Logger
|
||||
AppInfo AppInfo
|
||||
Sessions sessions.Store
|
||||
LogFormatter logging.Formatter
|
||||
Config config.Configuration
|
||||
Zabbix metrics.Transport
|
||||
ginEngine *gin.Engine
|
||||
@ -112,11 +114,6 @@ func (e *Engine) initGin() {
|
||||
e.buildSentryConfig()
|
||||
e.InitSentrySDK()
|
||||
r.Use(e.SentryMiddlewares()...)
|
||||
|
||||
if e.Config.IsDebug() {
|
||||
r.Use(gin.Logger())
|
||||
}
|
||||
|
||||
r.Use(e.LocalizationMiddleware())
|
||||
e.ginEngine = r
|
||||
}
|
||||
@ -133,9 +130,6 @@ func (e *Engine) Prepare() *Engine {
|
||||
if e.DefaultError == "" {
|
||||
e.DefaultError = "error"
|
||||
}
|
||||
if e.LogFormatter == nil {
|
||||
e.LogFormatter = logger.DefaultLogFormatter()
|
||||
}
|
||||
if e.LocaleMatcher == nil {
|
||||
e.LocaleMatcher = DefaultLocalizerMatcher()
|
||||
}
|
||||
@ -150,9 +144,14 @@ func (e *Engine) Prepare() *Engine {
|
||||
e.Localizer.Preload(e.PreloadLanguages)
|
||||
}
|
||||
|
||||
logFormat := "json"
|
||||
if format := e.Config.GetLogFormat(); format != "" {
|
||||
logFormat = format
|
||||
}
|
||||
|
||||
e.CreateDB(e.Config.GetDBConfig())
|
||||
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
|
||||
e.SetLogger(logger.NewStandard(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter))
|
||||
e.SetLogger(logger.NewDefault(logFormat, e.Config.IsDebug()))
|
||||
e.Sentry.Localizer = &e.Localizer
|
||||
e.Utils.Logger = e.Logger()
|
||||
e.Sentry.Logger = e.Logger()
|
||||
@ -175,7 +174,25 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine {
|
||||
}
|
||||
cfg := e.Config.GetZabbixConfig()
|
||||
sender := zabbix.NewSender(cfg.ServerHost, cfg.ServerPort)
|
||||
e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, e.Logger())
|
||||
e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, logger.ZabbixCollectorAdapter(e.Logger()))
|
||||
return e
|
||||
}
|
||||
|
||||
// HijackGinLogs will take control of GIN debug logs and will convert them into structured logs.
|
||||
// It will also affect default logging middleware. Use logger.GinMiddleware to circumvent this.
|
||||
func (e *Engine) HijackGinLogs() *Engine {
|
||||
if e.Logger() == nil {
|
||||
return e
|
||||
}
|
||||
gin.DefaultWriter = logger.WriterAdapter(e.Logger(), zap.DebugLevel)
|
||||
gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), zap.ErrorLevel)
|
||||
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
|
||||
e.Logger().Debug("route",
|
||||
zap.String(logger.HTTPMethodAttr, httpMethod),
|
||||
zap.String("path", absolutePath),
|
||||
zap.String(logger.HandlerAttr, handlerName),
|
||||
zap.Int("handlerCount", nuHandlers))
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
@ -247,6 +264,9 @@ func (e *Engine) SetLogger(l logger.Logger) *Engine {
|
||||
|
||||
e.mutex.Lock()
|
||||
defer e.mutex.Unlock()
|
||||
if !e.prepared && e.logger != nil {
|
||||
return e
|
||||
}
|
||||
e.logger = l
|
||||
return e
|
||||
}
|
||||
@ -262,10 +282,9 @@ func (e *Engine) BuildHTTPClient(certs *x509.CertPool, replaceDefault ...bool) *
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
e.httpClient = client
|
||||
}
|
||||
|
||||
e.httpClient = client
|
||||
return e
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -123,7 +123,6 @@ func (e *EngineTest) Test_Prepare() {
|
||||
assert.True(e.T(), e.engine.prepared)
|
||||
assert.NotNil(e.T(), e.engine.Config)
|
||||
assert.NotEmpty(e.T(), e.engine.DefaultError)
|
||||
assert.NotEmpty(e.T(), e.engine.LogFormatter)
|
||||
assert.NotEmpty(e.T(), e.engine.LocaleMatcher)
|
||||
assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag))
|
||||
assert.NotNil(e.T(), e.engine.DB)
|
||||
@ -253,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() {
|
||||
defer func() {
|
||||
e.engine.logger = origLogger
|
||||
}()
|
||||
e.engine.logger = &logger.StandardLogger{}
|
||||
e.engine.logger = logger.NewNil()
|
||||
e.engine.SetLogger(nil)
|
||||
assert.NotNil(e.T(), e.engine.logger)
|
||||
}
|
||||
@ -366,7 +365,7 @@ func (e *EngineTest) Test_GetCSRFToken() {
|
||||
URL: &url.URL{
|
||||
RawQuery: "",
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
Header: http.Header{"X-CSRF-Token": []string{"token"}},
|
||||
}}
|
||||
c.Set("csrf_token", "token")
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
const DefaultResetPeriod = time.Minute * 15
|
||||
|
||||
// AtomicCounter is a default Counter implementation.
|
||||
// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and
|
||||
// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and.
|
||||
type AtomicCounter struct {
|
||||
name atomic.String
|
||||
msg atomic.String
|
||||
|
@ -1,7 +1,7 @@
|
||||
package healthcheck
|
||||
|
||||
var (
|
||||
// compile-time checks to ensure that implementations are compatible with the interface
|
||||
// compile-time checks to ensure that implementations are compatible with the interface.
|
||||
_ = Storage(&SyncMapStorage{})
|
||||
_ = Counter(&AtomicCounter{})
|
||||
_ = Processor(CounterProcessor{})
|
||||
|
@ -2,6 +2,7 @@ package healthcheck
|
||||
|
||||
import (
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,19 +30,19 @@ type CounterProcessor struct {
|
||||
func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen
|
||||
if counter.IsFailed() {
|
||||
if counter.IsFailureProcessed() {
|
||||
c.debugLog("skipping counter id=%d because its failure is already processed", id)
|
||||
c.debugLog("skipping counter because its failure is already processed", zap.Int(logger.CounterIDAttr, id))
|
||||
return true
|
||||
}
|
||||
|
||||
apiURL, apiKey, _, exists := c.ConnectionDataProvider(id)
|
||||
if !exists {
|
||||
c.debugLog("cannot find connection data for counter id=%d", id)
|
||||
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
|
||||
return true
|
||||
}
|
||||
err := c.Notifier(apiURL, apiKey, counter.Message())
|
||||
if err != nil {
|
||||
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)",
|
||||
id, err, counter.Message())
|
||||
c.debugLog("cannot send notification for counter",
|
||||
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
|
||||
}
|
||||
counter.FailureProcessed()
|
||||
return true
|
||||
@ -53,7 +54,8 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
|
||||
// Ignore this counter for now because total count of requests is less than minimal count.
|
||||
// The results may not be representative.
|
||||
if (succeeded + failed) < c.MinRequests {
|
||||
c.debugLog("skipping counter id=%d because it has fewer than %d requests", id, c.MinRequests)
|
||||
c.debugLog("skipping counter because it has too few requests",
|
||||
zap.Int(logger.CounterIDAttr, id), zap.Any("minRequests", c.MinRequests))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -72,13 +74,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
|
||||
|
||||
apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id)
|
||||
if !exists {
|
||||
c.debugLog("cannot find connection data for counter id=%d", id)
|
||||
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
|
||||
return true
|
||||
}
|
||||
err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang))
|
||||
if err != nil {
|
||||
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)",
|
||||
id, err, counter.Message())
|
||||
c.debugLog("cannot send notification for counter",
|
||||
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
|
||||
}
|
||||
counter.CountersProcessed()
|
||||
return true
|
||||
@ -96,6 +98,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string {
|
||||
|
||||
func (c CounterProcessor) debugLog(msg string, args ...interface{}) {
|
||||
if c.Debug {
|
||||
c.Logger.Debugf(msg, args...)
|
||||
c.Logger.Debug(msg, logger.AnyZapFields(args)...)
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func (t *CounterProcessorTest) localizer() NotifyMessageLocalizer {
|
||||
}
|
||||
|
||||
func (t *CounterProcessorTest) new(
|
||||
nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, testutil.BufferedLogger) {
|
||||
nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, *testutil.JSONRecordScanner) {
|
||||
loc := t.localizer()
|
||||
if len(noLocalizer) > 0 && noLocalizer[0] {
|
||||
loc = nil
|
||||
@ -61,7 +61,7 @@ func (t *CounterProcessorTest) new(
|
||||
FailureThreshold: DefaultFailureThreshold,
|
||||
MinRequests: DefaultMinRequests,
|
||||
Debug: true,
|
||||
}, log
|
||||
}, testutil.NewJSONRecordScanner(log)
|
||||
}
|
||||
|
||||
func (t *CounterProcessorTest) notifier(err ...error) *notifierMock {
|
||||
@ -95,7 +95,12 @@ func (t *CounterProcessorTest) Test_FailureProcessed() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(), "skipping counter id=1 because its failure is already processed")
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "skipping counter because its failure is already processed")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
}
|
||||
|
||||
func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
|
||||
@ -107,7 +112,12 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "cannot find connection data for counter")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
}
|
||||
|
||||
func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
|
||||
@ -121,7 +131,14 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: http status code: 500 (message: error message)")
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "cannot send notification for counter")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
t.Assert().Equal("http status code: 500", logs[0].Context["error"])
|
||||
t.Assert().Equal("error message", logs[0].Context["failureMessage"])
|
||||
t.Assert().Equal(t.apiURL, n.apiURL)
|
||||
t.Assert().Equal(t.apiKey, n.apiKey)
|
||||
t.Assert().Equal("error message", n.message)
|
||||
@ -138,7 +155,10 @@ func (t *CounterProcessorTest) Test_CounterFailed_SentNotification() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Empty(log.String())
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 0)
|
||||
t.Assert().Equal(t.apiURL, n.apiURL)
|
||||
t.Assert().Equal(t.apiKey, n.apiKey)
|
||||
t.Assert().Equal("error message", n.message)
|
||||
@ -154,8 +174,13 @@ func (t *CounterProcessorTest) Test_TooFewRequests() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(),
|
||||
fmt.Sprintf("skipping counter id=%d because it has fewer than %d requests", 1, DefaultMinRequests))
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "skipping counter because it has too few requests")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
t.Assert().Equal(float64(DefaultMinRequests), logs[0].Context["minRequests"])
|
||||
}
|
||||
|
||||
func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
|
||||
@ -170,7 +195,10 @@ func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Empty(log.String())
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 0)
|
||||
t.Assert().Empty(n.message)
|
||||
}
|
||||
|
||||
@ -185,7 +213,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_AlreadyProcessed() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Empty(log.String())
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 0)
|
||||
t.Assert().Empty(n.message)
|
||||
}
|
||||
|
||||
@ -200,7 +231,12 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "cannot find connection data for counter")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
t.Assert().Empty(n.message)
|
||||
}
|
||||
|
||||
@ -218,7 +254,13 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: unknown error (message: )")
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 1)
|
||||
t.Assert().Contains(logs[0].Message, "cannot send notification for counter")
|
||||
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||
t.Assert().Equal("unknown error", logs[0].Context["error"])
|
||||
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
||||
}
|
||||
|
||||
@ -235,7 +277,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent() {
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Empty(log.String())
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 0)
|
||||
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
||||
}
|
||||
|
||||
@ -252,7 +297,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent_NoLocalizer
|
||||
|
||||
p.Process(1, c)
|
||||
c.AssertExpectations(t.T())
|
||||
t.Assert().Empty(log.String())
|
||||
|
||||
logs, err := log.ScanAll()
|
||||
t.Require().NoError(err)
|
||||
t.Require().Len(logs, 0)
|
||||
t.Assert().Equal(`default error`, n.message)
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// JobFunc is empty func which should be executed in a parallel goroutine.
|
||||
@ -46,7 +47,7 @@ type Job struct {
|
||||
// SetLogger(logger).
|
||||
// SetLogging(false)
|
||||
// _ = manager.RegisterJob("updateTokens", &Job{
|
||||
// Command: func(log logger.Logger) error {
|
||||
// Command: func(log logger.LoggerOld) error {
|
||||
// // logic goes here...
|
||||
// logger.Info("All tokens were updated successfully")
|
||||
// return nil
|
||||
@ -73,6 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf
|
||||
}
|
||||
}()
|
||||
|
||||
log = log.With(logger.Handler(name))
|
||||
err := j.Command(log)
|
||||
if err != nil && j.ErrorHandler != nil {
|
||||
j.ErrorHandler(name, err, log)
|
||||
@ -153,7 +155,7 @@ func NewJobManager() *JobManager {
|
||||
func DefaultJobErrorHandler() JobErrorHandler {
|
||||
return func(name string, err error, log logger.Logger) {
|
||||
if err != nil && name != "" {
|
||||
log.Errorf("Job `%s` errored with an error: `%s`", name, err.Error())
|
||||
log.Error("job failed with an error", zap.String("job", name), logger.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,7 +164,7 @@ func DefaultJobErrorHandler() JobErrorHandler {
|
||||
func DefaultJobPanicHandler() JobPanicHandler {
|
||||
return func(name string, recoverValue interface{}, log logger.Logger) {
|
||||
if recoverValue != nil && name != "" {
|
||||
log.Errorf("Job `%s` panicked with value: `%#v`", name, recoverValue)
|
||||
log.Error("job panicked with the value", zap.String("job", name), zap.Any("value", recoverValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
)
|
||||
@ -25,7 +26,7 @@ type JobTest struct {
|
||||
executeErr chan error
|
||||
panicValue chan interface{}
|
||||
lastLog string
|
||||
lastMsgLevel logging.Level
|
||||
lastMsgLevel zapcore.Level
|
||||
syncBool bool
|
||||
}
|
||||
|
||||
@ -36,68 +37,94 @@ type JobManagerTest struct {
|
||||
syncRunnerFlag bool
|
||||
}
|
||||
|
||||
type callbackLoggerFunc func(level logging.Level, format string, args ...interface{})
|
||||
type callbackLoggerFunc func(level zapcore.Level, msg string, args ...zap.Field)
|
||||
|
||||
type callbackLogger struct {
|
||||
fields []zap.Field
|
||||
fn callbackLoggerFunc
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Fatal(args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, "", args...)
|
||||
func (n *callbackLogger) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
|
||||
return &zapcore.CheckedEntry{}
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Fatalf(format string, args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, format, args...)
|
||||
func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) {
|
||||
n.fn(zap.PanicLevel, msg, fields...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Panic(args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, "", args...)
|
||||
}
|
||||
func (n *callbackLogger) Panicf(format string, args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, format, args...)
|
||||
func (n *callbackLogger) Panic(msg string, fields ...zap.Field) {
|
||||
n.fn(zap.PanicLevel, msg, fields...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Critical(args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, "", args...)
|
||||
func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) {
|
||||
n.fn(zap.FatalLevel, msg, fields...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Criticalf(format string, args ...interface{}) {
|
||||
n.fn(logging.CRITICAL, format, args...)
|
||||
func (n *callbackLogger) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Error(args ...interface{}) {
|
||||
n.fn(logging.ERROR, "", args...)
|
||||
}
|
||||
func (n *callbackLogger) Errorf(format string, args ...interface{}) {
|
||||
n.fn(logging.ERROR, format, args...)
|
||||
func (n *callbackLogger) clone() *callbackLogger {
|
||||
return &callbackLogger{fn: n.fn, fields: n.fields}
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Warning(args ...interface{}) {
|
||||
n.fn(logging.WARNING, "", args...)
|
||||
func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger {
|
||||
cl := &callbackLogger{fn: n.fn, fields: n.fields}
|
||||
existing := cl.fields
|
||||
if len(existing) == 0 {
|
||||
cl.fields = fields
|
||||
return cl
|
||||
}
|
||||
func (n *callbackLogger) Warningf(format string, args ...interface{}) {
|
||||
n.fn(logging.WARNING, format, args...)
|
||||
cl.fields = append(existing, fields...) // nolint:gocritic
|
||||
return cl
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Notice(args ...interface{}) {
|
||||
n.fn(logging.NOTICE, "", args...)
|
||||
}
|
||||
func (n *callbackLogger) Noticef(format string, args ...interface{}) {
|
||||
n.fn(logging.NOTICE, format, args...)
|
||||
func (n *callbackLogger) Level() zapcore.Level {
|
||||
return zapcore.DebugLevel
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Info(args ...interface{}) {
|
||||
n.fn(logging.INFO, "", args...)
|
||||
}
|
||||
func (n *callbackLogger) Infof(format string, args ...interface{}) {
|
||||
n.fn(logging.INFO, format, args...)
|
||||
func (n *callbackLogger) With(args ...zap.Field) logger.Logger {
|
||||
return n.cloneWithFields(args)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Debug(args ...interface{}) {
|
||||
n.fn(logging.DEBUG, "", args...)
|
||||
func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger {
|
||||
return n.cloneWithFields(args)
|
||||
}
|
||||
func (n *callbackLogger) Debugf(format string, args ...interface{}) {
|
||||
n.fn(logging.DEBUG, format, args...)
|
||||
|
||||
func (n *callbackLogger) WithGroup(_ string) logger.Logger {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *callbackLogger) ForHandler(_ any) logger.Logger {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *callbackLogger) ForConnection(_ any) logger.Logger {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *callbackLogger) ForAccount(_ any) logger.Logger {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Log(level zapcore.Level, msg string, args ...zap.Field) {
|
||||
n.fn(level, msg, args...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Debug(msg string, args ...zap.Field) {
|
||||
n.Log(zap.DebugLevel, msg, args...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Info(msg string, args ...zap.Field) {
|
||||
n.Log(zap.InfoLevel, msg, args...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Warn(msg string, args ...zap.Field) {
|
||||
n.Log(zap.WarnLevel, msg, args...)
|
||||
}
|
||||
|
||||
func (n *callbackLogger) Error(msg string, args ...zap.Field) {
|
||||
n.Log(zap.ErrorLevel, msg, args...)
|
||||
}
|
||||
|
||||
func TestJob(t *testing.T) {
|
||||
@ -115,9 +142,9 @@ func TestDefaultJobErrorHandler(t *testing.T) {
|
||||
|
||||
fn := DefaultJobErrorHandler()
|
||||
require.NotNil(t, fn)
|
||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) {
|
||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
|
||||
require.Len(t, i, 2)
|
||||
assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
|
||||
assert.Equal(t, "error=test", fmt.Sprintf("%s=%v", i[1].Key, i[1].Interface))
|
||||
}})
|
||||
}
|
||||
|
||||
@ -128,9 +155,9 @@ func TestDefaultJobPanicHandler(t *testing.T) {
|
||||
|
||||
fn := DefaultJobPanicHandler()
|
||||
require.NotNil(t, fn)
|
||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) {
|
||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
|
||||
require.Len(t, i, 2)
|
||||
assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
|
||||
assert.Equal(t, "value=test", fmt.Sprintf("%s=%s", i[1].Key, i[1].Interface))
|
||||
}})
|
||||
}
|
||||
|
||||
@ -147,7 +174,7 @@ func (t *JobTest) testPanicHandler() JobPanicHandler {
|
||||
}
|
||||
|
||||
func (t *JobTest) testLogger() logger.Logger {
|
||||
return &callbackLogger{fn: func(level logging.Level, format string, args ...interface{}) {
|
||||
return &callbackLogger{fn: func(level zapcore.Level, format string, args ...zap.Field) {
|
||||
if format == "" {
|
||||
var sb strings.Builder
|
||||
sb.Grow(3 * len(args)) // nolint:gomnd
|
||||
@ -159,7 +186,12 @@ func (t *JobTest) testLogger() logger.Logger {
|
||||
format = strings.TrimRight(sb.String(), " ")
|
||||
}
|
||||
|
||||
t.lastLog = fmt.Sprintf(format, args...)
|
||||
anyFields := []any{}
|
||||
for _, item := range args {
|
||||
anyFields = append(anyFields, item.Key+"="+fmt.Sprint(item.Interface))
|
||||
}
|
||||
|
||||
t.lastLog = fmt.Sprintf(format, anyFields...)
|
||||
t.lastMsgLevel = level
|
||||
}}
|
||||
}
|
||||
@ -256,11 +288,11 @@ func (t *JobTest) oncePanicJob() {
|
||||
}
|
||||
|
||||
func (t *JobTest) regularJob() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec
|
||||
t.job = &Job{
|
||||
Command: func(log logger.Logger) error {
|
||||
t.executedChan <- true
|
||||
t.randomNumber <- rand.Int() // nolint:gosec
|
||||
t.randomNumber <- r.Int() // nolint:gosec
|
||||
return nil
|
||||
},
|
||||
ErrorHandler: t.testErrorHandler(),
|
||||
@ -271,7 +303,6 @@ func (t *JobTest) regularJob() {
|
||||
}
|
||||
|
||||
func (t *JobTest) regularSyncJob() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
t.job = &Job{
|
||||
Command: func(log logger.Logger) error {
|
||||
t.syncBool = true
|
||||
@ -404,11 +435,11 @@ func (t *JobManagerTest) WaitForJob() bool {
|
||||
|
||||
func (t *JobManagerTest) Test_SetLogger() {
|
||||
t.manager.logger = nil
|
||||
t.manager.SetLogger(logger.NewStandard("test", logging.ERROR, logger.DefaultLogFormatter()))
|
||||
assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger)
|
||||
t.manager.SetLogger(logger.NewDefault("json", true))
|
||||
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
||||
|
||||
t.manager.SetLogger(nil)
|
||||
assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger)
|
||||
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
||||
}
|
||||
|
||||
func (t *JobManagerTest) Test_SetLogging() {
|
||||
|
@ -3,7 +3,7 @@ package core
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
@ -90,6 +90,7 @@ type CloneableLocalizer interface {
|
||||
|
||||
// NewLocalizer returns localizer instance with specified parameters.
|
||||
// Usage:
|
||||
//
|
||||
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
|
||||
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface {
|
||||
localizer := &Localizer{
|
||||
@ -106,7 +107,9 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat
|
||||
|
||||
// NewLocalizerFS returns localizer instance with specified parameters.
|
||||
// Usage:
|
||||
//
|
||||
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS)
|
||||
//
|
||||
// TODO This code should be covered with tests.
|
||||
func NewLocalizerFS(
|
||||
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
|
||||
// i18n.Bundle methods (those aren't goroutine-safe to use).
|
||||
// Usage:
|
||||
//
|
||||
// engine := gin.New()
|
||||
// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations")
|
||||
// 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
|
||||
// Usage in code:
|
||||
//
|
||||
// engine := gin.New()
|
||||
// engine.FuncMap = localizer.LocalizationFuncMap()
|
||||
//
|
||||
// or (with multitemplate)
|
||||
//
|
||||
// renderer := multitemplate.NewRenderer()
|
||||
// funcMap := localizer.LocalizationFuncMap()
|
||||
// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html")
|
||||
//
|
||||
// funcMap must be passed for every .AddFromFilesFuncs call
|
||||
// Usage in templates:
|
||||
//
|
||||
// <p class="info">{{"need_login_msg" | trans}}
|
||||
//
|
||||
// You can borrow FuncMap from this method and add your functions to it.
|
||||
func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
@ -196,7 +206,7 @@ func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
||||
parts = append(parts, "")
|
||||
}
|
||||
|
||||
partsMap := make(map[string]interface{}, len(parts)/2)
|
||||
partsMap := make(map[string]interface{}, len(parts)/2) // nolint:gomnd
|
||||
|
||||
for i := 0; i < len(parts)-1; i += 2 {
|
||||
partsMap[parts[i]] = parts[i+1]
|
||||
@ -239,7 +249,7 @@ func (l *Localizer) loadTranslationsToBundle(i18nBundle *i18n.Bundle) {
|
||||
|
||||
// LoadTranslations will load all translation files from translations directory.
|
||||
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
|
||||
files, err := ioutil.ReadDir(l.TranslationsPath)
|
||||
files, err := os.ReadDir(l.TranslationsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -40,7 +39,7 @@ func createTestLangFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) {
|
||||
err = ioutil.WriteFile(fileName, data, os.ModePerm)
|
||||
err = os.WriteFile(fileName, data, os.ModePerm)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}
|
||||
@ -103,7 +102,6 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
|
||||
|
||||
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
||||
var wg sync.WaitGroup
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
l.localizer.Preload(DefaultLanguages)
|
||||
langMsgMap := map[language.Tag]string{
|
||||
language.English: "Test message",
|
||||
@ -120,9 +118,11 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
i := i
|
||||
go func(m map[language.Tag]string, wg *sync.WaitGroup) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(i))) // nolint:gosec
|
||||
var tag language.Tag
|
||||
switch rand.Intn(3-1) + 1 { // nolint:gosec
|
||||
switch r.Intn(3-1) + 1 { // nolint:gosec
|
||||
case 1:
|
||||
tag = language.English
|
||||
case 2:
|
||||
|
@ -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
|
||||
}
|
@ -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"))
|
||||
}
|
42
core/logger/api_client_adapter.go
Normal file
42
core/logger/api_client_adapter.go
Normal 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...))
|
||||
}
|
||||
}
|
41
core/logger/api_client_adapter_test.go
Normal file
41
core/logger/api_client_adapter_test.go
Normal 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
94
core/logger/attrs.go
Normal 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
156
core/logger/attrs_test.go
Normal 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)
|
||||
}
|
266
core/logger/buffer_logger_test.go
Normal file
266
core/logger/buffer_logger_test.go
Normal 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
29
core/logger/bufferpool.go
Normal 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
100
core/logger/default.go
Normal 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
|
||||
}
|
90
core/logger/default_test.go
Normal file
90
core/logger/default_test.go
Normal 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
37
core/logger/gin.go
Normal 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
38
core/logger/gin_test.go
Normal 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"])
|
||||
}
|
640
core/logger/json_with_context_encoder.go
Normal file
640
core/logger/json_with_context_encoder.go
Normal 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:])
|
||||
}
|
@ -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...)
|
||||
}
|
@ -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")
|
||||
}
|
60
core/logger/mg_transport_client_adapter.go
Normal file
60
core/logger/mg_transport_client_adapter.go
Normal 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...)
|
||||
}
|
45
core/logger/mg_transport_client_adapter_test.go
Normal file
45
core/logger/mg_transport_client_adapter_test.go
Normal 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
62
core/logger/nil.go
Normal 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
|
||||
}
|
@ -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{}
|
||||
}
|
@ -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
57
core/logger/pool.go
Normal 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
19
core/logger/pool_test.go
Normal 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)
|
||||
}
|
@ -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...)
|
||||
}
|
@ -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")
|
||||
}
|
22
core/logger/writer_adapter.go
Normal file
22
core/logger/writer_adapter.go
Normal 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
|
||||
}
|
25
core/logger/writer_adapter_test.go
Normal file
25
core/logger/writer_adapter_test.go
Normal 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)
|
||||
}
|
34
core/logger/zabbix_collector_adapter.go
Normal file
34
core/logger/zabbix_collector_adapter.go
Normal 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}
|
||||
}
|
26
core/logger/zabbix_collector_adapter_test.go
Normal file
26
core/logger/zabbix_collector_adapter_test.go
Normal 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
102
core/logger/zap.go
Normal 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: " ",
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
@ -57,10 +56,10 @@ var DefaultCSRFTokenGetter = func(c *gin.Context) string {
|
||||
} else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 {
|
||||
return t
|
||||
} else if c.Request.Body != nil {
|
||||
data, _ := ioutil.ReadAll(c.Request.Body)
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
|
||||
data, _ := io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||
t := r.FormValue("csrf_token")
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||
|
||||
if len(t) > 0 {
|
||||
return t
|
||||
@ -91,6 +90,7 @@ type CSRF struct {
|
||||
// csrfTokenGetter will be used to obtain token.
|
||||
//
|
||||
// Usage (with random salt):
|
||||
//
|
||||
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) {
|
||||
// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"})
|
||||
// }, core.DefaultCSRFTokenGetter)
|
||||
@ -99,9 +99,11 @@ type CSRF struct {
|
||||
// - don't forget to restore Body data!
|
||||
//
|
||||
// Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that:
|
||||
//
|
||||
// if t := r.FormValue("csrf_token"); len(t) > 0 {
|
||||
// return t
|
||||
// }
|
||||
//
|
||||
// will close body - and all next middlewares won't be able to read body at all!
|
||||
//
|
||||
// Use DefaultCSRFTokenGetter as example to implement your own token getter.
|
||||
@ -185,11 +187,11 @@ func (x *CSRF) generateSalt() string {
|
||||
|
||||
// pseudoRandomString generates pseudo-random string with specified length.
|
||||
func (x *CSRF) pseudoRandomString(length int) string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec
|
||||
data := make([]byte, length)
|
||||
|
||||
for i := 0; i < length; i++ { // it is supposed to use pseudo-random data.
|
||||
data[i] = byte(65 + rand.Intn(90-65)) // nolint:gosec,gomnd
|
||||
data[i] = byte(65 + r.Intn(90-65)) // nolint:gosec,gomnd
|
||||
}
|
||||
|
||||
return string(data)
|
||||
@ -209,6 +211,7 @@ func (x *CSRF) CSRFFromContext(c *gin.Context) string {
|
||||
|
||||
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
|
||||
// Usage:
|
||||
//
|
||||
// engine := gin.New()
|
||||
// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer)
|
||||
// engine.Use(csrf.GenerateCSRFMiddleware())
|
||||
@ -216,8 +219,8 @@ func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session, _ := x.store.Get(c.Request, x.sessionName)
|
||||
|
||||
if i, ok := session.Values["csrf_token"]; ok {
|
||||
if i, ok := i.(string); !ok || i == "" {
|
||||
if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif
|
||||
if i, ok := i.(string); !ok || i == "" { // nolint:nestif
|
||||
if x.fillToken(session, c) != nil {
|
||||
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
|
||||
c.Abort()
|
||||
@ -243,6 +246,7 @@ func (x *CSRF) fillToken(s *sessions.Session, c *gin.Context) error {
|
||||
|
||||
// VerifyCSRFMiddleware verifies CSRF token
|
||||
// Usage:
|
||||
//
|
||||
// engine := gin.New()
|
||||
// engine.Use(csrf.VerifyCSRFMiddleware())
|
||||
func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
|
||||
@ -254,9 +258,9 @@ func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
|
||||
var token string
|
||||
session, _ := x.store.Get(c.Request, x.sessionName)
|
||||
|
||||
if i, ok := session.Values["csrf_token"]; ok {
|
||||
if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif
|
||||
var v string
|
||||
if v, ok = i.(string); !ok || v == "" {
|
||||
if v, ok = i.(string); !ok || v == "" { // nolint:nestif
|
||||
if !ok {
|
||||
x.abortFunc(c, CSRFErrorIncorrectTokenType)
|
||||
} else if v == "" {
|
||||
|
@ -3,7 +3,6 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@ -32,7 +31,7 @@ func TestCSRF_DefaultCSRFTokenGetter_Empty(t *testing.T) {
|
||||
URL: &url.URL{
|
||||
RawQuery: "",
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}}
|
||||
|
||||
assert.Empty(t, DefaultCSRFTokenGetter(c))
|
||||
@ -85,14 +84,14 @@ func TestCSRF_DefaultCSRFTokenGetter_Form(t *testing.T) {
|
||||
RawQuery: "",
|
||||
},
|
||||
Header: headers,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}}
|
||||
c.Request.PostForm = url.Values{"csrf_token": {"token"}}
|
||||
|
||||
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
|
||||
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
|
||||
|
||||
_, err := ioutil.ReadAll(c.Request.Body)
|
||||
_, err := io.ReadAll(c.Request.Body)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
@ -64,14 +65,13 @@ func NewModuleFeaturesUploader(
|
||||
awsConfig.WithCredentialsProvider(customProvider),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Error("cannot load S3 configuration", logger.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
client := manager.NewUploader(s3.NewFromConfig(cfg))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Error("cannot load S3 configuration", logger.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -87,18 +87,18 @@ func NewModuleFeaturesUploader(
|
||||
}
|
||||
|
||||
func (s *ModuleFeaturesUploader) Upload() {
|
||||
s.log.Debugf("upload module features started...")
|
||||
s.log.Debug("upload module features started...")
|
||||
|
||||
content, err := os.ReadFile(s.featuresFilename)
|
||||
if err != nil {
|
||||
s.log.Errorf("cannot read markdown file %s %s", s.featuresFilename, err.Error())
|
||||
s.log.Error("cannot read markdown file %s %s", zap.String("fileName", s.featuresFilename), logger.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, lang := range languages {
|
||||
translated, err := s.translate(content, lang)
|
||||
if err != nil {
|
||||
s.log.Errorf("cannot translate module features file to %s: %s", lang.String(), err.Error())
|
||||
s.log.Error("cannot translate module features file", zap.String("lang", lang.String()), logger.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() {
|
||||
resp, err := s.uploadFile(html, lang.String())
|
||||
|
||||
if err != nil {
|
||||
s.log.Errorf("cannot upload file %s: %s", lang.String(), err.Error())
|
||||
s.log.Error("cannot upload file", zap.String("lang", lang.String()), logger.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ func (s *ModuleFeaturesUploader) Upload() {
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
s.log.Debugf("upload module features finished")
|
||||
s.log.Debug("upload module features finished")
|
||||
}
|
||||
|
||||
func (s *ModuleFeaturesUploader) translate(content []byte, lang language.Tag) ([]byte, error) {
|
||||
|
@ -1,19 +1,18 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/util/testutil"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/op/go-logging"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
)
|
||||
|
||||
type ModuleFeaturesUploaderTest struct {
|
||||
@ -36,8 +35,7 @@ func (t *ModuleFeaturesUploaderTest) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() {
|
||||
logs := &bytes.Buffer{}
|
||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
||||
log := testutil.NewBufferedLogger()
|
||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||
|
||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
||||
@ -50,8 +48,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeature
|
||||
}
|
||||
|
||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
|
||||
logs := &bytes.Buffer{}
|
||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
||||
log := testutil.NewBufferedLogger()
|
||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
||||
content := "test content " + t.localizer.GetLocalizedMessage("message")
|
||||
@ -62,8 +59,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
|
||||
}
|
||||
|
||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() {
|
||||
logs := &bytes.Buffer{}
|
||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
||||
log := testutil.NewBufferedLogger()
|
||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md")
|
||||
content := "test content"
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentrygin "github.com/getsentry/sentry-go/gin"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/stacktrace"
|
||||
@ -22,8 +23,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// reset is borrowed directly from the gin.
|
||||
const reset = "\033[0m"
|
||||
const recoveryMiddlewareSkipFrames = 3
|
||||
|
||||
// ErrorHandlerFunc will handle errors.
|
||||
type ErrorHandlerFunc func(recovery interface{}, c *gin.Context)
|
||||
@ -142,17 +142,17 @@ func (s *Sentry) SentryMiddlewares() []gin.HandlerFunc {
|
||||
|
||||
// obtainErrorLogger extracts logger from the context or builds it right here from tags used in Sentry events
|
||||
// Those tags can be configured with SentryLoggerConfig field.
|
||||
func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger {
|
||||
func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.Logger {
|
||||
if item, ok := c.Get("logger"); ok {
|
||||
if accountLogger, ok := item.(logger.AccountLogger); ok {
|
||||
return accountLogger
|
||||
if ctxLogger, ok := item.(logger.Logger); ok {
|
||||
return ctxLogger
|
||||
}
|
||||
}
|
||||
|
||||
connectionID := "{no connection ID}"
|
||||
accountID := "{no account ID}"
|
||||
if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" {
|
||||
return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID)
|
||||
return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
|
||||
}
|
||||
|
||||
for tag := range s.tagsFromContext(c) {
|
||||
@ -164,7 +164,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger {
|
||||
}
|
||||
}
|
||||
|
||||
return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID)
|
||||
return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
|
||||
}
|
||||
|
||||
// tagsSetterMiddleware sets event tags into Sentry events.
|
||||
@ -201,13 +201,13 @@ func (s *Sentry) exceptionCaptureMiddleware() gin.HandlerFunc { // nolint:gocogn
|
||||
for _, err := range publicErrors {
|
||||
messages[index] = err.Error()
|
||||
s.CaptureException(c, err)
|
||||
l.Error(err)
|
||||
l.Error(err.Error())
|
||||
index++
|
||||
}
|
||||
|
||||
for _, err := range privateErrors {
|
||||
s.CaptureException(c, err)
|
||||
l.Error(err)
|
||||
l.Error(err.Error())
|
||||
}
|
||||
|
||||
if privateLen > 0 || recovery != nil {
|
||||
@ -250,8 +250,9 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
|
||||
}
|
||||
}
|
||||
if l != nil {
|
||||
stack := stacktrace.FormattedStack(3, l.Prefix()+" ")
|
||||
formattedErr := fmt.Sprintf("%s %s", l.Prefix(), err)
|
||||
// TODO: Check if we can output stacktraces with prefix data like before if we really need it.
|
||||
stack := stacktrace.FormattedStack(recoveryMiddlewareSkipFrames, "trace: ")
|
||||
formattedErr := logger.Err(err)
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
headers := strings.Split(string(httpRequest), "\r\n")
|
||||
for idx, header := range headers {
|
||||
@ -259,18 +260,17 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
|
||||
if current[0] == "Authorization" {
|
||||
headers[idx] = current[0] + ": *"
|
||||
}
|
||||
headers[idx] = l.Prefix() + " " + headers[idx]
|
||||
headers[idx] = "header: " + headers[idx]
|
||||
}
|
||||
headersToStr := strings.Join(headers, "\r\n")
|
||||
headersToStr := zap.String("headers", strings.Join(headers, "\r\n"))
|
||||
formattedStack := zap.String("stacktrace", string(stack))
|
||||
switch {
|
||||
case brokenPipe:
|
||||
l.Errorf("%s\n%s%s", formattedErr, headersToStr, reset)
|
||||
l.Error("error", formattedErr, headersToStr)
|
||||
case gin.IsDebugging():
|
||||
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||
timeFormat(time.Now()), headersToStr, formattedErr, stack, reset)
|
||||
l.Error("[Recovery] panic recovered", headersToStr, formattedErr, formattedStack)
|
||||
default:
|
||||
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||
timeFormat(time.Now()), formattedErr, stack, reset)
|
||||
l.Error("[Recovery] panic recovered", formattedErr, formattedStack)
|
||||
}
|
||||
}
|
||||
if brokenPipe {
|
||||
|
@ -2,7 +2,6 @@ package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
@ -32,12 +31,12 @@ type sentryMockTransport struct {
|
||||
sending sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *sentryMockTransport) Flush(timeout time.Duration) bool {
|
||||
func (s *sentryMockTransport) Flush(_ time.Duration) bool {
|
||||
// noop
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sentryMockTransport) Configure(options sentry.ClientOptions) {
|
||||
func (s *sentryMockTransport) Configure(_ sentry.ClientOptions) {
|
||||
// noop
|
||||
}
|
||||
|
||||
@ -278,7 +277,7 @@ func (s *SentryTest) TestSentry_CaptureException() {
|
||||
|
||||
func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() {
|
||||
ctx, _ := s.ginCtxMock()
|
||||
log := logger.DecorateForAccount(testutil.NewBufferedLogger(), "component", "conn", "acc")
|
||||
log := testutil.NewBufferedLogger().ForHandler("component").ForConnection("conn").ForAccount("acc")
|
||||
ctx.Set("logger", log)
|
||||
|
||||
s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx))
|
||||
@ -299,12 +298,8 @@ func (s *SentryTest) TestSentry_obtainErrorLogger_Constructed() {
|
||||
|
||||
s.Assert().NotNil(log)
|
||||
s.Assert().NotNil(logNoConfig)
|
||||
s.Assert().Implements((*logger.AccountLogger)(nil), log)
|
||||
s.Assert().Implements((*logger.AccountLogger)(nil), logNoConfig)
|
||||
s.Assert().Equal(
|
||||
fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "{no connection ID}", "{no account ID}"),
|
||||
logNoConfig.Prefix())
|
||||
s.Assert().Equal(fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "conn_url", "acc_name"), log.Prefix())
|
||||
s.Assert().Implements((*logger.Logger)(nil), log)
|
||||
s.Assert().Implements((*logger.Logger)(nil), logNoConfig)
|
||||
}
|
||||
|
||||
func (s *SentryTest) TestSentry_MiddlewaresError() {
|
||||
|
@ -18,7 +18,7 @@ func TestError(t *testing.T) {
|
||||
|
||||
func (t *ErrorTest) TestAppendToError() {
|
||||
err := errors.New("test error")
|
||||
_, ok := err.(StackTraced)
|
||||
_, ok := err.(StackTraced) // nolint:errorlint
|
||||
|
||||
t.Assert().False(ok)
|
||||
|
||||
@ -28,16 +28,16 @@ func (t *ErrorTest) TestAppendToError() {
|
||||
t.Assert().Nil(AppendToError(nil))
|
||||
t.Assert().Implements((*StackTraced)(nil), withTrace)
|
||||
t.Assert().Implements((*StackTraced)(nil), twiceTrace)
|
||||
t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace())
|
||||
t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace()) // nolint:errorlint
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestCauseUnwrap() {
|
||||
err := errors.New("test error")
|
||||
wrapped := AppendToError(err)
|
||||
|
||||
t.Assert().Equal(err, wrapped.(*withStack).Cause())
|
||||
t.Assert().Equal(err, wrapped.(*withStack).Cause()) // nolint:errorlint
|
||||
t.Assert().Equal(err, errors.Unwrap(wrapped))
|
||||
t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped))
|
||||
t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped)) // nolint:errorlint
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFormat() {
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@ -78,19 +78,19 @@ func (f Frame) Format(s fmt.State, verb rune) {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
io.WriteString(s, f.name())
|
||||
io.WriteString(s, "\n\t")
|
||||
io.WriteString(s, f.file())
|
||||
_, _ = io.WriteString(s, f.name())
|
||||
_, _ = io.WriteString(s, "\n\t")
|
||||
_, _ = io.WriteString(s, f.file())
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
_, _ = io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
io.WriteString(s, strconv.Itoa(f.line()))
|
||||
_, _ = io.WriteString(s, strconv.Itoa(f.line()))
|
||||
case 'n':
|
||||
io.WriteString(s, funcname(f.name()))
|
||||
_, _ = io.WriteString(s, funcname(f.name()))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
_, _ = io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
@ -122,11 +122,11 @@ func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
io.WriteString(s, "\n")
|
||||
_, _ = io.WriteString(s, "\n")
|
||||
f.Format(s, verb)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
_, _ = fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
st.formatSlice(s, verb)
|
||||
}
|
||||
@ -138,14 +138,14 @@ func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
// formatSlice will format this StackTrace into the given buffer as a slice of
|
||||
// Frame, only valid when called with '%s' or '%v'.
|
||||
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
|
||||
io.WriteString(s, "[")
|
||||
_, _ = io.WriteString(s, "[")
|
||||
for i, f := range st {
|
||||
if i > 0 {
|
||||
io.WriteString(s, " ")
|
||||
_, _ = io.WriteString(s, " ")
|
||||
}
|
||||
f.Format(s, verb)
|
||||
}
|
||||
io.WriteString(s, "]")
|
||||
_, _ = io.WriteString(s, "]")
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
@ -159,7 +159,7 @@ func (s *stack) Format(st fmt.State, verb rune) {
|
||||
if verb == 'v' && st.Flag('+') {
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
_, _ = fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,16 +185,16 @@ func FormattedStack(skip int, prefix string) []byte {
|
||||
break
|
||||
}
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc)
|
||||
_, _ = fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line))
|
||||
_, _ = fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@ -37,8 +36,8 @@ func (t *TemplateTest) initTestData() {
|
||||
require.Nil(t.T(), err)
|
||||
data1 := []byte(`data {{template "body" .}}`)
|
||||
data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`)
|
||||
err1 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm)
|
||||
err2 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm)
|
||||
err1 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm)
|
||||
err2 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm)
|
||||
require.Nil(t.T(), err1)
|
||||
require.Nil(t.T(), err2)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
// because AsError() returns nil if there are no errors in the list.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// err := errorutil.NewCollector().
|
||||
// Do(errors.New("error 1")).
|
||||
// Do(errors.New("error 2"), errors.New("error 3"))
|
||||
@ -25,11 +26,13 @@ import (
|
||||
// fmt.Println(err)
|
||||
//
|
||||
// This code will produce something like this:
|
||||
//
|
||||
// #1 err at /home/user/main.go:62: error 1
|
||||
// #2 err at /home/user/main.go:63: error 2
|
||||
// #3 err at /home/user/main.go:64: error 3
|
||||
//
|
||||
// You can also iterate over the error to use their data instead of using predefined message:
|
||||
//
|
||||
// err := errorutil.NewCollector().
|
||||
// Do(errors.New("error 1")).
|
||||
// Do(errors.New("error 2"), errors.New("error 3"))
|
||||
@ -39,11 +42,13 @@ import (
|
||||
// }
|
||||
//
|
||||
// This code will produce output that looks like this:
|
||||
//
|
||||
// Error at /home/user/main.go:164: error 0
|
||||
// Error at /home/user/main.go:164: error 1
|
||||
// Error at /home/user/main.go:164: error 2
|
||||
//
|
||||
// Example with GORM migration (Collector is returned as an error here).
|
||||
//
|
||||
// return errorutil.NewCollector().Do(
|
||||
// db.CreateTable(models.Account{}, models.Connection{}).Error,
|
||||
// db.Table("account").AddUniqueIndex("account_key", "channel").Error,
|
||||
|
@ -14,6 +14,7 @@ type ListResponse struct {
|
||||
|
||||
// GetErrorResponse returns ErrorResponse with specified status code
|
||||
// Usage (with gin):
|
||||
//
|
||||
// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money"))
|
||||
func GetErrorResponse(statusCode int, err string) (int, interface{}) {
|
||||
return statusCode, Response{
|
||||
@ -23,6 +24,7 @@ func GetErrorResponse(statusCode int, err string) (int, interface{}) {
|
||||
|
||||
// BadRequest returns ErrorResponse with code 400
|
||||
// Usage (with gin):
|
||||
//
|
||||
// context.JSON(BadRequest("invalid data"))
|
||||
func BadRequest(err string) (int, interface{}) {
|
||||
return GetErrorResponse(http.StatusBadRequest, err)
|
||||
@ -30,6 +32,7 @@ func BadRequest(err string) (int, interface{}) {
|
||||
|
||||
// Unauthorized returns ErrorResponse with code 401
|
||||
// Usage (with gin):
|
||||
//
|
||||
// context.JSON(Unauthorized("invalid credentials"))
|
||||
func Unauthorized(err string) (int, interface{}) {
|
||||
return GetErrorResponse(http.StatusUnauthorized, err)
|
||||
@ -37,6 +40,7 @@ func Unauthorized(err string) (int, interface{}) {
|
||||
|
||||
// Forbidden returns ErrorResponse with code 403
|
||||
// Usage (with gin):
|
||||
//
|
||||
// context.JSON(Forbidden("forbidden"))
|
||||
func Forbidden(err string) (int, interface{}) {
|
||||
return GetErrorResponse(http.StatusForbidden, err)
|
||||
@ -44,6 +48,7 @@ func Forbidden(err string) (int, interface{}) {
|
||||
|
||||
// InternalServerError returns ErrorResponse with code 500
|
||||
// Usage (with gin):
|
||||
//
|
||||
// context.JSON(BadRequest("invalid data"))
|
||||
func InternalServerError(err string) (int, interface{}) {
|
||||
return GetErrorResponse(http.StatusInternalServerError, err)
|
||||
|
@ -10,10 +10,10 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
)
|
||||
|
||||
@ -230,12 +230,12 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
||||
return errors.New("dialer must be built first")
|
||||
}
|
||||
|
||||
if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 {
|
||||
b.logf("Mock address is \"%s\"\n", net.JoinHostPort(b.mockHost, b.mockPort))
|
||||
b.logf("Mocked domains: ")
|
||||
if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { // nolint:nestif
|
||||
b.log("Mock address has been set", zap.String("address", net.JoinHostPort(b.mockHost, b.mockPort)))
|
||||
b.log("Mocked domains: ")
|
||||
|
||||
for _, domain := range b.mockedDomains {
|
||||
b.logf(" - %s\n", domain)
|
||||
b.log(fmt.Sprintf(" - %s\n", domain))
|
||||
}
|
||||
|
||||
b.httpTransport.Proxy = nil
|
||||
@ -259,7 +259,7 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
||||
addr = net.JoinHostPort(b.mockHost, b.mockPort)
|
||||
}
|
||||
|
||||
b.logf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr)
|
||||
b.log(fmt.Sprintf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr))
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,13 +270,13 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logf prints logs via Engine or via fmt.Printf.
|
||||
func (b *HTTPClientBuilder) logf(format string, args ...interface{}) {
|
||||
// log prints logs via Engine or via fmt.Println.
|
||||
func (b *HTTPClientBuilder) log(msg string, args ...interface{}) {
|
||||
if b.logging {
|
||||
if b.logger != nil {
|
||||
b.logger.Infof(format, args...)
|
||||
b.logger.Info(msg, logger.AnyZapFields(args)...)
|
||||
} else {
|
||||
fmt.Printf(format, args...)
|
||||
fmt.Println(append([]any{msg}, args...))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -16,7 +15,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -137,14 +135,14 @@ func (t *HTTPClientBuilderTest) Test_buildMocks() {
|
||||
}
|
||||
|
||||
func (t *HTTPClientBuilderTest) Test_WithLogger() {
|
||||
logger := logger.NewStandard("telegram", logging.ERROR, logger.DefaultLogFormatter())
|
||||
builder := NewHTTPClientBuilder()
|
||||
require.Nil(t.T(), builder.logger)
|
||||
|
||||
builder.WithLogger(nil)
|
||||
assert.Nil(t.T(), builder.logger)
|
||||
|
||||
builder.WithLogger(logger)
|
||||
log := logger.NewDefault("json", true)
|
||||
builder.WithLogger(log)
|
||||
assert.NotNil(t.T(), builder.logger)
|
||||
}
|
||||
|
||||
@ -153,7 +151,7 @@ func (t *HTTPClientBuilderTest) Test_logf() {
|
||||
assert.Nil(t.T(), recover())
|
||||
}()
|
||||
|
||||
t.builder.logf("test %s", "string")
|
||||
t.builder.log(fmt.Sprintf("test %s", "string"))
|
||||
}
|
||||
|
||||
func (t *HTTPClientBuilderTest) Test_Build() {
|
||||
@ -210,6 +208,7 @@ x5porosgI2RgOTTwmiYOcYQTS2650jYydHhK16Gu2b3UKernO16mAWXNDWfvS2bk
|
||||
nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI
|
||||
saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB
|
||||
-----END CERTIFICATE-----`
|
||||
// nolint:gosec
|
||||
keyFileData := `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC
|
||||
5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn
|
||||
@ -238,9 +237,9 @@ weywTxDl/OD5ybNkZIRKsIXciFYG1VCGO2HNGN9qJcV+nJ63kyrIBauwUkuEhiN5
|
||||
uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
certFile, err := ioutil.TempFile("/tmp", "cert_")
|
||||
certFile, err := os.CreateTemp("/tmp", "cert_")
|
||||
require.NoError(t.T(), err, "cannot create temp cert file")
|
||||
keyFile, err := ioutil.TempFile("/tmp", "key_")
|
||||
keyFile, err := os.CreateTemp("/tmp", "key_")
|
||||
require.NoError(t.T(), err, "cannot create temp key file")
|
||||
|
||||
_, err = certFile.WriteString(certFileData)
|
||||
@ -253,7 +252,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
||||
errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file")
|
||||
|
||||
mux := &http.ServeMux{}
|
||||
srv := &http.Server{Addr: mockServerAddr, Handler: mux}
|
||||
srv := &http.Server{Addr: mockServerAddr, Handler: mux, ReadHeaderTimeout: defaultDialerTimeout}
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
@ -308,7 +307,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t.T(), err, "error while reading body")
|
||||
|
||||
assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code")
|
||||
|
@ -1,15 +1,13 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// ReadBuffer is implemented by the BufferLogger.
|
||||
@ -28,120 +26,75 @@ type BufferedLogger interface {
|
||||
}
|
||||
|
||||
// BufferLogger is an implementation of the BufferedLogger.
|
||||
//
|
||||
// BufferLogger can be used in tests to match specific log messages. It uses JSON by default (hardcoded for now).
|
||||
// It implements fmt.Stringer and provides an adapter to the underlying buffer, which means it can also return
|
||||
// Bytes(), can be used like io.Reader and can be cleaned using Reset() method.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// log := NewBufferedLogger()
|
||||
// // Some other code that works with logger.
|
||||
// fmt.Println(log.String())
|
||||
type BufferLogger struct {
|
||||
buf bytes.Buffer
|
||||
rw sync.RWMutex
|
||||
logger.Default
|
||||
buf LockableBuffer
|
||||
}
|
||||
|
||||
// NewBufferedLogger returns new BufferedLogger instance.
|
||||
func NewBufferedLogger() BufferedLogger {
|
||||
return &BufferLogger{}
|
||||
bl := &BufferLogger{}
|
||||
bl.Logger = zap.New(
|
||||
zapcore.NewCore(
|
||||
logger.NewJSONWithContextEncoder(
|
||||
logger.EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
|
||||
return bl
|
||||
}
|
||||
|
||||
func (l *BufferLogger) With(fields ...zapcore.Field) logger.Logger {
|
||||
return &BufferLogger{
|
||||
Default: logger.Default{
|
||||
Logger: l.Logger.With(fields...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BufferLogger) WithLazy(fields ...zapcore.Field) logger.Logger {
|
||||
return &BufferLogger{
|
||||
Default: logger.Default{
|
||||
Logger: l.Logger.WithLazy(fields...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BufferLogger) ForHandler(handler any) logger.Logger {
|
||||
return l.WithLazy(zap.Any(logger.HandlerAttr, handler))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) ForConnection(conn any) logger.Logger {
|
||||
return l.WithLazy(zap.Any(logger.ConnectionAttr, conn))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) ForAccount(acc any) logger.Logger {
|
||||
return l.WithLazy(zap.Any(logger.AccountAttr, acc))
|
||||
}
|
||||
|
||||
// Read bytes from the logger buffer. io.Reader implementation.
|
||||
func (l *BufferLogger) Read(p []byte) (n int, err error) {
|
||||
defer l.rw.RUnlock()
|
||||
l.rw.RLock()
|
||||
return l.buf.Read(p)
|
||||
}
|
||||
|
||||
// String contents of the logger buffer. fmt.Stringer implementation.
|
||||
func (l *BufferLogger) String() string {
|
||||
defer l.rw.RUnlock()
|
||||
l.rw.RLock()
|
||||
return l.buf.String()
|
||||
}
|
||||
|
||||
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
|
||||
func (l *BufferLogger) Bytes() []byte {
|
||||
defer l.rw.RUnlock()
|
||||
l.rw.RLock()
|
||||
return l.buf.Bytes()
|
||||
}
|
||||
|
||||
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
|
||||
func (l *BufferLogger) Reset() {
|
||||
defer l.rw.Unlock()
|
||||
l.rw.Lock()
|
||||
l.buf.Reset()
|
||||
}
|
||||
|
||||
func (l *BufferLogger) write(level logging.Level, args ...interface{}) {
|
||||
defer l.rw.Unlock()
|
||||
l.rw.Lock()
|
||||
l.buf.WriteString(fmt.Sprintln(append([]interface{}{level.String(), "=>"}, args...)...))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) writef(level logging.Level, format string, args ...interface{}) {
|
||||
defer l.rw.Unlock()
|
||||
l.rw.Lock()
|
||||
l.buf.WriteString(fmt.Sprintf(level.String()+" => "+format, args...))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Fatal(args ...interface{}) {
|
||||
l.write(logging.CRITICAL, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Fatalf(format string, args ...interface{}) {
|
||||
l.writef(logging.CRITICAL, format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Panic(args ...interface{}) {
|
||||
l.write(logging.CRITICAL, args...)
|
||||
panic(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Panicf(format string, args ...interface{}) {
|
||||
l.writef(logging.CRITICAL, format, args...)
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Critical(args ...interface{}) {
|
||||
l.write(logging.CRITICAL, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Criticalf(format string, args ...interface{}) {
|
||||
l.writef(logging.CRITICAL, format, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Error(args ...interface{}) {
|
||||
l.write(logging.ERROR, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Errorf(format string, args ...interface{}) {
|
||||
l.writef(logging.ERROR, format, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Warning(args ...interface{}) {
|
||||
l.write(logging.WARNING, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Warningf(format string, args ...interface{}) {
|
||||
l.writef(logging.WARNING, format, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Notice(args ...interface{}) {
|
||||
l.write(logging.NOTICE, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Noticef(format string, args ...interface{}) {
|
||||
l.writef(logging.NOTICE, format, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Info(args ...interface{}) {
|
||||
l.write(logging.INFO, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Infof(format string, args ...interface{}) {
|
||||
l.writef(logging.INFO, format, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Debug(args ...interface{}) {
|
||||
l.write(logging.DEBUG, args...)
|
||||
}
|
||||
|
||||
func (l *BufferLogger) Debugf(format string, args ...interface{}) {
|
||||
l.writef(logging.DEBUG, format, args...)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@ -30,17 +29,20 @@ func (t *BufferLoggerTest) Test_Read() {
|
||||
|
||||
data, err := io.ReadAll(t.logger)
|
||||
t.Require().NoError(err)
|
||||
t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), data)
|
||||
t.Assert().Contains(string(data), "\"level_name\":\"DEBUG\"")
|
||||
t.Assert().Contains(string(data), "\"message\":\"test\"")
|
||||
}
|
||||
|
||||
func (t *BufferLoggerTest) Test_Bytes() {
|
||||
t.logger.Debug("test")
|
||||
t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), t.logger.Bytes())
|
||||
t.Assert().Contains(string(t.logger.Bytes()), "\"level_name\":\"DEBUG\"")
|
||||
t.Assert().Contains(string(t.logger.Bytes()), "\"message\":\"test\"")
|
||||
}
|
||||
|
||||
func (t *BufferLoggerTest) Test_String() {
|
||||
t.logger.Debug("test")
|
||||
t.Assert().Equal(logging.DEBUG.String()+" => test\n", t.logger.String())
|
||||
t.Assert().Contains(t.logger.String(), "\"level_name\":\"DEBUG\"")
|
||||
t.Assert().Contains(t.logger.String(), "\"message\":\"test\"")
|
||||
}
|
||||
|
||||
func (t *BufferLoggerTest) TestRace() {
|
||||
|
@ -46,7 +46,7 @@ func printRequestData(t UnmatchedRequestsTestingT, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if r.Body == nil {
|
||||
if r.Body == nil { // nolint:nestif
|
||||
t.Log("No body is present.")
|
||||
} else {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
|
52
core/util/testutil/json_record_scanner.go
Normal file
52
core/util/testutil/json_record_scanner.go
Normal 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
|
||||
}
|
108
core/util/testutil/json_record_scanner_test.go
Normal file
108
core/util/testutil/json_record_scanner_test.go
Normal 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])
|
||||
}
|
155
core/util/testutil/lockable_buffer.go
Normal file
155
core/util/testutil/lockable_buffer.go
Normal 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
|
||||
}
|
@ -3,7 +3,6 @@ package testutil
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -63,8 +62,8 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
|
||||
err error
|
||||
)
|
||||
|
||||
if info, err = os.Stat(fileName); err == nil {
|
||||
if !info.IsDir() {
|
||||
if info, err = os.Stat(fileName); err == nil { // nolint:nestif
|
||||
if !info.IsDir() { // nolint:nestif
|
||||
var (
|
||||
path string
|
||||
source []byte
|
||||
@ -75,7 +74,7 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
|
||||
return dataMap, err
|
||||
}
|
||||
|
||||
if source, err = ioutil.ReadFile(path); err != nil {
|
||||
if source, err = os.ReadFile(path); err != nil {
|
||||
return dataMap, err
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
@ -40,7 +39,7 @@ func (t *TranslationsExtractorTest) SetupSuite() {
|
||||
data, _ := yaml.Marshal(translation)
|
||||
// It's not regular temporary file. Little hack in order to test translations extractor.
|
||||
// nolint:gosec
|
||||
errWrite := ioutil.WriteFile("/tmp/translate.en.yml", data, os.ModePerm)
|
||||
errWrite := os.WriteFile("/tmp/translate.en.yml", data, os.ModePerm)
|
||||
require.NoError(t.T(), errWrite)
|
||||
|
||||
t.extractor = NewTranslationsExtractor("translate.{}.yml")
|
||||
|
@ -1,11 +1,13 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
// nolint:gosec
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -16,11 +18,12 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/gin-gonic/gin"
|
||||
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
|
||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/util/errorutil"
|
||||
@ -132,7 +135,7 @@ func (u *Utils) GenerateToken() string {
|
||||
func (u *Utils) GetAPIClient(
|
||||
url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) {
|
||||
client := retailcrm.New(url, key).
|
||||
WithLogger(retailcrm.DebugLoggerAdapter(u.Logger))
|
||||
WithLogger(logger.APIClientAdapter(u.Logger))
|
||||
client.Debug = u.IsDebug
|
||||
|
||||
cr, status, err := client.APICredentials()
|
||||
@ -142,12 +145,12 @@ func (u *Utils) GetAPIClient(
|
||||
|
||||
if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 {
|
||||
if len(credentials) == 0 || len(cr.Scopes) > 0 {
|
||||
u.Logger.Error(url, status, res)
|
||||
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
|
||||
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
||||
}
|
||||
|
||||
if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 {
|
||||
u.Logger.Error(url, status, res)
|
||||
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
|
||||
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
||||
}
|
||||
}
|
||||
@ -281,3 +284,15 @@ func GetCurrencySymbol(code string) string {
|
||||
func FormatCurrencyValue(value float32) string {
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
|
||||
// BindJSONWithRaw will perform usual ShouldBindJSON and will return the original body data.
|
||||
func BindJSONWithRaw(c *gin.Context, obj any) ([]byte, error) {
|
||||
closer := c.Request.Body
|
||||
defer func() { _ = closer.Close() }()
|
||||
data, err := io.ReadAll(closer)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||
return data, c.ShouldBindJSON(obj)
|
||||
}
|
||||
|
@ -9,13 +9,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
"github.com/op/go-logging"
|
||||
|
||||
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||
|
||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||
@ -38,7 +39,7 @@ func mgClient() *v1.MgClient {
|
||||
}
|
||||
|
||||
func (u *UtilsTest) SetupSuite() {
|
||||
logger := logger.NewStandard("code", logging.DEBUG, logger.DefaultLogFormatter())
|
||||
logger := logger.NewDefault("json", true)
|
||||
awsConfig := config.AWS{
|
||||
AccessKeyID: "access key id (will be removed)",
|
||||
SecretAccessKey: "secret access key",
|
||||
|
9
go.mod
9
go.mod
@ -1,6 +1,8 @@
|
||||
module github.com/retailcrm/mg-transport-core/v2
|
||||
|
||||
go 1.18
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||
@ -17,6 +19,7 @@ require (
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/guregu/null/v5 v5.0.0
|
||||
github.com/h2non/gock v1.2.0
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/jinzhu/gorm v1.9.11
|
||||
@ -24,10 +27,11 @@ require (
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/retailcrm/api-client-go/v2 v2.1.3
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.3.4
|
||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0
|
||||
github.com/stretchr/testify v1.8.3
|
||||
go.uber.org/atomic v1.10.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@ -77,6 +81,7 @@ require (
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -157,6 +157,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
@ -237,6 +238,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw=
|
||||
github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
@ -296,8 +299,9 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
@ -376,8 +380,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKKB//MUAe0Zb0=
|
||||
github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ=
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs=
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE=
|
||||
github.com/retailcrm/mg-transport-api-client-go v1.3.4/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro=
|
||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA=
|
||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0/go.mod h1:3Orc+gfSg1tXj89QNvOn22t0cO1i2whR/4NJUGonWJA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@ -441,6 +445,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
|
Loading…
Reference in New Issue
Block a user