New logging format

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

View File

@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
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

View File

@ -3,7 +3,7 @@
[![Coverage](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/mg-transport-core)
[![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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,96 +0,0 @@
package logger
import (
"fmt"
"github.com/op/go-logging"
)
// DefaultAccountLoggerFormat contains default prefix format for the AccountLoggerDecorator.
// Its messages will look like this (assuming you will provide the connection URL and account name):
// messageHandler (https://any.simla.com => @tg_account): sent message with id=1
const DefaultAccountLoggerFormat = "%s (%s => %s):"
type ComponentAware interface {
SetComponent(string)
}
type ConnectionAware interface {
SetConnectionIdentifier(string)
}
type AccountAware interface {
SetAccountIdentifier(string)
}
type PrefixFormatAware interface {
SetPrefixFormat(string)
}
type AccountLogger interface {
PrefixedLogger
ComponentAware
ConnectionAware
AccountAware
PrefixFormatAware
}
type AccountLoggerDecorator struct {
format string
component string
connIdentifier string
accIdentifier string
PrefixDecorator
}
func DecorateForAccount(base Logger, component, connIdentifier, accIdentifier string) AccountLogger {
return (&AccountLoggerDecorator{
PrefixDecorator: PrefixDecorator{
backend: base,
},
component: component,
connIdentifier: connIdentifier,
accIdentifier: accIdentifier,
}).updatePrefix()
}
// NewForAccount returns logger for account. It uses StandardLogger under the hood.
func NewForAccount(
transportCode, component, connIdentifier, accIdentifier string,
logLevel logging.Level,
logFormat logging.Formatter) AccountLogger {
return DecorateForAccount(NewStandard(transportCode, logLevel, logFormat),
component, connIdentifier, accIdentifier)
}
func (a *AccountLoggerDecorator) SetComponent(s string) {
a.component = s
a.updatePrefix()
}
func (a *AccountLoggerDecorator) SetConnectionIdentifier(s string) {
a.connIdentifier = s
a.updatePrefix()
}
func (a *AccountLoggerDecorator) SetAccountIdentifier(s string) {
a.accIdentifier = s
a.updatePrefix()
}
func (a *AccountLoggerDecorator) SetPrefixFormat(s string) {
a.format = s
a.updatePrefix()
}
func (a *AccountLoggerDecorator) updatePrefix() AccountLogger {
a.SetPrefix(fmt.Sprintf(a.prefixFormat(), a.component, a.connIdentifier, a.accIdentifier))
return a
}
func (a *AccountLoggerDecorator) prefixFormat() string {
if a.format == "" {
return DefaultAccountLoggerFormat
}
return a.format
}

View File

@ -1,98 +0,0 @@
package logger
import (
"bytes"
"fmt"
"testing"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
const (
testComponent = "ComponentName"
testConnectionID = "https://test.retailcrm.pro"
testAccountID = "@account_name"
)
type AccountLoggerDecoratorTest struct {
suite.Suite
buf *bytes.Buffer
logger AccountLogger
}
func TestAccountLoggerDecorator(t *testing.T) {
suite.Run(t, new(AccountLoggerDecoratorTest))
}
func TestNewForAccount(t *testing.T) {
buf := &bytes.Buffer{}
logger := NewForAccount("code", "component", "conn", "acc", logging.DEBUG, DefaultLogFormatter())
logger.(*AccountLoggerDecorator).backend.(*StandardLogger).
SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter()))
logger.Debugf("message %s", "text")
assert.Contains(t, buf.String(), fmt.Sprintf(DefaultAccountLoggerFormat+" message", "component", "conn", "acc"))
}
func (t *AccountLoggerDecoratorTest) SetupSuite() {
t.buf = &bytes.Buffer{}
t.logger = DecorateForAccount((&StandardLogger{}).
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())),
testComponent, testConnectionID, testAccountID)
}
func (t *AccountLoggerDecoratorTest) SetupTest() {
t.buf.Reset()
t.logger.SetComponent(testComponent)
t.logger.SetConnectionIdentifier(testConnectionID)
t.logger.SetAccountIdentifier(testAccountID)
t.logger.SetPrefixFormat(DefaultAccountLoggerFormat)
}
func (t *AccountLoggerDecoratorTest) Test_LogWithNewFormat() {
t.logger.SetPrefixFormat("[%s (%s: %s)] =>")
t.logger.Infof("test message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(),
fmt.Sprintf("[%s (%s: %s)] =>", testComponent, testConnectionID, testAccountID))
}
func (t *AccountLoggerDecoratorTest) Test_Log() {
t.logger.Infof("test message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(),
fmt.Sprintf(DefaultAccountLoggerFormat, testComponent, testConnectionID, testAccountID))
}
func (t *AccountLoggerDecoratorTest) Test_SetComponent() {
t.logger.SetComponent("NewComponent")
t.logger.Infof("test message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(),
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", testConnectionID, testAccountID))
}
func (t *AccountLoggerDecoratorTest) Test_SetConnectionIdentifier() {
t.logger.SetComponent("NewComponent")
t.logger.SetConnectionIdentifier("https://test.simla.com")
t.logger.Infof("test message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(),
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", testAccountID))
}
func (t *AccountLoggerDecoratorTest) Test_SetAccountIdentifier() {
t.logger.SetComponent("NewComponent")
t.logger.SetConnectionIdentifier("https://test.simla.com")
t.logger.SetAccountIdentifier("@new_account_name")
t.logger.Infof("test message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(),
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", "@new_account_name"))
}

View File

@ -0,0 +1,42 @@
package logger
import (
"fmt"
"go.uber.org/zap"
retailcrm "github.com/retailcrm/api-client-go/v2"
)
const (
apiDebugLogReq = "API Request: %s %s"
apiDebugLogResp = "API Response: %s"
)
type apiClientAdapter struct {
logger Logger
}
// APIClientAdapter returns BasicLogger that calls underlying logger.
func APIClientAdapter(logger Logger) retailcrm.BasicLogger {
return &apiClientAdapter{logger: logger}
}
// Printf data in the log using Debug method.
func (l *apiClientAdapter) Printf(format string, v ...interface{}) {
switch format {
case apiDebugLogReq:
var url, key string
if len(v) > 0 {
url = fmt.Sprint(v[0])
}
if len(v) > 1 {
key = fmt.Sprint(v[1])
}
l.logger.Debug("API Request", zap.String("url", url), zap.String("key", key))
case apiDebugLogResp:
l.logger.Debug("API Response", Body(v[0]))
default:
l.logger.Debug(fmt.Sprintf(format, v...))
}
}

View File

@ -0,0 +1,41 @@
package logger
import (
"net/http"
"testing"
"github.com/h2non/gock"
retailcrm "github.com/retailcrm/api-client-go/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIClientAdapter(t *testing.T) {
log := newJSONBufferedLogger(nil)
client := retailcrm.New("https://example.com", "test_key").WithLogger(APIClientAdapter(log.Logger()))
client.Debug = true
defer gock.Off()
gock.New("https://example.com").
Get("/api/credentials").
Reply(http.StatusOK).
JSON(retailcrm.CredentialResponse{Success: true})
_, _, err := client.APICredentials()
require.NoError(t, err)
entries, err := log.ScanAll()
require.NoError(t, err)
require.Len(t, entries, 2)
assert.Equal(t, "DEBUG", entries[0].LevelName)
assert.True(t, entries[0].DateTime.Valid)
assert.Equal(t, "API Request", entries[0].Message)
assert.Equal(t, "test_key", entries[0].Context["key"])
assert.Equal(t, "https://example.com/api/credentials", entries[0].Context["url"])
assert.Equal(t, "DEBUG", entries[1].LevelName)
assert.True(t, entries[1].DateTime.Valid)
assert.Equal(t, "API Response", entries[1].Message)
assert.Equal(t, map[string]interface{}{"success": true}, entries[1].Context["body"])
}

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

@ -0,0 +1,94 @@
package logger
import (
"fmt"
"io"
"net/http"
json "github.com/goccy/go-json"
"go.uber.org/zap"
)
// HandlerAttr represents the attribute name for the handler.
const HandlerAttr = "handler"
// ConnectionAttr represents the attribute name for the connection.
const ConnectionAttr = "connection"
// AccountAttr represents the attribute name for the account.
const AccountAttr = "account"
// CounterIDAttr represents the attribute name for the counter ID.
const CounterIDAttr = "counterId"
// ErrorAttr represents the attribute name for an error.
const ErrorAttr = "error"
// FailureMessageAttr represents the attribute name for a failure message.
const FailureMessageAttr = "failureMessage"
// BodyAttr represents the attribute name for the request body.
const BodyAttr = "body"
// HTTPMethodAttr represents the attribute name for the HTTP method.
const HTTPMethodAttr = "method"
// HTTPStatusAttr represents the attribute name for the HTTP status code.
const HTTPStatusAttr = "statusCode"
// HTTPStatusNameAttr represents the attribute name for the HTTP status name.
const HTTPStatusNameAttr = "statusName"
// Err returns a zap.Field with the given error value.
func Err(err any) zap.Field {
if err == nil {
return zap.String(ErrorAttr, "<nil>")
}
return zap.Any(ErrorAttr, err)
}
// Handler returns a zap.Field with the given handler name.
func Handler(name string) zap.Field {
return zap.String(HandlerAttr, name)
}
// HTTPStatusCode returns a zap.Field with the given HTTP status code.
func HTTPStatusCode(code int) zap.Field {
return zap.Int(HTTPStatusAttr, code)
}
// HTTPStatusName returns a zap.Field with the given HTTP status name.
func HTTPStatusName(code int) zap.Field {
return zap.String(HTTPStatusNameAttr, http.StatusText(code))
}
// Body returns a zap.Field with the given request body value.
func Body(val any) zap.Field {
switch item := val.(type) {
case string:
var m map[string]interface{}
if err := json.Unmarshal([]byte(item), &m); err == nil {
return zap.Any(BodyAttr, m)
}
return zap.String(BodyAttr, item)
case []byte:
var m interface{}
if err := json.Unmarshal(item, &m); err == nil {
return zap.Any(BodyAttr, m)
}
return zap.String(BodyAttr, string(item))
case io.Reader:
data, err := io.ReadAll(item)
if err != nil {
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
}
var m interface{}
if err := json.Unmarshal(data, &m); err == nil {
return zap.Any(BodyAttr, m)
}
return zap.String(BodyAttr, string(data))
default:
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
}
}

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

@ -0,0 +1,156 @@
package logger
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestErr(t *testing.T) {
var cases = []struct {
source interface{}
expected string
}{
{
source: nil,
expected: "<nil>",
},
{
source: errors.New("untimely error"),
expected: "untimely error",
},
}
for _, c := range cases {
val := Err(c.source)
assert.Equal(t, c.expected, func() string {
if val.String != "" {
return val.String
}
if val.Interface != nil {
return fmt.Sprintf("%s", val.Interface)
}
return ""
}())
assert.Equal(t, ErrorAttr, val.Key)
}
}
func TestHandler(t *testing.T) {
val := Handler("handlerName")
assert.Equal(t, HandlerAttr, val.Key)
assert.Equal(t, "handlerName", val.String)
}
func TestHTTPStatusCode(t *testing.T) {
val := HTTPStatusCode(http.StatusOK)
assert.Equal(t, HTTPStatusAttr, val.Key)
assert.Equal(t, http.StatusOK, int(val.Integer))
}
func TestHTTPStatusName(t *testing.T) {
val := HTTPStatusName(http.StatusOK)
assert.Equal(t, HTTPStatusNameAttr, val.Key)
assert.Equal(t, http.StatusText(http.StatusOK), val.String)
}
func TestBody(t *testing.T) {
var cases = []struct {
input interface{}
result interface{}
}{
{
input: "",
result: nil,
},
{
input: nil,
result: nil,
},
{
input: "ooga booga",
result: "ooga booga",
},
{
input: `{"success":true}`,
result: map[string]interface{}{"success": true},
},
{
input: []byte{},
result: nil,
},
{
input: nil,
result: nil,
},
{
input: []byte("ooga booga"),
result: "ooga booga",
},
{
input: []byte(`{"success":true}`),
result: map[string]interface{}{"success": true},
},
{
input: newReaderMock(func(p []byte) (n int, err error) {
return 0, io.EOF
}),
result: nil,
},
{
input: newReaderMockData([]byte{}),
result: nil,
},
{
input: newReaderMockData([]byte("ooga booga")),
result: "ooga booga",
},
{
input: newReaderMockData([]byte(`{"success":true}`)),
result: map[string]interface{}{"success": true},
},
}
for _, c := range cases {
val := Body(c.input)
assert.Equal(t, BodyAttr, val.Key)
switch assertion := c.result.(type) {
case string:
assert.Equal(t, assertion, val.String)
case int:
assert.Equal(t, assertion, int(val.Integer))
default:
assert.Equal(t, c.result, val.Interface)
}
}
}
type readerMock struct {
mock.Mock
}
func newReaderMock(cb func(p []byte) (n int, err error)) io.Reader {
r := &readerMock{}
r.On("Read", mock.Anything).Return(cb)
return r
}
func newReaderMockData(data []byte) io.Reader {
return newReaderMock(bytes.NewReader(data).Read)
}
func (m *readerMock) Read(p []byte) (n int, err error) {
args := m.Called(p)
out := args.Get(0)
if cb, ok := out.(func(p []byte) (n int, err error)); ok {
return cb(p)
}
return args.Int(0), args.Error(1)
}

View File

@ -0,0 +1,266 @@
package logger
import (
"bufio"
"bytes"
"encoding/json"
"io"
"os"
"sync"
"github.com/guregu/null/v5"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type logRecord struct {
LevelName string `json:"level_name"`
DateTime null.Time `json:"datetime"`
Caller string `json:"caller"`
Message string `json:"message"`
Handler string `json:"handler,omitempty"`
Connection string `json:"connection,omitempty"`
Account string `json:"account,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
}
type jSONRecordScanner struct {
scan *bufio.Scanner
buf *bufferLogger
}
func newJSONBufferedLogger(buf *bufferLogger) *jSONRecordScanner {
if buf == nil {
buf = newBufferLogger()
}
return &jSONRecordScanner{scan: bufio.NewScanner(buf), buf: buf}
}
func (s *jSONRecordScanner) ScanAll() ([]logRecord, error) {
var entries []logRecord
for s.scan.Scan() {
entry := logRecord{}
if err := json.Unmarshal(s.scan.Bytes(), &entry); err != nil {
return entries, err
}
entries = append(entries, entry)
}
return entries, nil
}
func (s *jSONRecordScanner) Logger() Logger {
return s.buf
}
// bufferLogger is an implementation of the BufferedLogger.
type bufferLogger struct {
Default
buf lockableBuffer
}
// NewBufferedLogger returns new BufferedLogger instance.
func newBufferLogger() *bufferLogger {
bl := &bufferLogger{}
bl.Logger = zap.New(
zapcore.NewCore(
NewJSONWithContextEncoder(
EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
return bl
}
func (l *bufferLogger) With(fields ...zapcore.Field) Logger {
return &bufferLogger{
Default: Default{
Logger: l.Logger.With(fields...),
},
}
}
func (l *bufferLogger) WithLazy(fields ...zapcore.Field) Logger {
return &bufferLogger{
Default: Default{
Logger: l.Logger.WithLazy(fields...),
},
}
}
func (l *bufferLogger) ForHandler(handler any) Logger {
return l.WithLazy(zap.Any(HandlerAttr, handler))
}
func (l *bufferLogger) ForConnection(conn any) Logger {
return l.WithLazy(zap.Any(ConnectionAttr, conn))
}
func (l *bufferLogger) ForAccount(acc any) Logger {
return l.WithLazy(zap.Any(AccountAttr, acc))
}
// Read bytes from the logger buffer. io.Reader implementation.
func (l *bufferLogger) Read(p []byte) (n int, err error) {
return l.buf.Read(p)
}
// String contents of the logger buffer. fmt.Stringer implementation.
func (l *bufferLogger) String() string {
return l.buf.String()
}
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
func (l *bufferLogger) Bytes() []byte {
return l.buf.Bytes()
}
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
func (l *bufferLogger) Reset() {
l.buf.Reset()
}
type lockableBuffer struct {
buf bytes.Buffer
rw sync.RWMutex
}
func (b *lockableBuffer) Bytes() []byte {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Bytes()
}
func (b *lockableBuffer) AvailableBuffer() []byte {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.AvailableBuffer()
}
func (b *lockableBuffer) String() string {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.String()
}
func (b *lockableBuffer) Len() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Len()
}
func (b *lockableBuffer) Cap() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Cap()
}
func (b *lockableBuffer) Available() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Available()
}
func (b *lockableBuffer) Truncate(n int) {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Truncate(n)
}
func (b *lockableBuffer) Reset() {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Reset()
}
func (b *lockableBuffer) Grow(n int) {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Grow(n)
}
func (b *lockableBuffer) Write(p []byte) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Write(p)
}
func (b *lockableBuffer) WriteString(s string) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteString(s)
}
func (b *lockableBuffer) ReadFrom(r io.Reader) (n int64, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadFrom(r)
}
func (b *lockableBuffer) WriteTo(w io.Writer) (n int64, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteTo(w)
}
func (b *lockableBuffer) WriteByte(c byte) error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteByte(c)
}
func (b *lockableBuffer) WriteRune(r rune) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteRune(r)
}
func (b *lockableBuffer) Read(p []byte) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Read(p)
}
func (b *lockableBuffer) Next(n int) []byte {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Next(n)
}
func (b *lockableBuffer) ReadByte() (byte, error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadByte()
}
func (b *lockableBuffer) ReadRune() (r rune, size int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadRune()
}
func (b *lockableBuffer) UnreadRune() error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.UnreadRune()
}
func (b *lockableBuffer) UnreadByte() error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.UnreadByte()
}
func (b *lockableBuffer) ReadBytes(delim byte) (line []byte, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadBytes(delim)
}
func (b *lockableBuffer) ReadString(delim byte) (line string, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadString(delim)
}
// Sync is a no-op.
func (b *lockableBuffer) Sync() error {
return nil
}

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

@ -0,0 +1,29 @@
// Copyright (c) 2016 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package logger
import "go.uber.org/zap/buffer"
var (
_pool = buffer.NewPool()
// GetBufferPool retrieves a buffer from the pool, creating one if necessary.
GetBufferPool = _pool.Get
)

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

@ -0,0 +1,100 @@
package logger
import (
"strconv"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger is a logging interface.
type Logger interface {
// With adds fields to the logger and returns a new logger with those fields.
With(fields ...zap.Field) Logger
// WithLazy adds fields to the logger lazily and returns a new logger with those fields.
WithLazy(fields ...zap.Field) Logger
// Level returns the logging level of the logger.
Level() zapcore.Level
// Check checks if the log message meets the given level.
Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry
// Log logs a message with the given level and fields.
Log(lvl zapcore.Level, msg string, fields ...zap.Field)
// Debug logs a debug-level message with the given fields.
Debug(msg string, fields ...zap.Field)
// Info logs an info-level message with the given fields.
Info(msg string, fields ...zap.Field)
// Warn logs a warning-level message with the given fields.
Warn(msg string, fields ...zap.Field)
// Error logs an error-level message with the given fields.
Error(msg string, fields ...zap.Field)
// DPanic logs a debug-panic-level message with the given fields and panics
// if the logger's panic level is set to a non-zero value.
DPanic(msg string, fields ...zap.Field)
// Panic logs a panic-level message with the given fields and panics immediately.
Panic(msg string, fields ...zap.Field)
// Fatal logs a fatal-level message with the given fields, then calls os.Exit(1).
Fatal(msg string, fields ...zap.Field)
// ForHandler returns a new logger that is associated with the given handler.
ForHandler(handler any) Logger
// ForConnection returns a new logger that is associated with the given connection.
ForConnection(conn any) Logger
// ForAccount returns a new logger that is associated with the given account.
ForAccount(acc any) Logger
// Sync returns an error if there's a problem writing log messages to disk, or nil if all writes were successful.
Sync() error
}
// Default is a default logger implementation.
type Default struct {
*zap.Logger
}
// NewDefault creates a new default logger with the given format and debug level.
func NewDefault(format string, debug bool) Logger {
return &Default{
Logger: NewZap(format, debug),
}
}
// With adds fields to the logger and returns a new logger with those fields.
func (l *Default) With(fields ...zap.Field) Logger {
return l.clone(l.Logger.With(fields...))
}
// WithLazy adds fields to the logger lazily and returns a new logger with those fields.
func (l *Default) WithLazy(fields ...zap.Field) Logger {
return l.clone(l.Logger.WithLazy(fields...))
}
// ForHandler returns a new logger that is associated with the given handler.
func (l *Default) ForHandler(handler any) Logger {
return l.WithLazy(zap.Any(HandlerAttr, handler))
}
// ForConnection returns a new logger that is associated with the given connection.
func (l *Default) ForConnection(conn any) Logger {
return l.WithLazy(zap.Any(ConnectionAttr, conn))
}
// ForAccount returns a new logger that is associated with the given account.
func (l *Default) ForAccount(acc any) Logger {
return l.WithLazy(zap.Any(AccountAttr, acc))
}
// clone creates a copy of the given logger.
func (l *Default) clone(log *zap.Logger) Logger {
return &Default{Logger: log}
}
// AnyZapFields converts an array of values to zap fields.
func AnyZapFields(args []interface{}) []zap.Field {
fields := make([]zap.Field, len(args))
for i := 0; i < len(fields); i++ {
if val, ok := args[i].(zap.Field); ok {
fields[i] = val
continue
}
fields[i] = zap.Any("arg"+strconv.Itoa(i), args[i])
}
return fields
}

View File

@ -0,0 +1,90 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
)
type TestDefaultSuite struct {
suite.Suite
}
func TestDefault(t *testing.T) {
suite.Run(t, new(TestDefaultSuite))
}
func (s *TestDefaultSuite) TestNewDefault_OK() {
jsonLog := NewDefault("json", false)
consoleLog := NewDefault("console", true)
s.Assert().NotNil(jsonLog)
s.Assert().NotNil(consoleLog)
}
func (s *TestDefaultSuite) TestNewDefault_Panic() {
s.Assert().PanicsWithValue("unknown logger format: rar", func() {
NewDefault("rar", false)
})
}
func (s *TestDefaultSuite) TestWith() {
log := newBufferLogger()
log.With(zap.String(HandlerAttr, "Handler")).Info("test")
items, err := newJSONBufferedLogger(log).ScanAll()
s.Require().NoError(err)
s.Require().Len(items, 1)
s.Assert().Equal("Handler", items[0].Handler)
}
func (s *TestDefaultSuite) TestWithLazy() {
log := newBufferLogger()
log.WithLazy(zap.String(HandlerAttr, "Handler")).Info("test")
items, err := newJSONBufferedLogger(log).ScanAll()
s.Require().NoError(err)
s.Require().Len(items, 1)
s.Assert().Equal("Handler", items[0].Handler)
}
func (s *TestDefaultSuite) TestForHandler() {
log := newBufferLogger()
log.ForHandler("Handler").Info("test")
items, err := newJSONBufferedLogger(log).ScanAll()
s.Require().NoError(err)
s.Require().Len(items, 1)
s.Assert().Equal("Handler", items[0].Handler)
}
func (s *TestDefaultSuite) TestForConnection() {
log := newBufferLogger()
log.ForConnection("connection").Info("test")
items, err := newJSONBufferedLogger(log).ScanAll()
s.Require().NoError(err)
s.Require().Len(items, 1)
s.Assert().Equal("connection", items[0].Connection)
}
func (s *TestDefaultSuite) TestForAccount() {
log := newBufferLogger()
log.ForAccount("account").Info("test")
items, err := newJSONBufferedLogger(log).ScanAll()
s.Require().NoError(err)
s.Require().Len(items, 1)
s.Assert().Equal("account", items[0].Account)
}
func TestAnyZapFields(t *testing.T) {
fields := AnyZapFields([]interface{}{zap.String("k0", "v0"), "ooga", "booga"})
require.Len(t, fields, 3)
assert.Equal(t, zap.String("k0", "v0"), fields[0])
assert.Equal(t, zap.String("arg1", "ooga"), fields[1])
assert.Equal(t, zap.String("arg2", "booga"), fields[2])
}

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

@ -0,0 +1,37 @@
package logger
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// GinMiddleware will construct Gin middleware which will log requests.
func GinMiddleware(log Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Process request
c.Next()
end := time.Now()
if raw != "" {
path = path + "?" + raw
}
log.Info("request",
zap.String(HandlerAttr, "GIN"),
zap.String("startTime", start.Format(time.RFC3339)),
zap.String("endTime", end.Format(time.RFC3339)),
zap.Any("latency", end.Sub(start)/time.Millisecond),
zap.String("remoteAddress", c.ClientIP()),
zap.String(HTTPMethodAttr, c.Request.Method),
zap.String("path", path),
zap.Int("bodySize", c.Writer.Size()),
)
}
}

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

@ -0,0 +1,38 @@
package logger
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGinMiddleware(t *testing.T) {
log := newBufferLogger()
rr := httptest.NewRecorder()
r := gin.New()
r.Use(GinMiddleware(log))
r.GET("/mine", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
r.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/mine", nil))
require.Equal(t, http.StatusOK, rr.Code)
items, err := newJSONBufferedLogger(log).ScanAll()
require.NoError(t, err)
require.Len(t, items, 1)
require.NotEmpty(t, items[0].Context)
assert.NotEmpty(t, items[0].Context["startTime"])
assert.NotEmpty(t, items[0].Context["endTime"])
assert.True(t, func() bool {
_, ok := items[0].Context["latency"]
return ok
}())
assert.NotEmpty(t, items[0].Context["remoteAddress"])
assert.NotEmpty(t, items[0].Context[HTTPMethodAttr])
assert.NotEmpty(t, items[0].Context["path"])
assert.NotEmpty(t, items[0].Context["bodySize"])
}

View File

@ -0,0 +1,640 @@
// Copyright (c) 2016 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// nolint
package logger
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math"
"time"
"unicode/utf8"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/buffer"
)
// For JSON-escaping; see jsonWithContextEncoder.safeAddString below.
const _hex = "0123456789abcdef"
var _jsonWithContextPool = NewPool(func() *jsonWithContextEncoder {
return &jsonWithContextEncoder{}
})
func init() {
err := zap.RegisterEncoder("json-with-context", func(config zapcore.EncoderConfig) (zapcore.Encoder, error) {
return NewJSONWithContextEncoder(config), nil
})
if err != nil {
panic(err)
}
}
func putJSONWithContextEncoder(enc *jsonWithContextEncoder) {
if enc.reflectBuf != nil {
enc.reflectBuf.Free()
}
enc.EncoderConfig = nil
enc.buf = nil
enc.spaced = false
enc.openNamespaces = 0
enc.reflectBuf = nil
enc.reflectEnc = nil
_jsonWithContextPool.Put(enc)
}
type jsonWithContextEncoder struct {
*zapcore.EncoderConfig
buf *buffer.Buffer
spaced bool // include spaces after colons and commas
openNamespaces int
// for encoding generic values by reflection
reflectBuf *buffer.Buffer
reflectEnc zapcore.ReflectedEncoder
}
// NewJSONWithContextEncoder creates a fast, low-allocation JSON encoder. The encoder
// appropriately escapes all field keys and values.
//
// Note that the encoder doesn't deduplicate keys, so it's possible to produce
// a message like
//
// {"foo":"bar","foo":"baz"}
//
// This is permitted by the JSON specification, but not encouraged. Many
// libraries will ignore duplicate key-value pairs (typically keeping the last
// pair) when unmarshaling, but users should attempt to avoid adding duplicate
// keys.
func NewJSONWithContextEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
return newJSONWithContextEncoder(cfg, false)
}
func newJSONWithContextEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonWithContextEncoder {
if cfg.SkipLineEnding {
cfg.LineEnding = ""
} else if cfg.LineEnding == "" {
cfg.LineEnding = zapcore.DefaultLineEnding
}
// If no EncoderConfig.NewReflectedEncoder is provided by the user, then use default
if cfg.NewReflectedEncoder == nil {
cfg.NewReflectedEncoder = defaultReflectedEncoder
}
return &jsonWithContextEncoder{
EncoderConfig: &cfg,
buf: GetBufferPool(),
spaced: spaced,
}
}
func defaultReflectedEncoder(w io.Writer) zapcore.ReflectedEncoder {
enc := json.NewEncoder(w)
// For consistency with our custom JSON encoder.
enc.SetEscapeHTML(false)
return enc
}
func (enc *jsonWithContextEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
enc.addKey(key)
return enc.AppendArray(arr)
}
func (enc *jsonWithContextEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
enc.addKey(key)
return enc.AppendObject(obj)
}
func (enc *jsonWithContextEncoder) AddBinary(key string, val []byte) {
enc.AddString(key, base64.StdEncoding.EncodeToString(val))
}
func (enc *jsonWithContextEncoder) AddByteString(key string, val []byte) {
enc.addKey(key)
enc.AppendByteString(val)
}
func (enc *jsonWithContextEncoder) AddBool(key string, val bool) {
enc.addKey(key)
enc.AppendBool(val)
}
func (enc *jsonWithContextEncoder) AddComplex128(key string, val complex128) {
enc.addKey(key)
enc.AppendComplex128(val)
}
func (enc *jsonWithContextEncoder) AddComplex64(key string, val complex64) {
enc.addKey(key)
enc.AppendComplex64(val)
}
func (enc *jsonWithContextEncoder) AddDuration(key string, val time.Duration) {
enc.addKey(key)
enc.AppendDuration(val)
}
func (enc *jsonWithContextEncoder) AddFloat64(key string, val float64) {
enc.addKey(key)
enc.AppendFloat64(val)
}
func (enc *jsonWithContextEncoder) AddFloat32(key string, val float32) {
enc.addKey(key)
enc.AppendFloat32(val)
}
func (enc *jsonWithContextEncoder) AddInt64(key string, val int64) {
enc.addKey(key)
enc.AppendInt64(val)
}
func (enc *jsonWithContextEncoder) resetReflectBuf() {
if enc.reflectBuf == nil {
enc.reflectBuf = GetBufferPool()
enc.reflectEnc = enc.NewReflectedEncoder(enc.reflectBuf)
} else {
enc.reflectBuf.Reset()
}
}
var nullLiteralBytes = []byte("null")
// Only invoke the standard JSON encoder if there is actually something to
// encode; otherwise write JSON null literal directly.
func (enc *jsonWithContextEncoder) encodeReflected(obj interface{}) ([]byte, error) {
if obj == nil {
return nullLiteralBytes, nil
}
enc.resetReflectBuf()
if err := enc.reflectEnc.Encode(obj); err != nil {
return nil, err
}
enc.reflectBuf.TrimNewline()
return enc.reflectBuf.Bytes(), nil
}
func (enc *jsonWithContextEncoder) AddReflected(key string, obj interface{}) error {
valueBytes, err := enc.encodeReflected(obj)
if err != nil {
return err
}
enc.addKey(key)
_, err = enc.buf.Write(valueBytes)
return err
}
func (enc *jsonWithContextEncoder) OpenNamespace(key string) {
enc.addKey(key)
enc.buf.AppendByte('{')
enc.openNamespaces++
}
func (enc *jsonWithContextEncoder) AddString(key, val string) {
enc.addKey(key)
enc.AppendString(val)
}
func (enc *jsonWithContextEncoder) AddTime(key string, val time.Time) {
enc.addKey(key)
enc.AppendTime(val)
}
func (enc *jsonWithContextEncoder) AddUint64(key string, val uint64) {
enc.addKey(key)
enc.AppendUint64(val)
}
func (enc *jsonWithContextEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
enc.addElementSeparator()
enc.buf.AppendByte('[')
err := arr.MarshalLogArray(enc)
enc.buf.AppendByte(']')
return err
}
func (enc *jsonWithContextEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
// Close ONLY new openNamespaces that are created during
// AppendObject().
old := enc.openNamespaces
enc.openNamespaces = 0
enc.addElementSeparator()
enc.buf.AppendByte('{')
err := obj.MarshalLogObject(enc)
enc.buf.AppendByte('}')
enc.closeOpenNamespaces()
enc.openNamespaces = old
return err
}
func (enc *jsonWithContextEncoder) AppendBool(val bool) {
enc.addElementSeparator()
enc.buf.AppendBool(val)
}
func (enc *jsonWithContextEncoder) AppendByteString(val []byte) {
enc.addElementSeparator()
enc.buf.AppendByte('"')
enc.safeAddByteString(val)
enc.buf.AppendByte('"')
}
// appendComplex appends the encoded form of the provided complex128 value.
// precision specifies the encoding precision for the real and imaginary
// components of the complex number.
func (enc *jsonWithContextEncoder) appendComplex(val complex128, precision int) {
enc.addElementSeparator()
// Cast to a platform-independent, fixed-size type.
r, i := float64(real(val)), float64(imag(val))
enc.buf.AppendByte('"')
// Because we're always in a quoted string, we can use strconv without
// special-casing NaN and +/-Inf.
enc.buf.AppendFloat(r, precision)
// If imaginary part is less than 0, minus (-) sign is added by default
// by AppendFloat.
if i >= 0 {
enc.buf.AppendByte('+')
}
enc.buf.AppendFloat(i, precision)
enc.buf.AppendByte('i')
enc.buf.AppendByte('"')
}
func (enc *jsonWithContextEncoder) AppendDuration(val time.Duration) {
cur := enc.buf.Len()
if e := enc.EncodeDuration; e != nil {
e(val, enc)
}
if cur == enc.buf.Len() {
// User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
// JSON valid.
enc.AppendInt64(int64(val))
}
}
func (enc *jsonWithContextEncoder) AppendInt64(val int64) {
enc.addElementSeparator()
enc.buf.AppendInt(val)
}
func (enc *jsonWithContextEncoder) AppendReflected(val interface{}) error {
valueBytes, err := enc.encodeReflected(val)
if err != nil {
return err
}
enc.addElementSeparator()
_, err = enc.buf.Write(valueBytes)
return err
}
func (enc *jsonWithContextEncoder) AppendString(val string) {
enc.addElementSeparator()
enc.buf.AppendByte('"')
enc.safeAddString(val)
enc.buf.AppendByte('"')
}
func (enc *jsonWithContextEncoder) AppendTimeLayout(time time.Time, layout string) {
enc.addElementSeparator()
enc.buf.AppendByte('"')
enc.buf.AppendTime(time, layout)
enc.buf.AppendByte('"')
}
func (enc *jsonWithContextEncoder) AppendTime(val time.Time) {
cur := enc.buf.Len()
if e := enc.EncodeTime; e != nil {
e(val, enc)
}
if cur == enc.buf.Len() {
// User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
// output JSON valid.
enc.AppendInt64(val.UnixNano())
}
}
func (enc *jsonWithContextEncoder) AppendUint64(val uint64) {
enc.addElementSeparator()
enc.buf.AppendUint(val)
}
func (enc *jsonWithContextEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
func (enc *jsonWithContextEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
func (enc *jsonWithContextEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
func (enc *jsonWithContextEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
func (enc *jsonWithContextEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
func (enc *jsonWithContextEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
func (enc *jsonWithContextEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
func (enc *jsonWithContextEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
func (enc *jsonWithContextEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
func (enc *jsonWithContextEncoder) AppendComplex64(v complex64) { enc.appendComplex(complex128(v), 32) }
func (enc *jsonWithContextEncoder) AppendComplex128(v complex128) {
enc.appendComplex(complex128(v), 64)
}
func (enc *jsonWithContextEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
func (enc *jsonWithContextEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
func (enc *jsonWithContextEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
func (enc *jsonWithContextEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
func (enc *jsonWithContextEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
func (enc *jsonWithContextEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
func (enc *jsonWithContextEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
func (enc *jsonWithContextEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
func (enc *jsonWithContextEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
func (enc *jsonWithContextEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
func (enc *jsonWithContextEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
func (enc *jsonWithContextEncoder) Clone() zapcore.Encoder {
clone := enc.clone()
clone.buf.Write(enc.buf.Bytes())
return clone
}
func (enc *jsonWithContextEncoder) clone() *jsonWithContextEncoder {
clone := _jsonWithContextPool.Get()
clone.EncoderConfig = enc.EncoderConfig
clone.spaced = enc.spaced
clone.openNamespaces = enc.openNamespaces
clone.buf = GetBufferPool()
return clone
}
func (enc *jsonWithContextEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
final := enc.clone()
final.buf.AppendByte('{')
if final.LevelKey != "" && final.EncodeLevel != nil {
final.addKey(final.LevelKey)
cur := final.buf.Len()
final.EncodeLevel(ent.Level, final)
if cur == final.buf.Len() {
// User-supplied EncodeLevel was a no-op. Fall back to strings to keep
// output JSON valid.
final.AppendString(ent.Level.String())
}
}
if final.TimeKey != "" {
final.AddTime(final.TimeKey, ent.Time)
}
if ent.LoggerName != "" && final.NameKey != "" {
final.addKey(final.NameKey)
cur := final.buf.Len()
nameEncoder := final.EncodeName
// if no name encoder provided, fall back to FullNameEncoder for backwards
// compatibility
if nameEncoder == nil {
nameEncoder = zapcore.FullNameEncoder
}
nameEncoder(ent.LoggerName, final)
if cur == final.buf.Len() {
// User-supplied EncodeName was a no-op. Fall back to strings to
// keep output JSON valid.
final.AppendString(ent.LoggerName)
}
}
if ent.Caller.Defined {
if final.CallerKey != "" {
final.addKey(final.CallerKey)
cur := final.buf.Len()
final.EncodeCaller(ent.Caller, final)
if cur == final.buf.Len() {
// User-supplied EncodeCaller was a no-op. Fall back to strings to
// keep output JSON valid.
final.AppendString(ent.Caller.String())
}
}
if final.FunctionKey != "" {
final.addKey(final.FunctionKey)
final.AppendString(ent.Caller.Function)
}
}
if final.MessageKey != "" {
final.addKey(enc.MessageKey)
final.AppendString(ent.Message)
}
if enc.buf.Len() > 0 {
final.addElementSeparator()
final.buf.Write(enc.buf.Bytes())
}
addFields(final, fields)
final.closeOpenNamespaces()
if ent.Stack != "" && final.StacktraceKey != "" {
final.AddString(final.StacktraceKey, ent.Stack)
}
final.buf.AppendByte('}')
final.buf.AppendString(final.LineEnding)
ret := final.buf
putJSONWithContextEncoder(final)
return ret, nil
}
func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
m := make(map[string]interface{})
hasEntries := false
for _, f := range fields {
switch f.Key {
case HandlerAttr, ConnectionAttr, AccountAttr:
f.AddTo(enc)
default:
hasEntries = true
if f.Interface != nil {
switch t := f.Interface.(type) {
case fmt.Stringer:
m[f.Key] = t.String()
case fmt.GoStringer:
m[f.Key] = t.GoString()
case error:
m[f.Key] = t.Error()
default:
m[f.Key] = f.Interface
}
continue
}
if f.String != "" {
m[f.Key] = f.String
continue
}
m[f.Key] = f.Integer
}
}
if hasEntries {
zap.Any("context", m).AddTo(enc)
}
}
func (enc *jsonWithContextEncoder) truncate() {
enc.buf.Reset()
}
func (enc *jsonWithContextEncoder) closeOpenNamespaces() {
for i := 0; i < enc.openNamespaces; i++ {
enc.buf.AppendByte('}')
}
enc.openNamespaces = 0
}
func (enc *jsonWithContextEncoder) addKey(key string) {
enc.addElementSeparator()
enc.buf.AppendByte('"')
enc.safeAddString(key)
enc.buf.AppendByte('"')
enc.buf.AppendByte(':')
if enc.spaced {
enc.buf.AppendByte(' ')
}
}
func (enc *jsonWithContextEncoder) addElementSeparator() {
last := enc.buf.Len() - 1
if last < 0 {
return
}
switch enc.buf.Bytes()[last] {
case '{', '[', ':', ',', ' ':
return
default:
enc.buf.AppendByte(',')
if enc.spaced {
enc.buf.AppendByte(' ')
}
}
}
func (enc *jsonWithContextEncoder) appendFloat(val float64, bitSize int) {
enc.addElementSeparator()
switch {
case math.IsNaN(val):
enc.buf.AppendString(`"NaN"`)
case math.IsInf(val, 1):
enc.buf.AppendString(`"+Inf"`)
case math.IsInf(val, -1):
enc.buf.AppendString(`"-Inf"`)
default:
enc.buf.AppendFloat(val, bitSize)
}
}
// safeAddString JSON-escapes a string and appends it to the internal buffer.
// Unlike the standard library's encoder, it doesn't attempt to protect the
// user from browser vulnerabilities or JSONP-related problems.
func (enc *jsonWithContextEncoder) safeAddString(s string) {
safeAppendStringLike(
(*buffer.Buffer).AppendString,
utf8.DecodeRuneInString,
enc.buf,
s,
)
}
// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
func (enc *jsonWithContextEncoder) safeAddByteString(s []byte) {
safeAppendStringLike(
(*buffer.Buffer).AppendBytes,
utf8.DecodeRune,
enc.buf,
s,
)
}
// safeAppendStringLike is a generic implementation of safeAddString and safeAddByteString.
// It appends a string or byte slice to the buffer, escaping all special characters.
func safeAppendStringLike[S []byte | string](
// appendTo appends this string-like object to the buffer.
appendTo func(*buffer.Buffer, S),
// decodeRune decodes the next rune from the string-like object
// and returns its value and width in bytes.
decodeRune func(S) (rune, int),
buf *buffer.Buffer,
s S,
) {
// The encoding logic below works by skipping over characters
// that can be safely copied as-is,
// until a character is found that needs special handling.
// At that point, we copy everything we've seen so far,
// and then handle that special character.
//
// last is the index of the last byte that was copied to the buffer.
last := 0
for i := 0; i < len(s); {
if s[i] >= utf8.RuneSelf {
// Character >= RuneSelf may be part of a multi-byte rune.
// They need to be decoded before we can decide how to handle them.
r, size := decodeRune(s[i:])
if r != utf8.RuneError || size != 1 {
// No special handling required.
// Skip over this rune and continue.
i += size
continue
}
// Invalid UTF-8 sequence.
// Replace it with the Unicode replacement character.
appendTo(buf, s[last:i])
buf.AppendString(`\ufffd`)
i++
last = i
} else {
// Character < RuneSelf is a single-byte UTF-8 rune.
if s[i] >= 0x20 && s[i] != '\\' && s[i] != '"' {
// No escaping necessary.
// Skip over this character and continue.
i++
continue
}
// This character needs to be escaped.
appendTo(buf, s[last:i])
switch s[i] {
case '\\', '"':
buf.AppendByte('\\')
buf.AppendByte(s[i])
case '\n':
buf.AppendByte('\\')
buf.AppendByte('n')
case '\r':
buf.AppendByte('\\')
buf.AppendByte('r')
case '\t':
buf.AppendByte('\\')
buf.AppendByte('t')
default:
// Encode bytes < 0x20, except for the escape sequences above.
buf.AppendString(`\u00`)
buf.AppendByte(_hex[s[i]>>4])
buf.AppendByte(_hex[s[i]&0xF])
}
i++
last = i
}
}
// add remaining
appendTo(buf, s[last:])
}

View File

@ -1,205 +0,0 @@
package logger
import (
"io"
"os"
"sync"
"github.com/op/go-logging"
)
// Logger contains methods which should be present in logger implementation.
type Logger interface {
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Panic(args ...interface{})
Panicf(format string, args ...interface{})
Critical(args ...interface{})
Criticalf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Warning(args ...interface{})
Warningf(format string, args ...interface{})
Notice(args ...interface{})
Noticef(format string, args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Debug(args ...interface{})
Debugf(format string, args ...interface{})
}
// StandardLogger is a default implementation of Logger. Uses github.com/op/go-logging under the hood.
// This logger can prevent any write operations (disabled by default, use .Exclusive() method to enable).
type StandardLogger struct {
logger *logging.Logger
mutex *sync.RWMutex
}
// NewStandard will create new StandardLogger with specified formatter.
// Usage:
// logger := NewLogger("telegram", logging.ERROR, DefaultLogFormatter())
func NewStandard(transportCode string, logLevel logging.Level, logFormat logging.Formatter) *StandardLogger {
return &StandardLogger{
logger: NewBase(os.Stdout, transportCode, logLevel, logFormat),
}
}
// NewBase is a constructor for underlying logger in the StandardLogger struct.
func NewBase(out io.Writer, transportCode string, logLevel logging.Level, logFormat logging.Formatter) *logging.Logger {
logger := logging.MustGetLogger(transportCode)
logBackend := logging.NewLogBackend(out, "", 0)
formatBackend := logging.NewBackendFormatter(logBackend, logFormat)
backend1Leveled := logging.AddModuleLevel(formatBackend)
backend1Leveled.SetLevel(logLevel, "")
logger.SetBackend(backend1Leveled)
return logger
}
// DefaultLogFormatter will return default formatter for logs.
func DefaultLogFormatter() logging.Formatter {
return logging.MustStringFormatter(
`%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`,
)
}
// Exclusive makes logger goroutine-safe.
func (l *StandardLogger) Exclusive() *StandardLogger {
if l.mutex == nil {
l.mutex = &sync.RWMutex{}
}
return l
}
// SetBaseLogger replaces base logger with the provided instance.
func (l *StandardLogger) SetBaseLogger(logger *logging.Logger) *StandardLogger {
l.logger = logger
return l
}
// lock locks logger.
func (l *StandardLogger) lock() {
if l.mutex != nil {
l.mutex.Lock()
}
}
// unlock unlocks logger.
func (l *StandardLogger) unlock() {
if l.mutex != nil {
l.mutex.Unlock()
}
}
// Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1).
func (l *StandardLogger) Fatal(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Fatal(args...)
}
// Fatalf is equivalent to l.Critical followed by a call to os.Exit(1).
func (l *StandardLogger) Fatalf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Fatalf(format, args...)
}
// Panic is equivalent to l.Critical(fmt.Sprint()) followed by a call to panic().
func (l *StandardLogger) Panic(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Panic(args...)
}
// Panicf is equivalent to l.Critical followed by a call to panic().
func (l *StandardLogger) Panicf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Panicf(format, args...)
}
// Critical logs a message using CRITICAL as log level.
func (l *StandardLogger) Critical(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Critical(args...)
}
// Criticalf logs a message using CRITICAL as log level.
func (l *StandardLogger) Criticalf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Criticalf(format, args...)
}
// Error logs a message using ERROR as log level.
func (l *StandardLogger) Error(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Error(args...)
}
// Errorf logs a message using ERROR as log level.
func (l *StandardLogger) Errorf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Errorf(format, args...)
}
// Warning logs a message using WARNING as log level.
func (l *StandardLogger) Warning(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Warning(args...)
}
// Warningf logs a message using WARNING as log level.
func (l *StandardLogger) Warningf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Warningf(format, args...)
}
// Notice logs a message using NOTICE as log level.
func (l *StandardLogger) Notice(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Notice(args...)
}
// Noticef logs a message using NOTICE as log level.
func (l *StandardLogger) Noticef(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Noticef(format, args...)
}
// Info logs a message using INFO as log level.
func (l *StandardLogger) Info(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Info(args...)
}
// Infof logs a message using INFO as log level.
func (l *StandardLogger) Infof(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Infof(format, args...)
}
// Debug logs a message using DEBUG as log level.
func (l *StandardLogger) Debug(args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Debug(args...)
}
// Debugf logs a message using DEBUG as log level.
func (l *StandardLogger) Debugf(format string, args ...interface{}) {
l.lock()
defer l.unlock()
l.logger.Debugf(format, args...)
}

View File

@ -1,168 +0,0 @@
package logger
import (
"bytes"
"testing"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type StandardLoggerTest struct {
suite.Suite
logger *StandardLogger
buf *bytes.Buffer
}
func TestLogger_NewLogger(t *testing.T) {
logger := NewStandard("code", logging.DEBUG, DefaultLogFormatter())
assert.NotNil(t, logger)
}
func TestLogger_DefaultLogFormatter(t *testing.T) {
formatter := DefaultLogFormatter()
assert.NotNil(t, formatter)
assert.IsType(t, logging.MustStringFormatter(`%{message}`), formatter)
}
func Test_Logger(t *testing.T) {
suite.Run(t, new(StandardLoggerTest))
}
func (t *StandardLoggerTest) SetupSuite() {
t.buf = &bytes.Buffer{}
t.logger = (&StandardLogger{}).
Exclusive().
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter()))
}
func (t *StandardLoggerTest) SetupTest() {
t.buf.Reset()
}
// TODO Cover Fatal and Fatalf (implementation below is no-op)
// func (t *StandardLoggerTest) Test_Fatal() {
// if os.Getenv("FLAG") == "1" {
// t.logger.Fatal("test", "fatal")
// return
// }
// cmd := exec.Command(os.Args[0], "-test.run=TestGetConfig")
// cmd.Env = append(os.Environ(), "FLAG=1")
// err := cmd.Run()
// e, ok := err.(*exec.ExitError)
// expectedErrorString := "test fatal"
// t.Assert().Equal(true, ok)
// t.Assert().Equal(expectedErrorString, e.Error())
// }
func (t *StandardLoggerTest) Test_Panic() {
defer func() {
t.Assert().NotNil(recover())
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), "panic")
}()
t.logger.Panic("panic")
}
func (t *StandardLoggerTest) Test_Panicf() {
defer func() {
t.Assert().NotNil(recover())
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), "panicf")
}()
t.logger.Panicf("panicf")
}
func (t *StandardLoggerTest) Test_Critical() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), "critical")
}()
t.logger.Critical("critical")
}
func (t *StandardLoggerTest) Test_Criticalf() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), "critical")
}()
t.logger.Criticalf("critical")
}
func (t *StandardLoggerTest) Test_Warning() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "WARN")
t.Assert().Contains(t.buf.String(), "warning")
}()
t.logger.Warning("warning")
}
func (t *StandardLoggerTest) Test_Notice() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "NOTI")
t.Assert().Contains(t.buf.String(), "notice")
}()
t.logger.Notice("notice")
}
func (t *StandardLoggerTest) Test_Info() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), "info")
}()
t.logger.Info("info")
}
func (t *StandardLoggerTest) Test_Debug() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "DEBU")
t.Assert().Contains(t.buf.String(), "debug")
}()
t.logger.Debug("debug")
}
func (t *StandardLoggerTest) Test_Warningf() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "WARN")
t.Assert().Contains(t.buf.String(), "warning")
}()
t.logger.Warningf("%s", "warning")
}
func (t *StandardLoggerTest) Test_Noticef() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "NOTI")
t.Assert().Contains(t.buf.String(), "notice")
}()
t.logger.Noticef("%s", "notice")
}
func (t *StandardLoggerTest) Test_Infof() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), "info")
}()
t.logger.Infof("%s", "info")
}
func (t *StandardLoggerTest) Test_Debugf() {
defer func() {
t.Require().Nil(recover())
t.Assert().Contains(t.buf.String(), "DEBU")
t.Assert().Contains(t.buf.String(), "debug")
}()
t.logger.Debugf("%s", "debug")
}

View File

@ -0,0 +1,60 @@
package logger
import (
"fmt"
"go.uber.org/zap"
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
)
const (
mgDebugLogReq = "MG TRANSPORT API Request: %s %s %s %v"
mgDebugLogReqFile = "MG TRANSPORT API Request: %s %s %s [file data]"
mgDebugLogResp = "MG TRANSPORT API Response: %s"
)
type mgTransportClientAdapter struct {
log Logger
}
// MGTransportClientAdapter constructs an adapter that will log MG requests and responses.
func MGTransportClientAdapter(log Logger) v1.BasicLogger {
return &mgTransportClientAdapter{log: log}
}
// Debugf writes a message with Debug level.
func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) {
var body interface{}
switch msg {
case mgDebugLogReqFile:
body = "[file data]"
fallthrough
case mgDebugLogReq:
var method, uri, token string
if len(args) > 0 {
method = fmt.Sprint(args[0])
}
if len(args) > 1 {
uri = fmt.Sprint(args[1])
}
if len(args) > 2 { // nolint:gomnd
token = fmt.Sprint(args[2])
}
if len(args) > 3 { // nolint:gomnd
body = args[3]
}
m.log.Debug("MG TRANSPORT API Request",
zap.String(HTTPMethodAttr, method), zap.String("url", uri),
zap.String("token", token), Body(body))
case mgDebugLogResp:
m.log.Debug("MG TRANSPORT API Response", Body(args[0]))
default:
m.log.Debug(fmt.Sprintf(msg, args...))
}
}
// Printf is a v1.BasicLogger implementation.
func (m *mgTransportClientAdapter) Printf(msg string, args ...interface{}) {
m.Debugf(msg, args...)
}

View File

@ -0,0 +1,45 @@
package logger
import (
"net/http"
"testing"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
)
func TestMGTransportClientAdapter(t *testing.T) {
httpClient := &http.Client{}
log := newJSONBufferedLogger(nil)
client := v1.NewWithClient("https://mg.dev", "test_token", httpClient).
WithLogger(MGTransportClientAdapter(log.Logger()))
client.Debug = true
defer gock.Off()
gock.New("https://mg.dev").
Get("/api/transport/v1/channels").
Reply(http.StatusOK).
JSON([]v1.ChannelListItem{{ID: 123}})
_, _, err := client.TransportChannels(v1.Channels{})
require.NoError(t, err)
entries, err := log.ScanAll()
require.NoError(t, err)
require.Len(t, entries, 2)
assert.Equal(t, "DEBUG", entries[0].LevelName)
assert.True(t, entries[0].DateTime.Valid)
assert.Equal(t, "MG TRANSPORT API Request", entries[0].Message)
assert.Equal(t, http.MethodGet, entries[0].Context["method"])
assert.Equal(t, "test_token", entries[0].Context["token"])
assert.Equal(t, "https://mg.dev/api/transport/v1/channels?", entries[0].Context["url"])
assert.Equal(t, "DEBUG", entries[1].LevelName)
assert.True(t, entries[1].DateTime.Valid)
assert.Equal(t, "MG TRANSPORT API Response", entries[1].Message)
assert.Equal(t, float64(123), entries[1].Context["body"].([]interface{})[0].(map[string]interface{})["id"])
}

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

@ -0,0 +1,62 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Nil logger doesn't do anything.
type Nil struct{}
// NewNil constructs new *Nil.
func NewNil() Logger {
return &Nil{}
}
func (l *Nil) With(_ ...zap.Field) Logger {
return l
}
func (l *Nil) WithLazy(_ ...zap.Field) Logger {
return l
}
func (l *Nil) Level() zapcore.Level {
return zapcore.DebugLevel
}
func (l *Nil) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
return &zapcore.CheckedEntry{}
}
func (l *Nil) Log(_ zapcore.Level, _ string, _ ...zap.Field) {}
func (l *Nil) Debug(_ string, _ ...zap.Field) {}
func (l *Nil) Info(_ string, _ ...zap.Field) {}
func (l *Nil) Warn(_ string, _ ...zap.Field) {}
func (l *Nil) Error(_ string, _ ...zap.Field) {}
func (l *Nil) DPanic(_ string, _ ...zap.Field) {}
func (l *Nil) Panic(_ string, _ ...zap.Field) {}
func (l *Nil) Fatal(_ string, _ ...zap.Field) {}
func (l *Nil) ForHandler(_ any) Logger {
return l
}
func (l *Nil) ForConnection(_ any) Logger {
return l
}
func (l *Nil) ForAccount(_ any) Logger {
return l
}
func (l *Nil) Sync() error {
return nil
}

View File

@ -1,45 +0,0 @@
package logger
import (
"fmt"
"os"
)
// Nil provides Logger implementation that does almost nothing when called.
// Panic, Panicf, Fatal and Fatalf methods still cause panic and immediate program termination respectively.
// All other methods won't do anything at all.
type Nil struct{}
func (n Nil) Fatal(args ...interface{}) {
os.Exit(1)
}
func (n Nil) Fatalf(format string, args ...interface{}) {
os.Exit(1)
}
func (n Nil) Panic(args ...interface{}) {
panic(fmt.Sprint(args...))
}
func (n Nil) Panicf(format string, args ...interface{}) {
panic(fmt.Sprintf(format, args...))
}
func (n Nil) Critical(args ...interface{}) {}
func (n Nil) Criticalf(format string, args ...interface{}) {}
func (n Nil) Error(args ...interface{}) {}
func (n Nil) Errorf(format string, args ...interface{}) {}
func (n Nil) Warning(args ...interface{}) {}
func (n Nil) Warningf(format string, args ...interface{}) {}
func (n Nil) Notice(args ...interface{}) {}
func (n Nil) Noticef(format string, args ...interface{}) {}
func (n Nil) Info(args ...interface{}) {}
func (n Nil) Infof(format string, args ...interface{}) {}
func (n Nil) Debug(args ...interface{}) {}
func (n Nil) Debugf(format string, args ...interface{}) {}
// NewNil is a Nil logger constructor.
func NewNil() Logger {
return &Nil{}
}

View File

@ -1,91 +0,0 @@
package logger
import (
"bytes"
"io"
"os"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type NilTest struct {
suite.Suite
logger Logger
realStdout *os.File
r *os.File
w *os.File
}
func TestNilLogger(t *testing.T) {
suite.Run(t, new(NilTest))
}
func (t *NilTest) SetupSuite() {
t.logger = NewNil()
}
func (t *NilTest) SetupTest() {
t.realStdout = os.Stdout
t.r, t.w, _ = os.Pipe()
os.Stdout = t.w
}
func (t *NilTest) TearDownTest() {
if t.realStdout != nil {
t.Require().NoError(t.w.Close())
os.Stdout = t.realStdout
}
}
func (t *NilTest) readStdout() string {
outC := make(chan string)
go func() {
var buf bytes.Buffer
_, err := io.Copy(&buf, t.r)
t.Require().NoError(err)
outC <- buf.String()
close(outC)
}()
t.Require().NoError(t.w.Close())
os.Stdout = t.realStdout
t.realStdout = nil
select {
case c := <-outC:
return c
case <-time.After(time.Second):
return ""
}
}
func (t *NilTest) Test_Noop() {
t.logger.Critical("message")
t.logger.Criticalf("message")
t.logger.Error("message")
t.logger.Errorf("message")
t.logger.Warning("message")
t.logger.Warningf("message")
t.logger.Notice("message")
t.logger.Noticef("message")
t.logger.Info("message")
t.logger.Infof("message")
t.logger.Debug("message")
t.logger.Debugf("message")
t.Assert().Empty(t.readStdout())
}
func (t *NilTest) Test_Panic() {
t.Assert().Panics(func() {
t.logger.Panic("")
})
}
func (t *NilTest) Test_Panicf() {
t.Assert().Panics(func() {
t.logger.Panicf("")
})
}

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

@ -0,0 +1,57 @@
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package logger
import (
"sync"
)
// A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed
// object pooling.
//
// Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will
// not be detected, so all internal pool use must take care to only store
// pointer types.
type Pool[T any] struct {
pool sync.Pool
}
// NewPool returns a new [Pool] for T, and will use fn to construct new Ts when
// the pool is empty.
func NewPool[T any](fn func() T) *Pool[T] {
return &Pool[T]{
pool: sync.Pool{
New: func() any {
return fn()
},
},
}
}
// Get gets a T from the pool, or creates a new one if the pool is empty.
func (p *Pool[T]) Get() T {
return p.pool.Get().(T)
}
// Put returns x into the pool.
func (p *Pool[T]) Put(x T) {
p.pool.Put(x)
}

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

@ -0,0 +1,19 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPool(t *testing.T) {
p := NewPool[*uint8](func() *uint8 {
item := uint8(22)
return &item
})
val := p.Get()
assert.Equal(t, uint8(22), *val)
assert.Equal(t, uint8(22), *p.Get())
p.Put(val)
}

View File

@ -1,111 +0,0 @@
package logger
import "github.com/op/go-logging"
// PrefixAware is implemented if the logger allows you to change the prefix.
type PrefixAware interface {
SetPrefix(string)
Prefix() string
}
// PrefixedLogger is a base interface for the logger with prefix.
type PrefixedLogger interface {
Logger
PrefixAware
}
// PrefixDecorator is an implementation of the PrefixedLogger. It will allow you to decorate any Logger with
// the provided predefined prefix.
type PrefixDecorator struct {
backend Logger
prefix []interface{}
}
// DecorateWithPrefix using provided base logger and provided prefix.
// No internal state of the base logger will be touched.
func DecorateWithPrefix(backend Logger, prefix string) PrefixedLogger {
return &PrefixDecorator{backend: backend, prefix: []interface{}{prefix}}
}
// NewWithPrefix returns logger with prefix. It uses StandardLogger under the hood.
func NewWithPrefix(transportCode, prefix string, logLevel logging.Level, logFormat logging.Formatter) PrefixedLogger {
return DecorateWithPrefix(NewStandard(transportCode, logLevel, logFormat), prefix)
}
// SetPrefix will replace existing prefix with the provided value.
// Use this format for prefixes: "prefix here:" - omit space at the end (it will be inserted automatically).
func (p *PrefixDecorator) SetPrefix(prefix string) {
p.prefix = []interface{}{prefix}
}
func (p *PrefixDecorator) getFormat(fmt string) string {
return p.prefix[0].(string) + " " + fmt
}
func (p *PrefixDecorator) Prefix() string {
return p.prefix[0].(string)
}
func (p *PrefixDecorator) Fatal(args ...interface{}) {
p.backend.Fatal(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Fatalf(format string, args ...interface{}) {
p.backend.Fatalf(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Panic(args ...interface{}) {
p.backend.Panic(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Panicf(format string, args ...interface{}) {
p.backend.Panicf(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Critical(args ...interface{}) {
p.backend.Critical(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Criticalf(format string, args ...interface{}) {
p.backend.Criticalf(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Error(args ...interface{}) {
p.backend.Error(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Errorf(format string, args ...interface{}) {
p.backend.Errorf(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Warning(args ...interface{}) {
p.backend.Warning(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Warningf(format string, args ...interface{}) {
p.backend.Warningf(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Notice(args ...interface{}) {
p.backend.Notice(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Noticef(format string, args ...interface{}) {
p.backend.Noticef(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Info(args ...interface{}) {
p.backend.Info(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Infof(format string, args ...interface{}) {
p.backend.Infof(p.getFormat(format), args...)
}
func (p *PrefixDecorator) Debug(args ...interface{}) {
p.backend.Debug(append(p.prefix, args...)...)
}
func (p *PrefixDecorator) Debugf(format string, args ...interface{}) {
p.backend.Debugf(p.getFormat(format), args...)
}

View File

@ -1,167 +0,0 @@
package logger
import (
"bytes"
"testing"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
const testPrefix = "TestPrefix:"
type PrefixDecoratorTest struct {
suite.Suite
buf *bytes.Buffer
logger PrefixedLogger
}
func TestPrefixDecorator(t *testing.T) {
suite.Run(t, new(PrefixDecoratorTest))
}
func TestNewWithPrefix(t *testing.T) {
buf := &bytes.Buffer{}
logger := NewWithPrefix("code", "Prefix:", logging.DEBUG, DefaultLogFormatter())
logger.(*PrefixDecorator).backend.(*StandardLogger).
SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter()))
logger.Debugf("message %s", "text")
assert.Contains(t, buf.String(), "Prefix: message text")
}
func (t *PrefixDecoratorTest) SetupSuite() {
t.buf = &bytes.Buffer{}
t.logger = DecorateWithPrefix((&StandardLogger{}).
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())), testPrefix)
}
func (t *PrefixDecoratorTest) SetupTest() {
t.buf.Reset()
t.logger.SetPrefix(testPrefix)
}
func (t *PrefixDecoratorTest) Test_SetPrefix() {
t.logger.Info("message")
t.Assert().Equal(testPrefix, t.logger.Prefix())
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
t.logger.SetPrefix(testPrefix + testPrefix)
t.logger.Info("message")
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), testPrefix+testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Panic() {
t.Require().Panics(func() {
t.logger.Panic("message")
})
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Panicf() {
t.Require().Panics(func() {
t.logger.Panicf("%s", "message")
})
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Critical() {
t.Require().NotPanics(func() {
t.logger.Critical("message")
})
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Criticalf() {
t.Require().NotPanics(func() {
t.logger.Criticalf("%s", "message")
})
t.Assert().Contains(t.buf.String(), "CRIT")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Error() {
t.Require().NotPanics(func() {
t.logger.Error("message")
})
t.Assert().Contains(t.buf.String(), "ERRO")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Errorf() {
t.Require().NotPanics(func() {
t.logger.Errorf("%s", "message")
})
t.Assert().Contains(t.buf.String(), "ERRO")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Warning() {
t.Require().NotPanics(func() {
t.logger.Warning("message")
})
t.Assert().Contains(t.buf.String(), "WARN")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Warningf() {
t.Require().NotPanics(func() {
t.logger.Warningf("%s", "message")
})
t.Assert().Contains(t.buf.String(), "WARN")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Notice() {
t.Require().NotPanics(func() {
t.logger.Notice("message")
})
t.Assert().Contains(t.buf.String(), "NOTI")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Noticef() {
t.Require().NotPanics(func() {
t.logger.Noticef("%s", "message")
})
t.Assert().Contains(t.buf.String(), "NOTI")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Info() {
t.Require().NotPanics(func() {
t.logger.Info("message")
})
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Infof() {
t.Require().NotPanics(func() {
t.logger.Infof("%s", "message")
})
t.Assert().Contains(t.buf.String(), "INFO")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Debug() {
t.Require().NotPanics(func() {
t.logger.Debug("message")
})
t.Assert().Contains(t.buf.String(), "DEBU")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}
func (t *PrefixDecoratorTest) Test_Debugf() {
t.Require().NotPanics(func() {
t.logger.Debugf("%s", "message")
})
t.Assert().Contains(t.buf.String(), "DEBU")
t.Assert().Contains(t.buf.String(), testPrefix+" message")
}

View File

@ -0,0 +1,22 @@
package logger
import (
"io"
"go.uber.org/zap/zapcore"
)
type writerAdapter struct {
log Logger
level zapcore.Level
}
// WriterAdapter returns an io.Writer that can be used to write log messages. Message level is preconfigured.
func WriterAdapter(log Logger, level zapcore.Level) io.Writer {
return &writerAdapter{log: log, level: level}
}
func (w *writerAdapter) Write(p []byte) (n int, err error) {
w.log.Log(w.level, string(p))
return len(p), nil
}

View File

@ -0,0 +1,25 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestWriterAdapter(t *testing.T) {
log := newBufferLogger()
adapter := WriterAdapter(log, zap.InfoLevel)
msg := []byte("hello world")
total, err := adapter.Write(msg)
require.NoError(t, err)
require.Equal(t, total, len(msg))
items, err := newJSONBufferedLogger(log).ScanAll()
require.NoError(t, err)
require.Len(t, items, 1)
assert.Equal(t, "hello world", items[0].Message)
assert.Equal(t, "INFO", items[0].LevelName)
}

View File

@ -0,0 +1,34 @@
package logger
import (
"fmt"
metrics "github.com/retailcrm/zabbix-metrics-collector"
)
type zabbixCollectorAdapter struct {
log Logger
}
func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) {
baseMsg := "cannot send metrics to Zabbix"
switch format {
case "cannot stop collector: %s":
baseMsg = "cannot stop Zabbix collector"
fallthrough
case "cannot send metrics to Zabbix: %v":
var err interface{}
if len(args) > 0 {
err = args[0]
}
a.log.Error(baseMsg, Err(err))
default:
a.log.Error(fmt.Sprintf(format, args...))
}
}
// ZabbixCollectorAdapter works as a logger adapter for Zabbix metrics collector.
// It can extract error messages from Zabbix collector and convert them to structured format.
func ZabbixCollectorAdapter(log Logger) metrics.ErrorLogger {
return &zabbixCollectorAdapter{log: log}
}

View File

@ -0,0 +1,26 @@
package logger
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestZabbixCollectorAdapter(t *testing.T) {
log := newBufferLogger()
adapter := ZabbixCollectorAdapter(log)
adapter.Errorf("highly unexpected error: %s", "unexpected error")
adapter.Errorf("cannot stop collector: %s", "app error")
adapter.Errorf("cannot send metrics to Zabbix: %v", errors.New("send error"))
items, err := newJSONBufferedLogger(log).ScanAll()
require.NoError(t, err)
require.Len(t, items, 3)
assert.Equal(t, "highly unexpected error: unexpected error", items[0].Message)
assert.Equal(t, "cannot stop Zabbix collector", items[1].Message)
assert.Equal(t, "app error", items[1].Context[ErrorAttr])
assert.Equal(t, "cannot send metrics to Zabbix", items[2].Message)
assert.Equal(t, "send error", items[2].Context[ErrorAttr])
}

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

@ -0,0 +1,102 @@
package logger
import (
"fmt"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func NewZap(format string, debug bool) *zap.Logger {
switch format {
case "json":
return NewZapJSON(debug)
case "console":
return NewZapConsole(debug)
default:
panic(fmt.Sprintf("unknown logger format: %s", format))
}
}
func NewZapConsole(debug bool) *zap.Logger {
level := zapcore.InfoLevel
if debug {
level = zapcore.DebugLevel
}
log, err := zap.Config{
Level: zap.NewAtomicLevelAt(level),
Development: debug,
Encoding: "console",
EncoderConfig: EncoderConfigConsole(),
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}.Build()
if err != nil {
panic(err)
}
return log
}
func EncoderConfigConsole() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
TimeKey: "datetime",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "",
LineEnding: "\n",
EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("level_name=" + level.CapitalString())
},
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("datetime=" + t.Format(time.RFC3339))
},
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("caller=" + caller.TrimmedPath())
},
EncodeName: zapcore.FullNameEncoder,
ConsoleSeparator: " ",
}
}
func NewZapJSON(debug bool) *zap.Logger {
level := zapcore.InfoLevel
if debug {
level = zapcore.DebugLevel
}
log, err := zap.Config{
Level: zap.NewAtomicLevelAt(level),
Development: debug,
Encoding: "json-with-context",
EncoderConfig: EncoderConfigJSON(),
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}.Build()
if err != nil {
panic(err)
}
return log
}
func EncoderConfigJSON() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level_name",
TimeKey: "datetime",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "",
LineEnding: "\n",
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeName: zapcore.FullNameEncoder,
ConsoleSeparator: " ",
}
}

View File

@ -6,7 +6,6 @@ import (
"crypto/sha1"
"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 == "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ func printRequestData(t UnmatchedRequestsTestingT, r *http.Request) {
}
}
if r.Body == nil {
if r.Body == nil { // nolint:nestif
t.Log("No body is present.")
} else {
data, err := io.ReadAll(r.Body)

View File

@ -0,0 +1,52 @@
package testutil
import (
"bufio"
"encoding/json"
"io"
"github.com/guregu/null/v5"
)
type LogRecord struct {
LevelName string `json:"level_name"`
DateTime null.Time `json:"datetime"`
Caller string `json:"caller"`
Message string `json:"message"`
Handler string `json:"handler,omitempty"`
Connection string `json:"connection,omitempty"`
Account string `json:"account,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
}
type JSONRecordScanner struct {
r *bufio.Scanner
e LogRecord
}
func NewJSONRecordScanner(entryProvider io.Reader) *JSONRecordScanner {
return &JSONRecordScanner{r: bufio.NewScanner(entryProvider)}
}
func (s *JSONRecordScanner) Scan() error {
if s.r.Scan() {
return json.Unmarshal(s.r.Bytes(), &s.e)
}
return io.EOF
}
func (s *JSONRecordScanner) ScanAll() ([]LogRecord, error) {
var entries []LogRecord
for s.r.Scan() {
entry := LogRecord{}
if err := json.Unmarshal(s.r.Bytes(), &entry); err != nil {
return entries, err
}
entries = append(entries, entry)
}
return entries, nil
}
func (s *JSONRecordScanner) Entry() LogRecord {
return s.e
}

View File

@ -0,0 +1,108 @@
package testutil
import (
"bytes"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type JSONRecordScannerTest struct {
suite.Suite
}
func TestJSONRecordScanner(t *testing.T) {
suite.Run(t, new(JSONRecordScannerTest))
}
func (t *JSONRecordScannerTest) new(lines []string) *JSONRecordScanner {
return NewJSONRecordScanner(bytes.NewReader([]byte(strings.Join(lines, "\n"))))
}
func (t *JSONRecordScannerTest) newPredefined() *JSONRecordScanner {
return t.new([]string{strings.ReplaceAll(`{
"level_name": "ERROR",
"datetime": "2024-06-07T13:49:17+03:00",
"caller": "handlers/account_middleware.go:147",
"message": "Cannot add account",
"handler": "handlers.addAccount",
"connection": "https://fake-uri.retailcrm.pro",
"account": "@username",
"context": {
"body": "[]string{\"integration_read\", \"integration_write\"}",
"statusCode": 500
}
}`, "\n", "")})
}
func (t *JSONRecordScannerTest) assertPredefined(record LogRecord) {
ts, err := time.Parse(time.RFC3339, "2024-06-07T13:49:17+03:00")
t.Require().NoError(err)
t.Assert().True(record.DateTime.Valid)
t.Assert().Equal(ts, record.DateTime.Time)
t.Assert().Equal("ERROR", record.LevelName)
t.Assert().Equal("handlers/account_middleware.go:147", record.Caller)
t.Assert().Equal("Cannot add account", record.Message)
t.Assert().Equal("handlers.addAccount", record.Handler)
t.Assert().Equal("https://fake-uri.retailcrm.pro", record.Connection)
t.Assert().Equal("@username", record.Account)
t.Assert().Equal("[]string{\"integration_read\", \"integration_write\"}", record.Context["body"])
t.Assert().Equal(float64(500), record.Context["statusCode"])
}
func (t *JSONRecordScannerTest) TestScan_NotJSON() {
rs := t.new([]string{"this is", "not json"})
t.Assert().Error(rs.Scan())
}
func (t *JSONRecordScannerTest) TestScan_PartialJSON() {
rs := t.new([]string{"{}", "not json"})
t.Assert().NoError(rs.Scan())
t.Assert().Error(rs.Scan())
}
func (t *JSONRecordScannerTest) TestScan_JSON() {
rs := t.new([]string{"{}", "{}"})
t.Assert().NoError(rs.Scan())
t.Assert().NoError(rs.Scan())
t.Assert().ErrorIs(rs.Scan(), io.EOF)
}
func (t *JSONRecordScannerTest) TestScan_JSONRecord() {
rs := t.newPredefined()
t.Assert().NoError(rs.Scan())
t.Assert().ErrorIs(rs.Scan(), io.EOF)
t.assertPredefined(rs.Entry())
}
func (t *JSONRecordScannerTest) TestScanAll_NotJSON() {
rs := t.new([]string{"this is", "not json"})
records, err := rs.ScanAll()
t.Assert().Error(err)
t.Assert().Empty(records)
}
func (t *JSONRecordScannerTest) TestScanAll_PartialJSON() {
rs := t.new([]string{"{}", "not json"})
records, err := rs.ScanAll()
t.Assert().Error(err)
t.Assert().Len(records, 1)
}
func (t *JSONRecordScannerTest) TestScanAll_JSON() {
rs := t.new([]string{"{}", "{}"})
records, err := rs.ScanAll()
t.Assert().NoError(err)
t.Assert().Len(records, 2)
}
func (t *JSONRecordScannerTest) TestScanAll_JSONRecord() {
rs := t.newPredefined()
records, err := rs.ScanAll()
t.Assert().NoError(err)
t.Assert().Len(records, 1)
t.assertPredefined(records[0])
}

View File

@ -0,0 +1,155 @@
package testutil
import (
"bytes"
"io"
"sync"
)
type LockableBuffer struct {
buf bytes.Buffer
rw sync.RWMutex
}
func (b *LockableBuffer) Bytes() []byte {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Bytes()
}
func (b *LockableBuffer) AvailableBuffer() []byte {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.AvailableBuffer()
}
func (b *LockableBuffer) String() string {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.String()
}
func (b *LockableBuffer) Len() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Len()
}
func (b *LockableBuffer) Cap() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Cap()
}
func (b *LockableBuffer) Available() int {
defer b.rw.RUnlock()
b.rw.RLock()
return b.buf.Available()
}
func (b *LockableBuffer) Truncate(n int) {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Truncate(n)
}
func (b *LockableBuffer) Reset() {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Reset()
}
func (b *LockableBuffer) Grow(n int) {
defer b.rw.Unlock()
b.rw.Lock()
b.buf.Grow(n)
}
func (b *LockableBuffer) Write(p []byte) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Write(p)
}
func (b *LockableBuffer) WriteString(s string) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteString(s)
}
func (b *LockableBuffer) ReadFrom(r io.Reader) (n int64, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadFrom(r)
}
func (b *LockableBuffer) WriteTo(w io.Writer) (n int64, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteTo(w)
}
func (b *LockableBuffer) WriteByte(c byte) error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteByte(c)
}
func (b *LockableBuffer) WriteRune(r rune) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.WriteRune(r)
}
func (b *LockableBuffer) Read(p []byte) (n int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Read(p)
}
func (b *LockableBuffer) Next(n int) []byte {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.Next(n)
}
func (b *LockableBuffer) ReadByte() (byte, error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadByte()
}
func (b *LockableBuffer) ReadRune() (r rune, size int, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadRune()
}
func (b *LockableBuffer) UnreadRune() error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.UnreadRune()
}
func (b *LockableBuffer) UnreadByte() error {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.UnreadByte()
}
func (b *LockableBuffer) ReadBytes(delim byte) (line []byte, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadBytes(delim)
}
func (b *LockableBuffer) ReadString(delim byte) (line string, err error) {
defer b.rw.Unlock()
b.rw.Lock()
return b.buf.ReadString(delim)
}
// Sync is a no-op.
func (b *LockableBuffer) Sync() error {
return nil
}

View File

@ -3,7 +3,6 @@ package testutil
import (
"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
}

View File

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

View File

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

View File

@ -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
View File

@ -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
View File

@ -157,6 +157,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-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=