mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2024-11-24 06:06:03 +03:00
New logging format
This commit is contained in:
commit
36d32de72f
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: ['1.18', '1.19', '1.20', '1.21', '1.22', 'stable']
|
go-version: ['1.22', 'stable']
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Go ${{ matrix.go-version }}
|
- name: Set up Go ${{ matrix.go-version }}
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
@ -46,7 +46,9 @@ jobs:
|
|||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: go test ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic
|
run: |
|
||||||
|
go install gotest.tools/gotestsum@latest
|
||||||
|
gotestsum --format testdox ./... -v -cpu 2 -timeout 30s -race -cover -coverprofile=coverage.txt -covermode=atomic
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
run: |
|
run: |
|
||||||
go install github.com/axw/gocov/gocov@latest
|
go install github.com/axw/gocov/gocov@latest
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
[![Coverage](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/mg-transport-core)
|
[![Coverage](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/mg-transport-core)
|
||||||
[![GitHub release](https://img.shields.io/github/release/retailcrm/mg-transport-core.svg?logo=github&logoColor=white)](https://github.com/retailcrm/mg-transport-core/releases)
|
[![GitHub release](https://img.shields.io/github/release/retailcrm/mg-transport-core.svg?logo=github&logoColor=white)](https://github.com/retailcrm/mg-transport-core/releases)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core)
|
||||||
[![GoLang version](https://img.shields.io/badge/go->=1.12-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/)
|
[![GoLang version](https://img.shields.io/badge/go->=1.22-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/)
|
||||||
[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core)
|
[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core)
|
||||||
|
|
||||||
This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
|
This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
|
||||||
|
@ -32,8 +32,7 @@ func main() {
|
|||||||
if _, err := parser.Parse(); err != nil {
|
if _, err := parser.Parse(); err != nil {
|
||||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint
|
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { // nolint:errorlint
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ type Configuration interface {
|
|||||||
GetVersion() string
|
GetVersion() string
|
||||||
GetSentryDSN() string
|
GetSentryDSN() string
|
||||||
GetLogLevel() logging.Level
|
GetLogLevel() logging.Level
|
||||||
|
GetLogFormat() string
|
||||||
GetHTTPConfig() HTTPServerConfig
|
GetHTTPConfig() HTTPServerConfig
|
||||||
GetZabbixConfig() ZabbixConfig
|
GetZabbixConfig() ZabbixConfig
|
||||||
GetDBConfig() DatabaseConfig
|
GetDBConfig() DatabaseConfig
|
||||||
@ -44,6 +45,7 @@ type Config struct {
|
|||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
UpdateInterval int `yaml:"update_interval"`
|
UpdateInterval int `yaml:"update_interval"`
|
||||||
LogLevel logging.Level `yaml:"log_level"`
|
LogLevel logging.Level `yaml:"log_level"`
|
||||||
|
LogFormat string `yaml:"log_format"`
|
||||||
Debug bool `yaml:"debug"`
|
Debug bool `yaml:"debug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +132,7 @@ func (c *Config) GetConfigData(path string) []byte {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
source, err := ioutil.ReadFile(path)
|
source, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -153,6 +155,10 @@ func (c Config) GetLogLevel() logging.Level {
|
|||||||
return c.LogLevel
|
return c.LogLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Config) GetLogFormat() string {
|
||||||
|
return c.LogFormat
|
||||||
|
}
|
||||||
|
|
||||||
// GetTransportInfo transport basic data.
|
// GetTransportInfo transport basic data.
|
||||||
func (c Config) GetTransportInfo() InfoInterface {
|
func (c Config) GetTransportInfo() InfoInterface {
|
||||||
return c.TransportInfo
|
return c.TransportInfo
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@ -38,6 +37,7 @@ transport_info:
|
|||||||
|
|
||||||
sentry_dsn: dsn string
|
sentry_dsn: dsn string
|
||||||
log_level: 5
|
log_level: 5
|
||||||
|
log_format: console
|
||||||
debug: true
|
debug: true
|
||||||
update_interval: 24
|
update_interval: 24
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ config_aws:
|
|||||||
bucket: bucket
|
bucket: bucket
|
||||||
folder_name: folder
|
folder_name: folder
|
||||||
content_type: image/jpeg`)
|
content_type: image/jpeg`)
|
||||||
err := ioutil.WriteFile(testConfigFile, c.data, os.ModePerm)
|
err := os.WriteFile(testConfigFile, c.data, os.ModePerm)
|
||||||
require.Nil(c.T(), err)
|
require.Nil(c.T(), err)
|
||||||
|
|
||||||
c.config = NewConfig(testConfigFile)
|
c.config = NewConfig(testConfigFile)
|
||||||
@ -90,6 +90,10 @@ func (c *ConfigTest) Test_GetLogLevel() {
|
|||||||
assert.Equal(c.T(), logging.Level(5), c.config.GetLogLevel())
|
assert.Equal(c.T(), logging.Level(5), c.config.GetLogLevel())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ConfigTest) Test_GetLogFormat() {
|
||||||
|
assert.Equal(c.T(), "console", c.config.GetLogFormat())
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ConfigTest) Test_IsDebug() {
|
func (c *ConfigTest) Test_IsDebug() {
|
||||||
assert.Equal(c.T(), true, c.config.IsDebug())
|
assert.Equal(c.T(), true, c.config.IsDebug())
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func (x *NewMigrationCommand) FileExists(filename string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute migration generator command.
|
// Execute migration generator command.
|
||||||
func (x *NewMigrationCommand) Execute(args []string) error {
|
func (x *NewMigrationCommand) Execute(_ []string) error {
|
||||||
tpl, err := template.New("migration").Parse(migrationTemplate)
|
tpl, err := template.New("migration").Parse(migrationTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fatal: cannot parse base migration template: %w", err)
|
return fmt.Errorf("fatal: cannot parse base migration template: %w", err)
|
||||||
|
@ -2,7 +2,6 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
@ -36,7 +35,7 @@ func (s *MigrationGeneratorSuite) Test_FileExists() {
|
|||||||
func (s *MigrationGeneratorSuite) Test_Execute() {
|
func (s *MigrationGeneratorSuite) Test_Execute() {
|
||||||
found := false
|
found := false
|
||||||
assert.NoError(s.T(), s.command.Execute([]string{}))
|
assert.NoError(s.T(), s.command.Execute([]string{}))
|
||||||
files, err := ioutil.ReadDir(s.command.Directory)
|
files, err := os.ReadDir(s.command.Directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -17,3 +17,10 @@ type Connection struct {
|
|||||||
ID int `gorm:"primary_key" json:"id"`
|
ID int `gorm:"primary_key" json:"id"`
|
||||||
Active bool `json:"active,omitempty"`
|
Active bool `json:"active,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Connection) Address() string {
|
||||||
|
if c.PublicURL != "" {
|
||||||
|
return c.PublicURL
|
||||||
|
}
|
||||||
|
return c.URL
|
||||||
|
}
|
||||||
|
@ -14,11 +14,14 @@ type User struct {
|
|||||||
|
|
||||||
// TableName will return table name for User
|
// TableName will return table name for User
|
||||||
// It will not work if User is not embedded, but mapped as another type
|
// It will not work if User is not embedded, but mapped as another type
|
||||||
// type MyUser User // will not work
|
//
|
||||||
|
// type MyUser User // will not work
|
||||||
|
//
|
||||||
// but
|
// but
|
||||||
// type MyUser struct { // will work
|
//
|
||||||
// User
|
// type MyUser struct { // will work
|
||||||
// }
|
// User
|
||||||
|
// }
|
||||||
func (User) TableName() string {
|
func (User) TableName() string {
|
||||||
return "mg_user"
|
return "mg_user"
|
||||||
}
|
}
|
||||||
|
12
core/doc.go
12
core/doc.go
@ -2,6 +2,7 @@
|
|||||||
Package core provides different functions like error-reporting, logging, localization, etc.
|
Package core provides different functions like error-reporting, logging, localization, etc.
|
||||||
to make it easier to create transports.
|
to make it easier to create transports.
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -36,14 +37,19 @@ Usage:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource embedding
|
# Resource embedding
|
||||||
|
|
||||||
packr can be used to provide resource embedding, see:
|
packr can be used to provide resource embedding, see:
|
||||||
|
|
||||||
https://github.com/gobuffalo/packr/tree/master/v2
|
https://github.com/gobuffalo/packr/tree/master/v2
|
||||||
|
|
||||||
In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library.
|
In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library.
|
||||||
You can find instruction here:
|
You can find instruction here:
|
||||||
|
|
||||||
https://github.com/gobuffalo/packr/tree/master/v2#library-installation
|
https://github.com/gobuffalo/packr/tree/master/v2#library-installation
|
||||||
|
|
||||||
Example of usage:
|
Example of usage:
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -104,10 +110,12 @@ Example of usage:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Migration generator
|
# Migration generator
|
||||||
|
|
||||||
This library contains helper tool for transports. You can install it via go:
|
This library contains helper tool for transports. You can install it via go:
|
||||||
|
|
||||||
$ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
|
$ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
|
||||||
|
|
||||||
Currently, it only can generate new migrations for your transport.
|
Currently, it only can generate new migrations for your transport.
|
||||||
|
|
||||||
Copyright (c) 2019 RetailDriver LLC. Usage of this source code is governed by a MIT license.
|
Copyright (c) 2019 RetailDriver LLC. Usage of this source code is governed by a MIT license.
|
||||||
|
@ -2,7 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ func getDomainsByStore(store string) []Domain {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
respBody, readErr := ioutil.ReadAll(resp.Body)
|
respBody, readErr := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -7,14 +7,15 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/blacked/go-zabbix"
|
"github.com/blacked/go-zabbix"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/op/go-logging"
|
metrics "github.com/retailcrm/zabbix-metrics-collector"
|
||||||
"github.com/retailcrm/zabbix-metrics-collector"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
@ -26,12 +27,14 @@ import (
|
|||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultHTTPClientTimeout time.Duration = 30
|
||||||
|
|
||||||
var boolTrue = true
|
var boolTrue = true
|
||||||
|
|
||||||
// DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client
|
// DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client
|
||||||
// if HTTP client config is not present in the configuration.
|
// if HTTP client config is not present in the configuration.
|
||||||
var DefaultHTTPClientConfig = &config.HTTPClientConfig{
|
var DefaultHTTPClientConfig = &config.HTTPClientConfig{
|
||||||
Timeout: 30,
|
Timeout: DefaultHTTPClientTimeout,
|
||||||
SSLVerification: &boolTrue,
|
SSLVerification: &boolTrue,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,16 +65,15 @@ func (a AppInfo) Release() string {
|
|||||||
|
|
||||||
// Engine struct.
|
// Engine struct.
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
AppInfo AppInfo
|
AppInfo AppInfo
|
||||||
Sessions sessions.Store
|
Sessions sessions.Store
|
||||||
LogFormatter logging.Formatter
|
Config config.Configuration
|
||||||
Config config.Configuration
|
Zabbix metrics.Transport
|
||||||
Zabbix metrics.Transport
|
ginEngine *gin.Engine
|
||||||
ginEngine *gin.Engine
|
csrf *middleware.CSRF
|
||||||
csrf *middleware.CSRF
|
httpClient *http.Client
|
||||||
httpClient *http.Client
|
jobManager *JobManager
|
||||||
jobManager *JobManager
|
|
||||||
db.ORM
|
db.ORM
|
||||||
Localizer
|
Localizer
|
||||||
util.Utils
|
util.Utils
|
||||||
@ -112,11 +114,6 @@ func (e *Engine) initGin() {
|
|||||||
e.buildSentryConfig()
|
e.buildSentryConfig()
|
||||||
e.InitSentrySDK()
|
e.InitSentrySDK()
|
||||||
r.Use(e.SentryMiddlewares()...)
|
r.Use(e.SentryMiddlewares()...)
|
||||||
|
|
||||||
if e.Config.IsDebug() {
|
|
||||||
r.Use(gin.Logger())
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Use(e.LocalizationMiddleware())
|
r.Use(e.LocalizationMiddleware())
|
||||||
e.ginEngine = r
|
e.ginEngine = r
|
||||||
}
|
}
|
||||||
@ -133,9 +130,6 @@ func (e *Engine) Prepare() *Engine {
|
|||||||
if e.DefaultError == "" {
|
if e.DefaultError == "" {
|
||||||
e.DefaultError = "error"
|
e.DefaultError = "error"
|
||||||
}
|
}
|
||||||
if e.LogFormatter == nil {
|
|
||||||
e.LogFormatter = logger.DefaultLogFormatter()
|
|
||||||
}
|
|
||||||
if e.LocaleMatcher == nil {
|
if e.LocaleMatcher == nil {
|
||||||
e.LocaleMatcher = DefaultLocalizerMatcher()
|
e.LocaleMatcher = DefaultLocalizerMatcher()
|
||||||
}
|
}
|
||||||
@ -150,9 +144,14 @@ func (e *Engine) Prepare() *Engine {
|
|||||||
e.Localizer.Preload(e.PreloadLanguages)
|
e.Localizer.Preload(e.PreloadLanguages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logFormat := "json"
|
||||||
|
if format := e.Config.GetLogFormat(); format != "" {
|
||||||
|
logFormat = format
|
||||||
|
}
|
||||||
|
|
||||||
e.CreateDB(e.Config.GetDBConfig())
|
e.CreateDB(e.Config.GetDBConfig())
|
||||||
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
|
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
|
||||||
e.SetLogger(logger.NewStandard(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter))
|
e.SetLogger(logger.NewDefault(logFormat, e.Config.IsDebug()))
|
||||||
e.Sentry.Localizer = &e.Localizer
|
e.Sentry.Localizer = &e.Localizer
|
||||||
e.Utils.Logger = e.Logger()
|
e.Utils.Logger = e.Logger()
|
||||||
e.Sentry.Logger = e.Logger()
|
e.Sentry.Logger = e.Logger()
|
||||||
@ -175,7 +174,25 @@ func (e *Engine) UseZabbix(collectors []metrics.Collector) *Engine {
|
|||||||
}
|
}
|
||||||
cfg := e.Config.GetZabbixConfig()
|
cfg := e.Config.GetZabbixConfig()
|
||||||
sender := zabbix.NewSender(cfg.ServerHost, cfg.ServerPort)
|
sender := zabbix.NewSender(cfg.ServerHost, cfg.ServerPort)
|
||||||
e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, e.Logger())
|
e.Zabbix = metrics.NewZabbix(collectors, sender, cfg.Host, cfg.Interval, logger.ZabbixCollectorAdapter(e.Logger()))
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// HijackGinLogs will take control of GIN debug logs and will convert them into structured logs.
|
||||||
|
// It will also affect default logging middleware. Use logger.GinMiddleware to circumvent this.
|
||||||
|
func (e *Engine) HijackGinLogs() *Engine {
|
||||||
|
if e.Logger() == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
gin.DefaultWriter = logger.WriterAdapter(e.Logger(), zap.DebugLevel)
|
||||||
|
gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), zap.ErrorLevel)
|
||||||
|
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
|
||||||
|
e.Logger().Debug("route",
|
||||||
|
zap.String(logger.HTTPMethodAttr, httpMethod),
|
||||||
|
zap.String("path", absolutePath),
|
||||||
|
zap.String(logger.HandlerAttr, handlerName),
|
||||||
|
zap.Int("handlerCount", nuHandlers))
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +264,9 @@ func (e *Engine) SetLogger(l logger.Logger) *Engine {
|
|||||||
|
|
||||||
e.mutex.Lock()
|
e.mutex.Lock()
|
||||||
defer e.mutex.Unlock()
|
defer e.mutex.Unlock()
|
||||||
|
if !e.prepared && e.logger != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
e.logger = l
|
e.logger = l
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
@ -262,10 +282,9 @@ func (e *Engine) BuildHTTPClient(certs *x509.CertPool, replaceDefault ...bool) *
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
|
||||||
e.httpClient = client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.httpClient = client
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -123,7 +123,6 @@ func (e *EngineTest) Test_Prepare() {
|
|||||||
assert.True(e.T(), e.engine.prepared)
|
assert.True(e.T(), e.engine.prepared)
|
||||||
assert.NotNil(e.T(), e.engine.Config)
|
assert.NotNil(e.T(), e.engine.Config)
|
||||||
assert.NotEmpty(e.T(), e.engine.DefaultError)
|
assert.NotEmpty(e.T(), e.engine.DefaultError)
|
||||||
assert.NotEmpty(e.T(), e.engine.LogFormatter)
|
|
||||||
assert.NotEmpty(e.T(), e.engine.LocaleMatcher)
|
assert.NotEmpty(e.T(), e.engine.LocaleMatcher)
|
||||||
assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag))
|
assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag))
|
||||||
assert.NotNil(e.T(), e.engine.DB)
|
assert.NotNil(e.T(), e.engine.DB)
|
||||||
@ -253,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() {
|
|||||||
defer func() {
|
defer func() {
|
||||||
e.engine.logger = origLogger
|
e.engine.logger = origLogger
|
||||||
}()
|
}()
|
||||||
e.engine.logger = &logger.StandardLogger{}
|
e.engine.logger = logger.NewNil()
|
||||||
e.engine.SetLogger(nil)
|
e.engine.SetLogger(nil)
|
||||||
assert.NotNil(e.T(), e.engine.logger)
|
assert.NotNil(e.T(), e.engine.logger)
|
||||||
}
|
}
|
||||||
@ -366,7 +365,7 @@ func (e *EngineTest) Test_GetCSRFToken() {
|
|||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
RawQuery: "",
|
RawQuery: "",
|
||||||
},
|
},
|
||||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||||
Header: http.Header{"X-CSRF-Token": []string{"token"}},
|
Header: http.Header{"X-CSRF-Token": []string{"token"}},
|
||||||
}}
|
}}
|
||||||
c.Set("csrf_token", "token")
|
c.Set("csrf_token", "token")
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
const DefaultResetPeriod = time.Minute * 15
|
const DefaultResetPeriod = time.Minute * 15
|
||||||
|
|
||||||
// AtomicCounter is a default Counter implementation.
|
// AtomicCounter is a default Counter implementation.
|
||||||
// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and
|
// It uses atomics under the hood (hence the name) and can be configured with custom reset timeout and.
|
||||||
type AtomicCounter struct {
|
type AtomicCounter struct {
|
||||||
name atomic.String
|
name atomic.String
|
||||||
msg atomic.String
|
msg atomic.String
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package healthcheck
|
package healthcheck
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// compile-time checks to ensure that implementations are compatible with the interface
|
// compile-time checks to ensure that implementations are compatible with the interface.
|
||||||
_ = Storage(&SyncMapStorage{})
|
_ = Storage(&SyncMapStorage{})
|
||||||
_ = Counter(&AtomicCounter{})
|
_ = Counter(&AtomicCounter{})
|
||||||
_ = Processor(CounterProcessor{})
|
_ = Processor(CounterProcessor{})
|
||||||
|
@ -2,6 +2,7 @@ package healthcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -29,19 +30,19 @@ type CounterProcessor struct {
|
|||||||
func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen
|
func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen
|
||||||
if counter.IsFailed() {
|
if counter.IsFailed() {
|
||||||
if counter.IsFailureProcessed() {
|
if counter.IsFailureProcessed() {
|
||||||
c.debugLog("skipping counter id=%d because its failure is already processed", id)
|
c.debugLog("skipping counter because its failure is already processed", zap.Int(logger.CounterIDAttr, id))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL, apiKey, _, exists := c.ConnectionDataProvider(id)
|
apiURL, apiKey, _, exists := c.ConnectionDataProvider(id)
|
||||||
if !exists {
|
if !exists {
|
||||||
c.debugLog("cannot find connection data for counter id=%d", id)
|
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
err := c.Notifier(apiURL, apiKey, counter.Message())
|
err := c.Notifier(apiURL, apiKey, counter.Message())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)",
|
c.debugLog("cannot send notification for counter",
|
||||||
id, err, counter.Message())
|
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
|
||||||
}
|
}
|
||||||
counter.FailureProcessed()
|
counter.FailureProcessed()
|
||||||
return true
|
return true
|
||||||
@ -53,7 +54,8 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
|
|||||||
// Ignore this counter for now because total count of requests is less than minimal count.
|
// Ignore this counter for now because total count of requests is less than minimal count.
|
||||||
// The results may not be representative.
|
// The results may not be representative.
|
||||||
if (succeeded + failed) < c.MinRequests {
|
if (succeeded + failed) < c.MinRequests {
|
||||||
c.debugLog("skipping counter id=%d because it has fewer than %d requests", id, c.MinRequests)
|
c.debugLog("skipping counter because it has too few requests",
|
||||||
|
zap.Int(logger.CounterIDAttr, id), zap.Any("minRequests", c.MinRequests))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,13 +74,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
|
|||||||
|
|
||||||
apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id)
|
apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id)
|
||||||
if !exists {
|
if !exists {
|
||||||
c.debugLog("cannot find connection data for counter id=%d", id)
|
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang))
|
err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.debugLog("cannot send notification for counter id=%d: %s (message: %s)",
|
c.debugLog("cannot send notification for counter",
|
||||||
id, err, counter.Message())
|
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
|
||||||
}
|
}
|
||||||
counter.CountersProcessed()
|
counter.CountersProcessed()
|
||||||
return true
|
return true
|
||||||
@ -96,6 +98,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string {
|
|||||||
|
|
||||||
func (c CounterProcessor) debugLog(msg string, args ...interface{}) {
|
func (c CounterProcessor) debugLog(msg string, args ...interface{}) {
|
||||||
if c.Debug {
|
if c.Debug {
|
||||||
c.Logger.Debugf(msg, args...)
|
c.Logger.Debug(msg, logger.AnyZapFields(args)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ func (t *CounterProcessorTest) localizer() NotifyMessageLocalizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *CounterProcessorTest) new(
|
func (t *CounterProcessorTest) new(
|
||||||
nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, testutil.BufferedLogger) {
|
nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, *testutil.JSONRecordScanner) {
|
||||||
loc := t.localizer()
|
loc := t.localizer()
|
||||||
if len(noLocalizer) > 0 && noLocalizer[0] {
|
if len(noLocalizer) > 0 && noLocalizer[0] {
|
||||||
loc = nil
|
loc = nil
|
||||||
@ -61,7 +61,7 @@ func (t *CounterProcessorTest) new(
|
|||||||
FailureThreshold: DefaultFailureThreshold,
|
FailureThreshold: DefaultFailureThreshold,
|
||||||
MinRequests: DefaultMinRequests,
|
MinRequests: DefaultMinRequests,
|
||||||
Debug: true,
|
Debug: true,
|
||||||
}, log
|
}, testutil.NewJSONRecordScanner(log)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CounterProcessorTest) notifier(err ...error) *notifierMock {
|
func (t *CounterProcessorTest) notifier(err ...error) *notifierMock {
|
||||||
@ -95,7 +95,12 @@ func (t *CounterProcessorTest) Test_FailureProcessed() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(), "skipping counter id=1 because its failure is already processed")
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "skipping counter because its failure is already processed")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
|
func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
|
||||||
@ -107,7 +112,12 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "cannot find connection data for counter")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
|
func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
|
||||||
@ -121,7 +131,14 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: http status code: 500 (message: error message)")
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "cannot send notification for counter")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
|
t.Assert().Equal("http status code: 500", logs[0].Context["error"])
|
||||||
|
t.Assert().Equal("error message", logs[0].Context["failureMessage"])
|
||||||
t.Assert().Equal(t.apiURL, n.apiURL)
|
t.Assert().Equal(t.apiURL, n.apiURL)
|
||||||
t.Assert().Equal(t.apiKey, n.apiKey)
|
t.Assert().Equal(t.apiKey, n.apiKey)
|
||||||
t.Assert().Equal("error message", n.message)
|
t.Assert().Equal("error message", n.message)
|
||||||
@ -138,7 +155,10 @@ func (t *CounterProcessorTest) Test_CounterFailed_SentNotification() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Empty(log.String())
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 0)
|
||||||
t.Assert().Equal(t.apiURL, n.apiURL)
|
t.Assert().Equal(t.apiURL, n.apiURL)
|
||||||
t.Assert().Equal(t.apiKey, n.apiKey)
|
t.Assert().Equal(t.apiKey, n.apiKey)
|
||||||
t.Assert().Equal("error message", n.message)
|
t.Assert().Equal("error message", n.message)
|
||||||
@ -154,8 +174,13 @@ func (t *CounterProcessorTest) Test_TooFewRequests() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(),
|
|
||||||
fmt.Sprintf("skipping counter id=%d because it has fewer than %d requests", 1, DefaultMinRequests))
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "skipping counter because it has too few requests")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
|
t.Assert().Equal(float64(DefaultMinRequests), logs[0].Context["minRequests"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
|
func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
|
||||||
@ -170,7 +195,10 @@ func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Empty(log.String())
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 0)
|
||||||
t.Assert().Empty(n.message)
|
t.Assert().Empty(n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +213,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_AlreadyProcessed() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Empty(log.String())
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 0)
|
||||||
t.Assert().Empty(n.message)
|
t.Assert().Empty(n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +231,12 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "cannot find connection data for counter")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
t.Assert().Empty(n.message)
|
t.Assert().Empty(n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +254,13 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: unknown error (message: )")
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 1)
|
||||||
|
t.Assert().Contains(logs[0].Message, "cannot send notification for counter")
|
||||||
|
t.Assert().Equal(float64(1), logs[0].Context["counterId"])
|
||||||
|
t.Assert().Equal("unknown error", logs[0].Context["error"])
|
||||||
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +277,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent() {
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Empty(log.String())
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 0)
|
||||||
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +297,10 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent_NoLocalizer
|
|||||||
|
|
||||||
p.Process(1, c)
|
p.Process(1, c)
|
||||||
c.AssertExpectations(t.T())
|
c.AssertExpectations(t.T())
|
||||||
t.Assert().Empty(log.String())
|
|
||||||
|
logs, err := log.ScanAll()
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Require().Len(logs, 0)
|
||||||
t.Assert().Equal(`default error`, n.message)
|
t.Assert().Equal(`default error`, n.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JobFunc is empty func which should be executed in a parallel goroutine.
|
// JobFunc is empty func which should be executed in a parallel goroutine.
|
||||||
@ -46,7 +47,7 @@ type Job struct {
|
|||||||
// SetLogger(logger).
|
// SetLogger(logger).
|
||||||
// SetLogging(false)
|
// SetLogging(false)
|
||||||
// _ = manager.RegisterJob("updateTokens", &Job{
|
// _ = manager.RegisterJob("updateTokens", &Job{
|
||||||
// Command: func(log logger.Logger) error {
|
// Command: func(log logger.LoggerOld) error {
|
||||||
// // logic goes here...
|
// // logic goes here...
|
||||||
// logger.Info("All tokens were updated successfully")
|
// logger.Info("All tokens were updated successfully")
|
||||||
// return nil
|
// return nil
|
||||||
@ -73,6 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log = log.With(logger.Handler(name))
|
||||||
err := j.Command(log)
|
err := j.Command(log)
|
||||||
if err != nil && j.ErrorHandler != nil {
|
if err != nil && j.ErrorHandler != nil {
|
||||||
j.ErrorHandler(name, err, log)
|
j.ErrorHandler(name, err, log)
|
||||||
@ -153,7 +155,7 @@ func NewJobManager() *JobManager {
|
|||||||
func DefaultJobErrorHandler() JobErrorHandler {
|
func DefaultJobErrorHandler() JobErrorHandler {
|
||||||
return func(name string, err error, log logger.Logger) {
|
return func(name string, err error, log logger.Logger) {
|
||||||
if err != nil && name != "" {
|
if err != nil && name != "" {
|
||||||
log.Errorf("Job `%s` errored with an error: `%s`", name, err.Error())
|
log.Error("job failed with an error", zap.String("job", name), logger.Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,7 +164,7 @@ func DefaultJobErrorHandler() JobErrorHandler {
|
|||||||
func DefaultJobPanicHandler() JobPanicHandler {
|
func DefaultJobPanicHandler() JobPanicHandler {
|
||||||
return func(name string, recoverValue interface{}, log logger.Logger) {
|
return func(name string, recoverValue interface{}, log logger.Logger) {
|
||||||
if recoverValue != nil && name != "" {
|
if recoverValue != nil && name != "" {
|
||||||
log.Errorf("Job `%s` panicked with value: `%#v`", name, recoverValue)
|
log.Error("job panicked with the value", zap.String("job", name), zap.Any("value", recoverValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
)
|
)
|
||||||
@ -25,7 +26,7 @@ type JobTest struct {
|
|||||||
executeErr chan error
|
executeErr chan error
|
||||||
panicValue chan interface{}
|
panicValue chan interface{}
|
||||||
lastLog string
|
lastLog string
|
||||||
lastMsgLevel logging.Level
|
lastMsgLevel zapcore.Level
|
||||||
syncBool bool
|
syncBool bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,68 +37,94 @@ type JobManagerTest struct {
|
|||||||
syncRunnerFlag bool
|
syncRunnerFlag bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type callbackLoggerFunc func(level logging.Level, format string, args ...interface{})
|
type callbackLoggerFunc func(level zapcore.Level, msg string, args ...zap.Field)
|
||||||
|
|
||||||
type callbackLogger struct {
|
type callbackLogger struct {
|
||||||
fn callbackLoggerFunc
|
fields []zap.Field
|
||||||
|
fn callbackLoggerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Fatal(args ...interface{}) {
|
func (n *callbackLogger) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
|
||||||
n.fn(logging.CRITICAL, "", args...)
|
return &zapcore.CheckedEntry{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Fatalf(format string, args ...interface{}) {
|
func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) {
|
||||||
n.fn(logging.CRITICAL, format, args...)
|
n.fn(zap.PanicLevel, msg, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Panic(args ...interface{}) {
|
func (n *callbackLogger) Panic(msg string, fields ...zap.Field) {
|
||||||
n.fn(logging.CRITICAL, "", args...)
|
n.fn(zap.PanicLevel, msg, fields...)
|
||||||
}
|
|
||||||
func (n *callbackLogger) Panicf(format string, args ...interface{}) {
|
|
||||||
n.fn(logging.CRITICAL, format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Critical(args ...interface{}) {
|
func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) {
|
||||||
n.fn(logging.CRITICAL, "", args...)
|
n.fn(zap.FatalLevel, msg, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Criticalf(format string, args ...interface{}) {
|
func (n *callbackLogger) Sync() error {
|
||||||
n.fn(logging.CRITICAL, format, args...)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Error(args ...interface{}) {
|
func (n *callbackLogger) clone() *callbackLogger {
|
||||||
n.fn(logging.ERROR, "", args...)
|
return &callbackLogger{fn: n.fn, fields: n.fields}
|
||||||
}
|
|
||||||
func (n *callbackLogger) Errorf(format string, args ...interface{}) {
|
|
||||||
n.fn(logging.ERROR, format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Warning(args ...interface{}) {
|
func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger {
|
||||||
n.fn(logging.WARNING, "", args...)
|
cl := &callbackLogger{fn: n.fn, fields: n.fields}
|
||||||
}
|
existing := cl.fields
|
||||||
func (n *callbackLogger) Warningf(format string, args ...interface{}) {
|
if len(existing) == 0 {
|
||||||
n.fn(logging.WARNING, format, args...)
|
cl.fields = fields
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
cl.fields = append(existing, fields...) // nolint:gocritic
|
||||||
|
return cl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Notice(args ...interface{}) {
|
func (n *callbackLogger) Level() zapcore.Level {
|
||||||
n.fn(logging.NOTICE, "", args...)
|
return zapcore.DebugLevel
|
||||||
}
|
|
||||||
func (n *callbackLogger) Noticef(format string, args ...interface{}) {
|
|
||||||
n.fn(logging.NOTICE, format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Info(args ...interface{}) {
|
func (n *callbackLogger) With(args ...zap.Field) logger.Logger {
|
||||||
n.fn(logging.INFO, "", args...)
|
return n.cloneWithFields(args)
|
||||||
}
|
|
||||||
func (n *callbackLogger) Infof(format string, args ...interface{}) {
|
|
||||||
n.fn(logging.INFO, format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *callbackLogger) Debug(args ...interface{}) {
|
func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger {
|
||||||
n.fn(logging.DEBUG, "", args...)
|
return n.cloneWithFields(args)
|
||||||
}
|
}
|
||||||
func (n *callbackLogger) Debugf(format string, args ...interface{}) {
|
|
||||||
n.fn(logging.DEBUG, format, args...)
|
func (n *callbackLogger) WithGroup(_ string) logger.Logger {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) ForHandler(_ any) logger.Logger {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) ForConnection(_ any) logger.Logger {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) ForAccount(_ any) logger.Logger {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) Log(level zapcore.Level, msg string, args ...zap.Field) {
|
||||||
|
n.fn(level, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) Debug(msg string, args ...zap.Field) {
|
||||||
|
n.Log(zap.DebugLevel, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) Info(msg string, args ...zap.Field) {
|
||||||
|
n.Log(zap.InfoLevel, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) Warn(msg string, args ...zap.Field) {
|
||||||
|
n.Log(zap.WarnLevel, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *callbackLogger) Error(msg string, args ...zap.Field) {
|
||||||
|
n.Log(zap.ErrorLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJob(t *testing.T) {
|
func TestJob(t *testing.T) {
|
||||||
@ -115,9 +142,9 @@ func TestDefaultJobErrorHandler(t *testing.T) {
|
|||||||
|
|
||||||
fn := DefaultJobErrorHandler()
|
fn := DefaultJobErrorHandler()
|
||||||
require.NotNil(t, fn)
|
require.NotNil(t, fn)
|
||||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) {
|
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
|
||||||
require.Len(t, i, 2)
|
require.Len(t, i, 2)
|
||||||
assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
|
assert.Equal(t, "error=test", fmt.Sprintf("%s=%v", i[1].Key, i[1].Interface))
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,9 +155,9 @@ func TestDefaultJobPanicHandler(t *testing.T) {
|
|||||||
|
|
||||||
fn := DefaultJobPanicHandler()
|
fn := DefaultJobPanicHandler()
|
||||||
require.NotNil(t, fn)
|
require.NotNil(t, fn)
|
||||||
fn("job", errors.New("test"), &callbackLogger{fn: func(level logging.Level, s string, i ...interface{}) {
|
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
|
||||||
require.Len(t, i, 2)
|
require.Len(t, i, 2)
|
||||||
assert.Equal(t, fmt.Sprintf("%s", i[1]), "test")
|
assert.Equal(t, "value=test", fmt.Sprintf("%s=%s", i[1].Key, i[1].Interface))
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +174,7 @@ func (t *JobTest) testPanicHandler() JobPanicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *JobTest) testLogger() logger.Logger {
|
func (t *JobTest) testLogger() logger.Logger {
|
||||||
return &callbackLogger{fn: func(level logging.Level, format string, args ...interface{}) {
|
return &callbackLogger{fn: func(level zapcore.Level, format string, args ...zap.Field) {
|
||||||
if format == "" {
|
if format == "" {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.Grow(3 * len(args)) // nolint:gomnd
|
sb.Grow(3 * len(args)) // nolint:gomnd
|
||||||
@ -159,7 +186,12 @@ func (t *JobTest) testLogger() logger.Logger {
|
|||||||
format = strings.TrimRight(sb.String(), " ")
|
format = strings.TrimRight(sb.String(), " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.lastLog = fmt.Sprintf(format, args...)
|
anyFields := []any{}
|
||||||
|
for _, item := range args {
|
||||||
|
anyFields = append(anyFields, item.Key+"="+fmt.Sprint(item.Interface))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.lastLog = fmt.Sprintf(format, anyFields...)
|
||||||
t.lastMsgLevel = level
|
t.lastMsgLevel = level
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
@ -256,11 +288,11 @@ func (t *JobTest) oncePanicJob() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *JobTest) regularJob() {
|
func (t *JobTest) regularJob() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec
|
||||||
t.job = &Job{
|
t.job = &Job{
|
||||||
Command: func(log logger.Logger) error {
|
Command: func(log logger.Logger) error {
|
||||||
t.executedChan <- true
|
t.executedChan <- true
|
||||||
t.randomNumber <- rand.Int() // nolint:gosec
|
t.randomNumber <- r.Int() // nolint:gosec
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ErrorHandler: t.testErrorHandler(),
|
ErrorHandler: t.testErrorHandler(),
|
||||||
@ -271,7 +303,6 @@ func (t *JobTest) regularJob() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *JobTest) regularSyncJob() {
|
func (t *JobTest) regularSyncJob() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
t.job = &Job{
|
t.job = &Job{
|
||||||
Command: func(log logger.Logger) error {
|
Command: func(log logger.Logger) error {
|
||||||
t.syncBool = true
|
t.syncBool = true
|
||||||
@ -404,11 +435,11 @@ func (t *JobManagerTest) WaitForJob() bool {
|
|||||||
|
|
||||||
func (t *JobManagerTest) Test_SetLogger() {
|
func (t *JobManagerTest) Test_SetLogger() {
|
||||||
t.manager.logger = nil
|
t.manager.logger = nil
|
||||||
t.manager.SetLogger(logger.NewStandard("test", logging.ERROR, logger.DefaultLogFormatter()))
|
t.manager.SetLogger(logger.NewDefault("json", true))
|
||||||
assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger)
|
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
||||||
|
|
||||||
t.manager.SetLogger(nil)
|
t.manager.SetLogger(nil)
|
||||||
assert.IsType(t.T(), &logger.StandardLogger{}, t.manager.logger)
|
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *JobManagerTest) Test_SetLogging() {
|
func (t *JobManagerTest) Test_SetLogging() {
|
||||||
|
@ -3,7 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -90,7 +90,8 @@ type CloneableLocalizer interface {
|
|||||||
|
|
||||||
// NewLocalizer returns localizer instance with specified parameters.
|
// NewLocalizer returns localizer instance with specified parameters.
|
||||||
// Usage:
|
// Usage:
|
||||||
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
|
//
|
||||||
|
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
|
||||||
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface {
|
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) LocalizerInterface {
|
||||||
localizer := &Localizer{
|
localizer := &Localizer{
|
||||||
i18nStorage: &sync.Map{},
|
i18nStorage: &sync.Map{},
|
||||||
@ -106,7 +107,9 @@ func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPat
|
|||||||
|
|
||||||
// NewLocalizerFS returns localizer instance with specified parameters.
|
// NewLocalizerFS returns localizer instance with specified parameters.
|
||||||
// Usage:
|
// Usage:
|
||||||
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS)
|
//
|
||||||
|
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS)
|
||||||
|
//
|
||||||
// TODO This code should be covered with tests.
|
// TODO This code should be covered with tests.
|
||||||
func NewLocalizerFS(
|
func NewLocalizerFS(
|
||||||
locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
|
locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
|
||||||
@ -161,9 +164,10 @@ func (l *Localizer) Clone() CloneableLocalizer {
|
|||||||
// Because of that all Localizer instances from this middleware will share *same* mutex. This mutex is used to wrap
|
// Because of that all Localizer instances from this middleware will share *same* mutex. This mutex is used to wrap
|
||||||
// i18n.Bundle methods (those aren't goroutine-safe to use).
|
// i18n.Bundle methods (those aren't goroutine-safe to use).
|
||||||
// Usage:
|
// Usage:
|
||||||
// engine := gin.New()
|
//
|
||||||
// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations")
|
// engine := gin.New()
|
||||||
// engine.Use(localizer.LocalizationMiddleware())
|
// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations")
|
||||||
|
// engine.Use(localizer.LocalizationMiddleware())
|
||||||
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
|
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
clone := l.Clone().(LocaleControls)
|
clone := l.Clone().(LocaleControls)
|
||||||
@ -174,15 +178,21 @@ func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
// LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans
|
// LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans
|
||||||
// Usage in code:
|
// Usage in code:
|
||||||
// engine := gin.New()
|
//
|
||||||
// engine.FuncMap = localizer.LocalizationFuncMap()
|
// engine := gin.New()
|
||||||
|
// engine.FuncMap = localizer.LocalizationFuncMap()
|
||||||
|
//
|
||||||
// or (with multitemplate)
|
// or (with multitemplate)
|
||||||
// renderer := multitemplate.NewRenderer()
|
//
|
||||||
// funcMap := localizer.LocalizationFuncMap()
|
// renderer := multitemplate.NewRenderer()
|
||||||
// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html")
|
// funcMap := localizer.LocalizationFuncMap()
|
||||||
|
// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html")
|
||||||
|
//
|
||||||
// funcMap must be passed for every .AddFromFilesFuncs call
|
// funcMap must be passed for every .AddFromFilesFuncs call
|
||||||
// Usage in templates:
|
// Usage in templates:
|
||||||
// <p class="info">{{"need_login_msg" | trans}}
|
//
|
||||||
|
// <p class="info">{{"need_login_msg" | trans}}
|
||||||
|
//
|
||||||
// You can borrow FuncMap from this method and add your functions to it.
|
// You can borrow FuncMap from this method and add your functions to it.
|
||||||
func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
||||||
return template.FuncMap{
|
return template.FuncMap{
|
||||||
@ -196,7 +206,7 @@ func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
|||||||
parts = append(parts, "")
|
parts = append(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
partsMap := make(map[string]interface{}, len(parts)/2)
|
partsMap := make(map[string]interface{}, len(parts)/2) // nolint:gomnd
|
||||||
|
|
||||||
for i := 0; i < len(parts)-1; i += 2 {
|
for i := 0; i < len(parts)-1; i += 2 {
|
||||||
partsMap[parts[i]] = parts[i+1]
|
partsMap[parts[i]] = parts[i+1]
|
||||||
@ -239,7 +249,7 @@ func (l *Localizer) loadTranslationsToBundle(i18nBundle *i18n.Bundle) {
|
|||||||
|
|
||||||
// LoadTranslations will load all translation files from translations directory.
|
// LoadTranslations will load all translation files from translations directory.
|
||||||
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
|
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
|
||||||
files, err := ioutil.ReadDir(l.TranslationsPath)
|
files, err := os.ReadDir(l.TranslationsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -40,7 +39,7 @@ func createTestLangFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) {
|
||||||
err = ioutil.WriteFile(fileName, data, os.ModePerm)
|
err = os.WriteFile(fileName, data, os.ModePerm)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,7 +102,6 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
|
|||||||
|
|
||||||
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
l.localizer.Preload(DefaultLanguages)
|
l.localizer.Preload(DefaultLanguages)
|
||||||
langMsgMap := map[language.Tag]string{
|
langMsgMap := map[language.Tag]string{
|
||||||
language.English: "Test message",
|
language.English: "Test message",
|
||||||
@ -120,9 +118,11 @@ func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
|||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
i := i
|
||||||
go func(m map[language.Tag]string, wg *sync.WaitGroup) {
|
go func(m map[language.Tag]string, wg *sync.WaitGroup) {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(i))) // nolint:gosec
|
||||||
var tag language.Tag
|
var tag language.Tag
|
||||||
switch rand.Intn(3-1) + 1 { // nolint:gosec
|
switch r.Intn(3-1) + 1 { // nolint:gosec
|
||||||
case 1:
|
case 1:
|
||||||
tag = language.English
|
tag = language.English
|
||||||
case 2:
|
case 2:
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultAccountLoggerFormat contains default prefix format for the AccountLoggerDecorator.
|
|
||||||
// Its messages will look like this (assuming you will provide the connection URL and account name):
|
|
||||||
// messageHandler (https://any.simla.com => @tg_account): sent message with id=1
|
|
||||||
const DefaultAccountLoggerFormat = "%s (%s => %s):"
|
|
||||||
|
|
||||||
type ComponentAware interface {
|
|
||||||
SetComponent(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionAware interface {
|
|
||||||
SetConnectionIdentifier(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountAware interface {
|
|
||||||
SetAccountIdentifier(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrefixFormatAware interface {
|
|
||||||
SetPrefixFormat(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountLogger interface {
|
|
||||||
PrefixedLogger
|
|
||||||
ComponentAware
|
|
||||||
ConnectionAware
|
|
||||||
AccountAware
|
|
||||||
PrefixFormatAware
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountLoggerDecorator struct {
|
|
||||||
format string
|
|
||||||
component string
|
|
||||||
connIdentifier string
|
|
||||||
accIdentifier string
|
|
||||||
PrefixDecorator
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecorateForAccount(base Logger, component, connIdentifier, accIdentifier string) AccountLogger {
|
|
||||||
return (&AccountLoggerDecorator{
|
|
||||||
PrefixDecorator: PrefixDecorator{
|
|
||||||
backend: base,
|
|
||||||
},
|
|
||||||
component: component,
|
|
||||||
connIdentifier: connIdentifier,
|
|
||||||
accIdentifier: accIdentifier,
|
|
||||||
}).updatePrefix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewForAccount returns logger for account. It uses StandardLogger under the hood.
|
|
||||||
func NewForAccount(
|
|
||||||
transportCode, component, connIdentifier, accIdentifier string,
|
|
||||||
logLevel logging.Level,
|
|
||||||
logFormat logging.Formatter) AccountLogger {
|
|
||||||
return DecorateForAccount(NewStandard(transportCode, logLevel, logFormat),
|
|
||||||
component, connIdentifier, accIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) SetComponent(s string) {
|
|
||||||
a.component = s
|
|
||||||
a.updatePrefix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) SetConnectionIdentifier(s string) {
|
|
||||||
a.connIdentifier = s
|
|
||||||
a.updatePrefix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) SetAccountIdentifier(s string) {
|
|
||||||
a.accIdentifier = s
|
|
||||||
a.updatePrefix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) SetPrefixFormat(s string) {
|
|
||||||
a.format = s
|
|
||||||
a.updatePrefix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) updatePrefix() AccountLogger {
|
|
||||||
a.SetPrefix(fmt.Sprintf(a.prefixFormat(), a.component, a.connIdentifier, a.accIdentifier))
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountLoggerDecorator) prefixFormat() string {
|
|
||||||
if a.format == "" {
|
|
||||||
return DefaultAccountLoggerFormat
|
|
||||||
}
|
|
||||||
return a.format
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testComponent = "ComponentName"
|
|
||||||
testConnectionID = "https://test.retailcrm.pro"
|
|
||||||
testAccountID = "@account_name"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountLoggerDecoratorTest struct {
|
|
||||||
suite.Suite
|
|
||||||
buf *bytes.Buffer
|
|
||||||
logger AccountLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountLoggerDecorator(t *testing.T) {
|
|
||||||
suite.Run(t, new(AccountLoggerDecoratorTest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewForAccount(t *testing.T) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
logger := NewForAccount("code", "component", "conn", "acc", logging.DEBUG, DefaultLogFormatter())
|
|
||||||
logger.(*AccountLoggerDecorator).backend.(*StandardLogger).
|
|
||||||
SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter()))
|
|
||||||
logger.Debugf("message %s", "text")
|
|
||||||
|
|
||||||
assert.Contains(t, buf.String(), fmt.Sprintf(DefaultAccountLoggerFormat+" message", "component", "conn", "acc"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) SetupSuite() {
|
|
||||||
t.buf = &bytes.Buffer{}
|
|
||||||
t.logger = DecorateForAccount((&StandardLogger{}).
|
|
||||||
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())),
|
|
||||||
testComponent, testConnectionID, testAccountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) SetupTest() {
|
|
||||||
t.buf.Reset()
|
|
||||||
t.logger.SetComponent(testComponent)
|
|
||||||
t.logger.SetConnectionIdentifier(testConnectionID)
|
|
||||||
t.logger.SetAccountIdentifier(testAccountID)
|
|
||||||
t.logger.SetPrefixFormat(DefaultAccountLoggerFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) Test_LogWithNewFormat() {
|
|
||||||
t.logger.SetPrefixFormat("[%s (%s: %s)] =>")
|
|
||||||
t.logger.Infof("test message")
|
|
||||||
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(),
|
|
||||||
fmt.Sprintf("[%s (%s: %s)] =>", testComponent, testConnectionID, testAccountID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) Test_Log() {
|
|
||||||
t.logger.Infof("test message")
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(),
|
|
||||||
fmt.Sprintf(DefaultAccountLoggerFormat, testComponent, testConnectionID, testAccountID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) Test_SetComponent() {
|
|
||||||
t.logger.SetComponent("NewComponent")
|
|
||||||
t.logger.Infof("test message")
|
|
||||||
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(),
|
|
||||||
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", testConnectionID, testAccountID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) Test_SetConnectionIdentifier() {
|
|
||||||
t.logger.SetComponent("NewComponent")
|
|
||||||
t.logger.SetConnectionIdentifier("https://test.simla.com")
|
|
||||||
t.logger.Infof("test message")
|
|
||||||
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(),
|
|
||||||
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", testAccountID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AccountLoggerDecoratorTest) Test_SetAccountIdentifier() {
|
|
||||||
t.logger.SetComponent("NewComponent")
|
|
||||||
t.logger.SetConnectionIdentifier("https://test.simla.com")
|
|
||||||
t.logger.SetAccountIdentifier("@new_account_name")
|
|
||||||
t.logger.Infof("test message")
|
|
||||||
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(),
|
|
||||||
fmt.Sprintf(DefaultAccountLoggerFormat, "NewComponent", "https://test.simla.com", "@new_account_name"))
|
|
||||||
}
|
|
42
core/logger/api_client_adapter.go
Normal file
42
core/logger/api_client_adapter.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiDebugLogReq = "API Request: %s %s"
|
||||||
|
apiDebugLogResp = "API Response: %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiClientAdapter struct {
|
||||||
|
logger Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIClientAdapter returns BasicLogger that calls underlying logger.
|
||||||
|
func APIClientAdapter(logger Logger) retailcrm.BasicLogger {
|
||||||
|
return &apiClientAdapter{logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf data in the log using Debug method.
|
||||||
|
func (l *apiClientAdapter) Printf(format string, v ...interface{}) {
|
||||||
|
switch format {
|
||||||
|
case apiDebugLogReq:
|
||||||
|
var url, key string
|
||||||
|
if len(v) > 0 {
|
||||||
|
url = fmt.Sprint(v[0])
|
||||||
|
}
|
||||||
|
if len(v) > 1 {
|
||||||
|
key = fmt.Sprint(v[1])
|
||||||
|
}
|
||||||
|
l.logger.Debug("API Request", zap.String("url", url), zap.String("key", key))
|
||||||
|
case apiDebugLogResp:
|
||||||
|
l.logger.Debug("API Response", Body(v[0]))
|
||||||
|
default:
|
||||||
|
l.logger.Debug(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
41
core/logger/api_client_adapter_test.go
Normal file
41
core/logger/api_client_adapter_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIClientAdapter(t *testing.T) {
|
||||||
|
log := newJSONBufferedLogger(nil)
|
||||||
|
client := retailcrm.New("https://example.com", "test_key").WithLogger(APIClientAdapter(log.Logger()))
|
||||||
|
client.Debug = true
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://example.com").
|
||||||
|
Get("/api/credentials").
|
||||||
|
Reply(http.StatusOK).
|
||||||
|
JSON(retailcrm.CredentialResponse{Success: true})
|
||||||
|
|
||||||
|
_, _, err := client.APICredentials()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entries, err := log.ScanAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, entries, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, "DEBUG", entries[0].LevelName)
|
||||||
|
assert.True(t, entries[0].DateTime.Valid)
|
||||||
|
assert.Equal(t, "API Request", entries[0].Message)
|
||||||
|
assert.Equal(t, "test_key", entries[0].Context["key"])
|
||||||
|
assert.Equal(t, "https://example.com/api/credentials", entries[0].Context["url"])
|
||||||
|
|
||||||
|
assert.Equal(t, "DEBUG", entries[1].LevelName)
|
||||||
|
assert.True(t, entries[1].DateTime.Valid)
|
||||||
|
assert.Equal(t, "API Response", entries[1].Message)
|
||||||
|
assert.Equal(t, map[string]interface{}{"success": true}, entries[1].Context["body"])
|
||||||
|
}
|
94
core/logger/attrs.go
Normal file
94
core/logger/attrs.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
json "github.com/goccy/go-json"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerAttr represents the attribute name for the handler.
|
||||||
|
const HandlerAttr = "handler"
|
||||||
|
|
||||||
|
// ConnectionAttr represents the attribute name for the connection.
|
||||||
|
const ConnectionAttr = "connection"
|
||||||
|
|
||||||
|
// AccountAttr represents the attribute name for the account.
|
||||||
|
const AccountAttr = "account"
|
||||||
|
|
||||||
|
// CounterIDAttr represents the attribute name for the counter ID.
|
||||||
|
const CounterIDAttr = "counterId"
|
||||||
|
|
||||||
|
// ErrorAttr represents the attribute name for an error.
|
||||||
|
const ErrorAttr = "error"
|
||||||
|
|
||||||
|
// FailureMessageAttr represents the attribute name for a failure message.
|
||||||
|
const FailureMessageAttr = "failureMessage"
|
||||||
|
|
||||||
|
// BodyAttr represents the attribute name for the request body.
|
||||||
|
const BodyAttr = "body"
|
||||||
|
|
||||||
|
// HTTPMethodAttr represents the attribute name for the HTTP method.
|
||||||
|
const HTTPMethodAttr = "method"
|
||||||
|
|
||||||
|
// HTTPStatusAttr represents the attribute name for the HTTP status code.
|
||||||
|
const HTTPStatusAttr = "statusCode"
|
||||||
|
|
||||||
|
// HTTPStatusNameAttr represents the attribute name for the HTTP status name.
|
||||||
|
const HTTPStatusNameAttr = "statusName"
|
||||||
|
|
||||||
|
// Err returns a zap.Field with the given error value.
|
||||||
|
func Err(err any) zap.Field {
|
||||||
|
if err == nil {
|
||||||
|
return zap.String(ErrorAttr, "<nil>")
|
||||||
|
}
|
||||||
|
return zap.Any(ErrorAttr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns a zap.Field with the given handler name.
|
||||||
|
func Handler(name string) zap.Field {
|
||||||
|
return zap.String(HandlerAttr, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPStatusCode returns a zap.Field with the given HTTP status code.
|
||||||
|
func HTTPStatusCode(code int) zap.Field {
|
||||||
|
return zap.Int(HTTPStatusAttr, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPStatusName returns a zap.Field with the given HTTP status name.
|
||||||
|
func HTTPStatusName(code int) zap.Field {
|
||||||
|
return zap.String(HTTPStatusNameAttr, http.StatusText(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body returns a zap.Field with the given request body value.
|
||||||
|
func Body(val any) zap.Field {
|
||||||
|
switch item := val.(type) {
|
||||||
|
case string:
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(item), &m); err == nil {
|
||||||
|
return zap.Any(BodyAttr, m)
|
||||||
|
}
|
||||||
|
return zap.String(BodyAttr, item)
|
||||||
|
case []byte:
|
||||||
|
var m interface{}
|
||||||
|
if err := json.Unmarshal(item, &m); err == nil {
|
||||||
|
return zap.Any(BodyAttr, m)
|
||||||
|
}
|
||||||
|
return zap.String(BodyAttr, string(item))
|
||||||
|
case io.Reader:
|
||||||
|
data, err := io.ReadAll(item)
|
||||||
|
if err != nil {
|
||||||
|
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
|
||||||
|
}
|
||||||
|
var m interface{}
|
||||||
|
if err := json.Unmarshal(data, &m); err == nil {
|
||||||
|
return zap.Any(BodyAttr, m)
|
||||||
|
}
|
||||||
|
return zap.String(BodyAttr, string(data))
|
||||||
|
default:
|
||||||
|
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
|
||||||
|
}
|
||||||
|
}
|
156
core/logger/attrs_test.go
Normal file
156
core/logger/attrs_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErr(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
source interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: nil,
|
||||||
|
expected: "<nil>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: errors.New("untimely error"),
|
||||||
|
expected: "untimely error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
val := Err(c.source)
|
||||||
|
assert.Equal(t, c.expected, func() string {
|
||||||
|
if val.String != "" {
|
||||||
|
return val.String
|
||||||
|
}
|
||||||
|
if val.Interface != nil {
|
||||||
|
return fmt.Sprintf("%s", val.Interface)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}())
|
||||||
|
assert.Equal(t, ErrorAttr, val.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler(t *testing.T) {
|
||||||
|
val := Handler("handlerName")
|
||||||
|
assert.Equal(t, HandlerAttr, val.Key)
|
||||||
|
assert.Equal(t, "handlerName", val.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPStatusCode(t *testing.T) {
|
||||||
|
val := HTTPStatusCode(http.StatusOK)
|
||||||
|
assert.Equal(t, HTTPStatusAttr, val.Key)
|
||||||
|
assert.Equal(t, http.StatusOK, int(val.Integer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPStatusName(t *testing.T) {
|
||||||
|
val := HTTPStatusName(http.StatusOK)
|
||||||
|
assert.Equal(t, HTTPStatusNameAttr, val.Key)
|
||||||
|
assert.Equal(t, http.StatusText(http.StatusOK), val.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBody(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
input interface{}
|
||||||
|
result interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: nil,
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "ooga booga",
|
||||||
|
result: "ooga booga",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"success":true}`,
|
||||||
|
result: map[string]interface{}{"success": true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte{},
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: nil,
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte("ooga booga"),
|
||||||
|
result: "ooga booga",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`{"success":true}`),
|
||||||
|
result: map[string]interface{}{"success": true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: newReaderMock(func(p []byte) (n int, err error) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}),
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: newReaderMockData([]byte{}),
|
||||||
|
result: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: newReaderMockData([]byte("ooga booga")),
|
||||||
|
result: "ooga booga",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: newReaderMockData([]byte(`{"success":true}`)),
|
||||||
|
result: map[string]interface{}{"success": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
val := Body(c.input)
|
||||||
|
assert.Equal(t, BodyAttr, val.Key)
|
||||||
|
|
||||||
|
switch assertion := c.result.(type) {
|
||||||
|
case string:
|
||||||
|
assert.Equal(t, assertion, val.String)
|
||||||
|
case int:
|
||||||
|
assert.Equal(t, assertion, int(val.Integer))
|
||||||
|
default:
|
||||||
|
assert.Equal(t, c.result, val.Interface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type readerMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReaderMock(cb func(p []byte) (n int, err error)) io.Reader {
|
||||||
|
r := &readerMock{}
|
||||||
|
r.On("Read", mock.Anything).Return(cb)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReaderMockData(data []byte) io.Reader {
|
||||||
|
return newReaderMock(bytes.NewReader(data).Read)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *readerMock) Read(p []byte) (n int, err error) {
|
||||||
|
args := m.Called(p)
|
||||||
|
out := args.Get(0)
|
||||||
|
if cb, ok := out.(func(p []byte) (n int, err error)); ok {
|
||||||
|
return cb(p)
|
||||||
|
}
|
||||||
|
return args.Int(0), args.Error(1)
|
||||||
|
}
|
266
core/logger/buffer_logger_test.go
Normal file
266
core/logger/buffer_logger_test.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/guregu/null/v5"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logRecord struct {
|
||||||
|
LevelName string `json:"level_name"`
|
||||||
|
DateTime null.Time `json:"datetime"`
|
||||||
|
Caller string `json:"caller"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Handler string `json:"handler,omitempty"`
|
||||||
|
Connection string `json:"connection,omitempty"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
Context map[string]interface{} `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jSONRecordScanner struct {
|
||||||
|
scan *bufio.Scanner
|
||||||
|
buf *bufferLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONBufferedLogger(buf *bufferLogger) *jSONRecordScanner {
|
||||||
|
if buf == nil {
|
||||||
|
buf = newBufferLogger()
|
||||||
|
}
|
||||||
|
return &jSONRecordScanner{scan: bufio.NewScanner(buf), buf: buf}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *jSONRecordScanner) ScanAll() ([]logRecord, error) {
|
||||||
|
var entries []logRecord
|
||||||
|
for s.scan.Scan() {
|
||||||
|
entry := logRecord{}
|
||||||
|
if err := json.Unmarshal(s.scan.Bytes(), &entry); err != nil {
|
||||||
|
return entries, err
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *jSONRecordScanner) Logger() Logger {
|
||||||
|
return s.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufferLogger is an implementation of the BufferedLogger.
|
||||||
|
type bufferLogger struct {
|
||||||
|
Default
|
||||||
|
buf lockableBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedLogger returns new BufferedLogger instance.
|
||||||
|
func newBufferLogger() *bufferLogger {
|
||||||
|
bl := &bufferLogger{}
|
||||||
|
bl.Logger = zap.New(
|
||||||
|
zapcore.NewCore(
|
||||||
|
NewJSONWithContextEncoder(
|
||||||
|
EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
|
||||||
|
return bl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *bufferLogger) With(fields ...zapcore.Field) Logger {
|
||||||
|
return &bufferLogger{
|
||||||
|
Default: Default{
|
||||||
|
Logger: l.Logger.With(fields...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *bufferLogger) WithLazy(fields ...zapcore.Field) Logger {
|
||||||
|
return &bufferLogger{
|
||||||
|
Default: Default{
|
||||||
|
Logger: l.Logger.WithLazy(fields...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *bufferLogger) ForHandler(handler any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(HandlerAttr, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *bufferLogger) ForConnection(conn any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(ConnectionAttr, conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *bufferLogger) ForAccount(acc any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(AccountAttr, acc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read bytes from the logger buffer. io.Reader implementation.
|
||||||
|
func (l *bufferLogger) Read(p []byte) (n int, err error) {
|
||||||
|
return l.buf.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String contents of the logger buffer. fmt.Stringer implementation.
|
||||||
|
func (l *bufferLogger) String() string {
|
||||||
|
return l.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
|
||||||
|
func (l *bufferLogger) Bytes() []byte {
|
||||||
|
return l.buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
|
||||||
|
func (l *bufferLogger) Reset() {
|
||||||
|
l.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
type lockableBuffer struct {
|
||||||
|
buf bytes.Buffer
|
||||||
|
rw sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Bytes() []byte {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) AvailableBuffer() []byte {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.AvailableBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) String() string {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Len() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Cap() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Cap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Available() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Available()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Truncate(n int) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Truncate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Reset() {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Grow(n int) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Grow(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) WriteString(s string) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadFrom(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteTo(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) WriteByte(c byte) error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteByte(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) WriteRune(r rune) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Read(p []byte) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) Next(n int) []byte {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Next(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) ReadByte() (byte, error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) ReadRune() (r rune, size int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) UnreadRune() error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.UnreadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) UnreadByte() error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.UnreadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) ReadBytes(delim byte) (line []byte, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadBytes(delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *lockableBuffer) ReadString(delim byte) (line string, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadString(delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync is a no-op.
|
||||||
|
func (b *lockableBuffer) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
29
core/logger/bufferpool.go
Normal file
29
core/logger/bufferpool.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import "go.uber.org/zap/buffer"
|
||||||
|
|
||||||
|
var (
|
||||||
|
_pool = buffer.NewPool()
|
||||||
|
// GetBufferPool retrieves a buffer from the pool, creating one if necessary.
|
||||||
|
GetBufferPool = _pool.Get
|
||||||
|
)
|
100
core/logger/default.go
Normal file
100
core/logger/default.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is a logging interface.
|
||||||
|
type Logger interface {
|
||||||
|
// With adds fields to the logger and returns a new logger with those fields.
|
||||||
|
With(fields ...zap.Field) Logger
|
||||||
|
// WithLazy adds fields to the logger lazily and returns a new logger with those fields.
|
||||||
|
WithLazy(fields ...zap.Field) Logger
|
||||||
|
// Level returns the logging level of the logger.
|
||||||
|
Level() zapcore.Level
|
||||||
|
// Check checks if the log message meets the given level.
|
||||||
|
Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry
|
||||||
|
// Log logs a message with the given level and fields.
|
||||||
|
Log(lvl zapcore.Level, msg string, fields ...zap.Field)
|
||||||
|
// Debug logs a debug-level message with the given fields.
|
||||||
|
Debug(msg string, fields ...zap.Field)
|
||||||
|
// Info logs an info-level message with the given fields.
|
||||||
|
Info(msg string, fields ...zap.Field)
|
||||||
|
// Warn logs a warning-level message with the given fields.
|
||||||
|
Warn(msg string, fields ...zap.Field)
|
||||||
|
// Error logs an error-level message with the given fields.
|
||||||
|
Error(msg string, fields ...zap.Field)
|
||||||
|
// DPanic logs a debug-panic-level message with the given fields and panics
|
||||||
|
// if the logger's panic level is set to a non-zero value.
|
||||||
|
DPanic(msg string, fields ...zap.Field)
|
||||||
|
// Panic logs a panic-level message with the given fields and panics immediately.
|
||||||
|
Panic(msg string, fields ...zap.Field)
|
||||||
|
// Fatal logs a fatal-level message with the given fields, then calls os.Exit(1).
|
||||||
|
Fatal(msg string, fields ...zap.Field)
|
||||||
|
// ForHandler returns a new logger that is associated with the given handler.
|
||||||
|
ForHandler(handler any) Logger
|
||||||
|
// ForConnection returns a new logger that is associated with the given connection.
|
||||||
|
ForConnection(conn any) Logger
|
||||||
|
// ForAccount returns a new logger that is associated with the given account.
|
||||||
|
ForAccount(acc any) Logger
|
||||||
|
// Sync returns an error if there's a problem writing log messages to disk, or nil if all writes were successful.
|
||||||
|
Sync() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default is a default logger implementation.
|
||||||
|
type Default struct {
|
||||||
|
*zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefault creates a new default logger with the given format and debug level.
|
||||||
|
func NewDefault(format string, debug bool) Logger {
|
||||||
|
return &Default{
|
||||||
|
Logger: NewZap(format, debug),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With adds fields to the logger and returns a new logger with those fields.
|
||||||
|
func (l *Default) With(fields ...zap.Field) Logger {
|
||||||
|
return l.clone(l.Logger.With(fields...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLazy adds fields to the logger lazily and returns a new logger with those fields.
|
||||||
|
func (l *Default) WithLazy(fields ...zap.Field) Logger {
|
||||||
|
return l.clone(l.Logger.WithLazy(fields...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForHandler returns a new logger that is associated with the given handler.
|
||||||
|
func (l *Default) ForHandler(handler any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(HandlerAttr, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForConnection returns a new logger that is associated with the given connection.
|
||||||
|
func (l *Default) ForConnection(conn any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(ConnectionAttr, conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForAccount returns a new logger that is associated with the given account.
|
||||||
|
func (l *Default) ForAccount(acc any) Logger {
|
||||||
|
return l.WithLazy(zap.Any(AccountAttr, acc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone creates a copy of the given logger.
|
||||||
|
func (l *Default) clone(log *zap.Logger) Logger {
|
||||||
|
return &Default{Logger: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyZapFields converts an array of values to zap fields.
|
||||||
|
func AnyZapFields(args []interface{}) []zap.Field {
|
||||||
|
fields := make([]zap.Field, len(args))
|
||||||
|
for i := 0; i < len(fields); i++ {
|
||||||
|
if val, ok := args[i].(zap.Field); ok {
|
||||||
|
fields[i] = val
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields[i] = zap.Any("arg"+strconv.Itoa(i), args[i])
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
90
core/logger/default_test.go
Normal file
90
core/logger/default_test.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestDefaultSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefault(t *testing.T) {
|
||||||
|
suite.Run(t, new(TestDefaultSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestNewDefault_OK() {
|
||||||
|
jsonLog := NewDefault("json", false)
|
||||||
|
consoleLog := NewDefault("console", true)
|
||||||
|
|
||||||
|
s.Assert().NotNil(jsonLog)
|
||||||
|
s.Assert().NotNil(consoleLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestNewDefault_Panic() {
|
||||||
|
s.Assert().PanicsWithValue("unknown logger format: rar", func() {
|
||||||
|
NewDefault("rar", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestWith() {
|
||||||
|
log := newBufferLogger()
|
||||||
|
log.With(zap.String(HandlerAttr, "Handler")).Info("test")
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(items, 1)
|
||||||
|
s.Assert().Equal("Handler", items[0].Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestWithLazy() {
|
||||||
|
log := newBufferLogger()
|
||||||
|
log.WithLazy(zap.String(HandlerAttr, "Handler")).Info("test")
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(items, 1)
|
||||||
|
s.Assert().Equal("Handler", items[0].Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestForHandler() {
|
||||||
|
log := newBufferLogger()
|
||||||
|
log.ForHandler("Handler").Info("test")
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(items, 1)
|
||||||
|
s.Assert().Equal("Handler", items[0].Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestForConnection() {
|
||||||
|
log := newBufferLogger()
|
||||||
|
log.ForConnection("connection").Info("test")
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(items, 1)
|
||||||
|
s.Assert().Equal("connection", items[0].Connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestDefaultSuite) TestForAccount() {
|
||||||
|
log := newBufferLogger()
|
||||||
|
log.ForAccount("account").Info("test")
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(items, 1)
|
||||||
|
s.Assert().Equal("account", items[0].Account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyZapFields(t *testing.T) {
|
||||||
|
fields := AnyZapFields([]interface{}{zap.String("k0", "v0"), "ooga", "booga"})
|
||||||
|
require.Len(t, fields, 3)
|
||||||
|
assert.Equal(t, zap.String("k0", "v0"), fields[0])
|
||||||
|
assert.Equal(t, zap.String("arg1", "ooga"), fields[1])
|
||||||
|
assert.Equal(t, zap.String("arg2", "booga"), fields[2])
|
||||||
|
}
|
37
core/logger/gin.go
Normal file
37
core/logger/gin.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GinMiddleware will construct Gin middleware which will log requests.
|
||||||
|
func GinMiddleware(log Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Start timer
|
||||||
|
start := time.Now()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
raw := c.Request.URL.RawQuery
|
||||||
|
|
||||||
|
// Process request
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
end := time.Now()
|
||||||
|
if raw != "" {
|
||||||
|
path = path + "?" + raw
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("request",
|
||||||
|
zap.String(HandlerAttr, "GIN"),
|
||||||
|
zap.String("startTime", start.Format(time.RFC3339)),
|
||||||
|
zap.String("endTime", end.Format(time.RFC3339)),
|
||||||
|
zap.Any("latency", end.Sub(start)/time.Millisecond),
|
||||||
|
zap.String("remoteAddress", c.ClientIP()),
|
||||||
|
zap.String(HTTPMethodAttr, c.Request.Method),
|
||||||
|
zap.String("path", path),
|
||||||
|
zap.Int("bodySize", c.Writer.Size()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
38
core/logger/gin_test.go
Normal file
38
core/logger/gin_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGinMiddleware(t *testing.T) {
|
||||||
|
log := newBufferLogger()
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(GinMiddleware(log))
|
||||||
|
r.GET("/mine", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
})
|
||||||
|
r.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/mine", nil))
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, items, 1)
|
||||||
|
require.NotEmpty(t, items[0].Context)
|
||||||
|
assert.NotEmpty(t, items[0].Context["startTime"])
|
||||||
|
assert.NotEmpty(t, items[0].Context["endTime"])
|
||||||
|
assert.True(t, func() bool {
|
||||||
|
_, ok := items[0].Context["latency"]
|
||||||
|
return ok
|
||||||
|
}())
|
||||||
|
assert.NotEmpty(t, items[0].Context["remoteAddress"])
|
||||||
|
assert.NotEmpty(t, items[0].Context[HTTPMethodAttr])
|
||||||
|
assert.NotEmpty(t, items[0].Context["path"])
|
||||||
|
assert.NotEmpty(t, items[0].Context["bodySize"])
|
||||||
|
}
|
640
core/logger/json_with_context_encoder.go
Normal file
640
core/logger/json_with_context_encoder.go
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
|
"go.uber.org/zap/buffer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// For JSON-escaping; see jsonWithContextEncoder.safeAddString below.
|
||||||
|
const _hex = "0123456789abcdef"
|
||||||
|
|
||||||
|
var _jsonWithContextPool = NewPool(func() *jsonWithContextEncoder {
|
||||||
|
return &jsonWithContextEncoder{}
|
||||||
|
})
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := zap.RegisterEncoder("json-with-context", func(config zapcore.EncoderConfig) (zapcore.Encoder, error) {
|
||||||
|
return NewJSONWithContextEncoder(config), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putJSONWithContextEncoder(enc *jsonWithContextEncoder) {
|
||||||
|
if enc.reflectBuf != nil {
|
||||||
|
enc.reflectBuf.Free()
|
||||||
|
}
|
||||||
|
enc.EncoderConfig = nil
|
||||||
|
enc.buf = nil
|
||||||
|
enc.spaced = false
|
||||||
|
enc.openNamespaces = 0
|
||||||
|
enc.reflectBuf = nil
|
||||||
|
enc.reflectEnc = nil
|
||||||
|
_jsonWithContextPool.Put(enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonWithContextEncoder struct {
|
||||||
|
*zapcore.EncoderConfig
|
||||||
|
buf *buffer.Buffer
|
||||||
|
spaced bool // include spaces after colons and commas
|
||||||
|
openNamespaces int
|
||||||
|
|
||||||
|
// for encoding generic values by reflection
|
||||||
|
reflectBuf *buffer.Buffer
|
||||||
|
reflectEnc zapcore.ReflectedEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONWithContextEncoder creates a fast, low-allocation JSON encoder. The encoder
|
||||||
|
// appropriately escapes all field keys and values.
|
||||||
|
//
|
||||||
|
// Note that the encoder doesn't deduplicate keys, so it's possible to produce
|
||||||
|
// a message like
|
||||||
|
//
|
||||||
|
// {"foo":"bar","foo":"baz"}
|
||||||
|
//
|
||||||
|
// This is permitted by the JSON specification, but not encouraged. Many
|
||||||
|
// libraries will ignore duplicate key-value pairs (typically keeping the last
|
||||||
|
// pair) when unmarshaling, but users should attempt to avoid adding duplicate
|
||||||
|
// keys.
|
||||||
|
func NewJSONWithContextEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
|
||||||
|
return newJSONWithContextEncoder(cfg, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONWithContextEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonWithContextEncoder {
|
||||||
|
if cfg.SkipLineEnding {
|
||||||
|
cfg.LineEnding = ""
|
||||||
|
} else if cfg.LineEnding == "" {
|
||||||
|
cfg.LineEnding = zapcore.DefaultLineEnding
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no EncoderConfig.NewReflectedEncoder is provided by the user, then use default
|
||||||
|
if cfg.NewReflectedEncoder == nil {
|
||||||
|
cfg.NewReflectedEncoder = defaultReflectedEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jsonWithContextEncoder{
|
||||||
|
EncoderConfig: &cfg,
|
||||||
|
buf: GetBufferPool(),
|
||||||
|
spaced: spaced,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultReflectedEncoder(w io.Writer) zapcore.ReflectedEncoder {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
// For consistency with our custom JSON encoder.
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
|
||||||
|
enc.addKey(key)
|
||||||
|
return enc.AppendArray(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
|
||||||
|
enc.addKey(key)
|
||||||
|
return enc.AppendObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddBinary(key string, val []byte) {
|
||||||
|
enc.AddString(key, base64.StdEncoding.EncodeToString(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddByteString(key string, val []byte) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendByteString(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddBool(key string, val bool) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendBool(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddComplex128(key string, val complex128) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendComplex128(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddComplex64(key string, val complex64) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendComplex64(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddDuration(key string, val time.Duration) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendDuration(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddFloat64(key string, val float64) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendFloat64(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddFloat32(key string, val float32) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendFloat32(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddInt64(key string, val int64) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendInt64(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) resetReflectBuf() {
|
||||||
|
if enc.reflectBuf == nil {
|
||||||
|
enc.reflectBuf = GetBufferPool()
|
||||||
|
enc.reflectEnc = enc.NewReflectedEncoder(enc.reflectBuf)
|
||||||
|
} else {
|
||||||
|
enc.reflectBuf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nullLiteralBytes = []byte("null")
|
||||||
|
|
||||||
|
// Only invoke the standard JSON encoder if there is actually something to
|
||||||
|
// encode; otherwise write JSON null literal directly.
|
||||||
|
func (enc *jsonWithContextEncoder) encodeReflected(obj interface{}) ([]byte, error) {
|
||||||
|
if obj == nil {
|
||||||
|
return nullLiteralBytes, nil
|
||||||
|
}
|
||||||
|
enc.resetReflectBuf()
|
||||||
|
if err := enc.reflectEnc.Encode(obj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
enc.reflectBuf.TrimNewline()
|
||||||
|
return enc.reflectBuf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddReflected(key string, obj interface{}) error {
|
||||||
|
valueBytes, err := enc.encodeReflected(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enc.addKey(key)
|
||||||
|
_, err = enc.buf.Write(valueBytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) OpenNamespace(key string) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.buf.AppendByte('{')
|
||||||
|
enc.openNamespaces++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddString(key, val string) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendString(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddTime(key string, val time.Time) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendTime(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddUint64(key string, val uint64) {
|
||||||
|
enc.addKey(key)
|
||||||
|
enc.AppendUint64(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('[')
|
||||||
|
err := arr.MarshalLogArray(enc)
|
||||||
|
enc.buf.AppendByte(']')
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
|
||||||
|
// Close ONLY new openNamespaces that are created during
|
||||||
|
// AppendObject().
|
||||||
|
old := enc.openNamespaces
|
||||||
|
enc.openNamespaces = 0
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('{')
|
||||||
|
err := obj.MarshalLogObject(enc)
|
||||||
|
enc.buf.AppendByte('}')
|
||||||
|
enc.closeOpenNamespaces()
|
||||||
|
enc.openNamespaces = old
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendBool(val bool) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendBool(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendByteString(val []byte) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
enc.safeAddByteString(val)
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendComplex appends the encoded form of the provided complex128 value.
|
||||||
|
// precision specifies the encoding precision for the real and imaginary
|
||||||
|
// components of the complex number.
|
||||||
|
func (enc *jsonWithContextEncoder) appendComplex(val complex128, precision int) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
// Cast to a platform-independent, fixed-size type.
|
||||||
|
r, i := float64(real(val)), float64(imag(val))
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
// Because we're always in a quoted string, we can use strconv without
|
||||||
|
// special-casing NaN and +/-Inf.
|
||||||
|
enc.buf.AppendFloat(r, precision)
|
||||||
|
// If imaginary part is less than 0, minus (-) sign is added by default
|
||||||
|
// by AppendFloat.
|
||||||
|
if i >= 0 {
|
||||||
|
enc.buf.AppendByte('+')
|
||||||
|
}
|
||||||
|
enc.buf.AppendFloat(i, precision)
|
||||||
|
enc.buf.AppendByte('i')
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendDuration(val time.Duration) {
|
||||||
|
cur := enc.buf.Len()
|
||||||
|
if e := enc.EncodeDuration; e != nil {
|
||||||
|
e(val, enc)
|
||||||
|
}
|
||||||
|
if cur == enc.buf.Len() {
|
||||||
|
// User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
|
||||||
|
// JSON valid.
|
||||||
|
enc.AppendInt64(int64(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendInt64(val int64) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendInt(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendReflected(val interface{}) error {
|
||||||
|
valueBytes, err := enc.encodeReflected(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enc.addElementSeparator()
|
||||||
|
_, err = enc.buf.Write(valueBytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendString(val string) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
enc.safeAddString(val)
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendTimeLayout(time time.Time, layout string) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
enc.buf.AppendTime(time, layout)
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendTime(val time.Time) {
|
||||||
|
cur := enc.buf.Len()
|
||||||
|
if e := enc.EncodeTime; e != nil {
|
||||||
|
e(val, enc)
|
||||||
|
}
|
||||||
|
if cur == enc.buf.Len() {
|
||||||
|
// User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
|
||||||
|
// output JSON valid.
|
||||||
|
enc.AppendInt64(val.UnixNano())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUint64(val uint64) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendUint(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendComplex64(v complex64) { enc.appendComplex(complex128(v), 32) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendComplex128(v complex128) {
|
||||||
|
enc.appendComplex(complex128(v), 64)
|
||||||
|
}
|
||||||
|
func (enc *jsonWithContextEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
|
||||||
|
func (enc *jsonWithContextEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) Clone() zapcore.Encoder {
|
||||||
|
clone := enc.clone()
|
||||||
|
clone.buf.Write(enc.buf.Bytes())
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) clone() *jsonWithContextEncoder {
|
||||||
|
clone := _jsonWithContextPool.Get()
|
||||||
|
clone.EncoderConfig = enc.EncoderConfig
|
||||||
|
clone.spaced = enc.spaced
|
||||||
|
clone.openNamespaces = enc.openNamespaces
|
||||||
|
clone.buf = GetBufferPool()
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
|
||||||
|
final := enc.clone()
|
||||||
|
final.buf.AppendByte('{')
|
||||||
|
|
||||||
|
if final.LevelKey != "" && final.EncodeLevel != nil {
|
||||||
|
final.addKey(final.LevelKey)
|
||||||
|
cur := final.buf.Len()
|
||||||
|
final.EncodeLevel(ent.Level, final)
|
||||||
|
if cur == final.buf.Len() {
|
||||||
|
// User-supplied EncodeLevel was a no-op. Fall back to strings to keep
|
||||||
|
// output JSON valid.
|
||||||
|
final.AppendString(ent.Level.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if final.TimeKey != "" {
|
||||||
|
final.AddTime(final.TimeKey, ent.Time)
|
||||||
|
}
|
||||||
|
if ent.LoggerName != "" && final.NameKey != "" {
|
||||||
|
final.addKey(final.NameKey)
|
||||||
|
cur := final.buf.Len()
|
||||||
|
nameEncoder := final.EncodeName
|
||||||
|
|
||||||
|
// if no name encoder provided, fall back to FullNameEncoder for backwards
|
||||||
|
// compatibility
|
||||||
|
if nameEncoder == nil {
|
||||||
|
nameEncoder = zapcore.FullNameEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
nameEncoder(ent.LoggerName, final)
|
||||||
|
if cur == final.buf.Len() {
|
||||||
|
// User-supplied EncodeName was a no-op. Fall back to strings to
|
||||||
|
// keep output JSON valid.
|
||||||
|
final.AppendString(ent.LoggerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ent.Caller.Defined {
|
||||||
|
if final.CallerKey != "" {
|
||||||
|
final.addKey(final.CallerKey)
|
||||||
|
cur := final.buf.Len()
|
||||||
|
final.EncodeCaller(ent.Caller, final)
|
||||||
|
if cur == final.buf.Len() {
|
||||||
|
// User-supplied EncodeCaller was a no-op. Fall back to strings to
|
||||||
|
// keep output JSON valid.
|
||||||
|
final.AppendString(ent.Caller.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if final.FunctionKey != "" {
|
||||||
|
final.addKey(final.FunctionKey)
|
||||||
|
final.AppendString(ent.Caller.Function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if final.MessageKey != "" {
|
||||||
|
final.addKey(enc.MessageKey)
|
||||||
|
final.AppendString(ent.Message)
|
||||||
|
}
|
||||||
|
if enc.buf.Len() > 0 {
|
||||||
|
final.addElementSeparator()
|
||||||
|
final.buf.Write(enc.buf.Bytes())
|
||||||
|
}
|
||||||
|
addFields(final, fields)
|
||||||
|
final.closeOpenNamespaces()
|
||||||
|
if ent.Stack != "" && final.StacktraceKey != "" {
|
||||||
|
final.AddString(final.StacktraceKey, ent.Stack)
|
||||||
|
}
|
||||||
|
final.buf.AppendByte('}')
|
||||||
|
final.buf.AppendString(final.LineEnding)
|
||||||
|
|
||||||
|
ret := final.buf
|
||||||
|
putJSONWithContextEncoder(final)
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
hasEntries := false
|
||||||
|
for _, f := range fields {
|
||||||
|
switch f.Key {
|
||||||
|
case HandlerAttr, ConnectionAttr, AccountAttr:
|
||||||
|
f.AddTo(enc)
|
||||||
|
default:
|
||||||
|
hasEntries = true
|
||||||
|
if f.Interface != nil {
|
||||||
|
switch t := f.Interface.(type) {
|
||||||
|
case fmt.Stringer:
|
||||||
|
m[f.Key] = t.String()
|
||||||
|
case fmt.GoStringer:
|
||||||
|
m[f.Key] = t.GoString()
|
||||||
|
case error:
|
||||||
|
m[f.Key] = t.Error()
|
||||||
|
default:
|
||||||
|
m[f.Key] = f.Interface
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.String != "" {
|
||||||
|
m[f.Key] = f.String
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m[f.Key] = f.Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasEntries {
|
||||||
|
zap.Any("context", m).AddTo(enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) truncate() {
|
||||||
|
enc.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) closeOpenNamespaces() {
|
||||||
|
for i := 0; i < enc.openNamespaces; i++ {
|
||||||
|
enc.buf.AppendByte('}')
|
||||||
|
}
|
||||||
|
enc.openNamespaces = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) addKey(key string) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
enc.safeAddString(key)
|
||||||
|
enc.buf.AppendByte('"')
|
||||||
|
enc.buf.AppendByte(':')
|
||||||
|
if enc.spaced {
|
||||||
|
enc.buf.AppendByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) addElementSeparator() {
|
||||||
|
last := enc.buf.Len() - 1
|
||||||
|
if last < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch enc.buf.Bytes()[last] {
|
||||||
|
case '{', '[', ':', ',', ' ':
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
enc.buf.AppendByte(',')
|
||||||
|
if enc.spaced {
|
||||||
|
enc.buf.AppendByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *jsonWithContextEncoder) appendFloat(val float64, bitSize int) {
|
||||||
|
enc.addElementSeparator()
|
||||||
|
switch {
|
||||||
|
case math.IsNaN(val):
|
||||||
|
enc.buf.AppendString(`"NaN"`)
|
||||||
|
case math.IsInf(val, 1):
|
||||||
|
enc.buf.AppendString(`"+Inf"`)
|
||||||
|
case math.IsInf(val, -1):
|
||||||
|
enc.buf.AppendString(`"-Inf"`)
|
||||||
|
default:
|
||||||
|
enc.buf.AppendFloat(val, bitSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeAddString JSON-escapes a string and appends it to the internal buffer.
|
||||||
|
// Unlike the standard library's encoder, it doesn't attempt to protect the
|
||||||
|
// user from browser vulnerabilities or JSONP-related problems.
|
||||||
|
func (enc *jsonWithContextEncoder) safeAddString(s string) {
|
||||||
|
safeAppendStringLike(
|
||||||
|
(*buffer.Buffer).AppendString,
|
||||||
|
utf8.DecodeRuneInString,
|
||||||
|
enc.buf,
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
|
||||||
|
func (enc *jsonWithContextEncoder) safeAddByteString(s []byte) {
|
||||||
|
safeAppendStringLike(
|
||||||
|
(*buffer.Buffer).AppendBytes,
|
||||||
|
utf8.DecodeRune,
|
||||||
|
enc.buf,
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeAppendStringLike is a generic implementation of safeAddString and safeAddByteString.
|
||||||
|
// It appends a string or byte slice to the buffer, escaping all special characters.
|
||||||
|
func safeAppendStringLike[S []byte | string](
|
||||||
|
// appendTo appends this string-like object to the buffer.
|
||||||
|
appendTo func(*buffer.Buffer, S),
|
||||||
|
// decodeRune decodes the next rune from the string-like object
|
||||||
|
// and returns its value and width in bytes.
|
||||||
|
decodeRune func(S) (rune, int),
|
||||||
|
buf *buffer.Buffer,
|
||||||
|
s S,
|
||||||
|
) {
|
||||||
|
// The encoding logic below works by skipping over characters
|
||||||
|
// that can be safely copied as-is,
|
||||||
|
// until a character is found that needs special handling.
|
||||||
|
// At that point, we copy everything we've seen so far,
|
||||||
|
// and then handle that special character.
|
||||||
|
//
|
||||||
|
// last is the index of the last byte that was copied to the buffer.
|
||||||
|
last := 0
|
||||||
|
for i := 0; i < len(s); {
|
||||||
|
if s[i] >= utf8.RuneSelf {
|
||||||
|
// Character >= RuneSelf may be part of a multi-byte rune.
|
||||||
|
// They need to be decoded before we can decide how to handle them.
|
||||||
|
r, size := decodeRune(s[i:])
|
||||||
|
if r != utf8.RuneError || size != 1 {
|
||||||
|
// No special handling required.
|
||||||
|
// Skip over this rune and continue.
|
||||||
|
i += size
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid UTF-8 sequence.
|
||||||
|
// Replace it with the Unicode replacement character.
|
||||||
|
appendTo(buf, s[last:i])
|
||||||
|
buf.AppendString(`\ufffd`)
|
||||||
|
|
||||||
|
i++
|
||||||
|
last = i
|
||||||
|
} else {
|
||||||
|
// Character < RuneSelf is a single-byte UTF-8 rune.
|
||||||
|
if s[i] >= 0x20 && s[i] != '\\' && s[i] != '"' {
|
||||||
|
// No escaping necessary.
|
||||||
|
// Skip over this character and continue.
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This character needs to be escaped.
|
||||||
|
appendTo(buf, s[last:i])
|
||||||
|
switch s[i] {
|
||||||
|
case '\\', '"':
|
||||||
|
buf.AppendByte('\\')
|
||||||
|
buf.AppendByte(s[i])
|
||||||
|
case '\n':
|
||||||
|
buf.AppendByte('\\')
|
||||||
|
buf.AppendByte('n')
|
||||||
|
case '\r':
|
||||||
|
buf.AppendByte('\\')
|
||||||
|
buf.AppendByte('r')
|
||||||
|
case '\t':
|
||||||
|
buf.AppendByte('\\')
|
||||||
|
buf.AppendByte('t')
|
||||||
|
default:
|
||||||
|
// Encode bytes < 0x20, except for the escape sequences above.
|
||||||
|
buf.AppendString(`\u00`)
|
||||||
|
buf.AppendByte(_hex[s[i]>>4])
|
||||||
|
buf.AppendByte(_hex[s[i]&0xF])
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
last = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add remaining
|
||||||
|
appendTo(buf, s[last:])
|
||||||
|
}
|
@ -1,205 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger contains methods which should be present in logger implementation.
|
|
||||||
type Logger interface {
|
|
||||||
Fatal(args ...interface{})
|
|
||||||
Fatalf(format string, args ...interface{})
|
|
||||||
Panic(args ...interface{})
|
|
||||||
Panicf(format string, args ...interface{})
|
|
||||||
Critical(args ...interface{})
|
|
||||||
Criticalf(format string, args ...interface{})
|
|
||||||
Error(args ...interface{})
|
|
||||||
Errorf(format string, args ...interface{})
|
|
||||||
Warning(args ...interface{})
|
|
||||||
Warningf(format string, args ...interface{})
|
|
||||||
Notice(args ...interface{})
|
|
||||||
Noticef(format string, args ...interface{})
|
|
||||||
Info(args ...interface{})
|
|
||||||
Infof(format string, args ...interface{})
|
|
||||||
Debug(args ...interface{})
|
|
||||||
Debugf(format string, args ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// StandardLogger is a default implementation of Logger. Uses github.com/op/go-logging under the hood.
|
|
||||||
// This logger can prevent any write operations (disabled by default, use .Exclusive() method to enable).
|
|
||||||
type StandardLogger struct {
|
|
||||||
logger *logging.Logger
|
|
||||||
mutex *sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStandard will create new StandardLogger with specified formatter.
|
|
||||||
// Usage:
|
|
||||||
// logger := NewLogger("telegram", logging.ERROR, DefaultLogFormatter())
|
|
||||||
func NewStandard(transportCode string, logLevel logging.Level, logFormat logging.Formatter) *StandardLogger {
|
|
||||||
return &StandardLogger{
|
|
||||||
logger: NewBase(os.Stdout, transportCode, logLevel, logFormat),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBase is a constructor for underlying logger in the StandardLogger struct.
|
|
||||||
func NewBase(out io.Writer, transportCode string, logLevel logging.Level, logFormat logging.Formatter) *logging.Logger {
|
|
||||||
logger := logging.MustGetLogger(transportCode)
|
|
||||||
logBackend := logging.NewLogBackend(out, "", 0)
|
|
||||||
formatBackend := logging.NewBackendFormatter(logBackend, logFormat)
|
|
||||||
backend1Leveled := logging.AddModuleLevel(formatBackend)
|
|
||||||
backend1Leveled.SetLevel(logLevel, "")
|
|
||||||
logger.SetBackend(backend1Leveled)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultLogFormatter will return default formatter for logs.
|
|
||||||
func DefaultLogFormatter() logging.Formatter {
|
|
||||||
return logging.MustStringFormatter(
|
|
||||||
`%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclusive makes logger goroutine-safe.
|
|
||||||
func (l *StandardLogger) Exclusive() *StandardLogger {
|
|
||||||
if l.mutex == nil {
|
|
||||||
l.mutex = &sync.RWMutex{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBaseLogger replaces base logger with the provided instance.
|
|
||||||
func (l *StandardLogger) SetBaseLogger(logger *logging.Logger) *StandardLogger {
|
|
||||||
l.logger = logger
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// lock locks logger.
|
|
||||||
func (l *StandardLogger) lock() {
|
|
||||||
if l.mutex != nil {
|
|
||||||
l.mutex.Lock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unlock unlocks logger.
|
|
||||||
func (l *StandardLogger) unlock() {
|
|
||||||
if l.mutex != nil {
|
|
||||||
l.mutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1).
|
|
||||||
func (l *StandardLogger) Fatal(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Fatal(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf is equivalent to l.Critical followed by a call to os.Exit(1).
|
|
||||||
func (l *StandardLogger) Fatalf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Fatalf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panic is equivalent to l.Critical(fmt.Sprint()) followed by a call to panic().
|
|
||||||
func (l *StandardLogger) Panic(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Panic(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panicf is equivalent to l.Critical followed by a call to panic().
|
|
||||||
func (l *StandardLogger) Panicf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Panicf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Critical logs a message using CRITICAL as log level.
|
|
||||||
func (l *StandardLogger) Critical(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Critical(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criticalf logs a message using CRITICAL as log level.
|
|
||||||
func (l *StandardLogger) Criticalf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Criticalf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs a message using ERROR as log level.
|
|
||||||
func (l *StandardLogger) Error(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Error(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorf logs a message using ERROR as log level.
|
|
||||||
func (l *StandardLogger) Errorf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Errorf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warning logs a message using WARNING as log level.
|
|
||||||
func (l *StandardLogger) Warning(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Warning(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningf logs a message using WARNING as log level.
|
|
||||||
func (l *StandardLogger) Warningf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Warningf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notice logs a message using NOTICE as log level.
|
|
||||||
func (l *StandardLogger) Notice(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Notice(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Noticef logs a message using NOTICE as log level.
|
|
||||||
func (l *StandardLogger) Noticef(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Noticef(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs a message using INFO as log level.
|
|
||||||
func (l *StandardLogger) Info(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Info(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof logs a message using INFO as log level.
|
|
||||||
func (l *StandardLogger) Infof(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Infof(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs a message using DEBUG as log level.
|
|
||||||
func (l *StandardLogger) Debug(args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Debug(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugf logs a message using DEBUG as log level.
|
|
||||||
func (l *StandardLogger) Debugf(format string, args ...interface{}) {
|
|
||||||
l.lock()
|
|
||||||
defer l.unlock()
|
|
||||||
l.logger.Debugf(format, args...)
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StandardLoggerTest struct {
|
|
||||||
suite.Suite
|
|
||||||
logger *StandardLogger
|
|
||||||
buf *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogger_NewLogger(t *testing.T) {
|
|
||||||
logger := NewStandard("code", logging.DEBUG, DefaultLogFormatter())
|
|
||||||
assert.NotNil(t, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogger_DefaultLogFormatter(t *testing.T) {
|
|
||||||
formatter := DefaultLogFormatter()
|
|
||||||
|
|
||||||
assert.NotNil(t, formatter)
|
|
||||||
assert.IsType(t, logging.MustStringFormatter(`%{message}`), formatter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Logger(t *testing.T) {
|
|
||||||
suite.Run(t, new(StandardLoggerTest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) SetupSuite() {
|
|
||||||
t.buf = &bytes.Buffer{}
|
|
||||||
t.logger = (&StandardLogger{}).
|
|
||||||
Exclusive().
|
|
||||||
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) SetupTest() {
|
|
||||||
t.buf.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Cover Fatal and Fatalf (implementation below is no-op)
|
|
||||||
// func (t *StandardLoggerTest) Test_Fatal() {
|
|
||||||
// if os.Getenv("FLAG") == "1" {
|
|
||||||
// t.logger.Fatal("test", "fatal")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// cmd := exec.Command(os.Args[0], "-test.run=TestGetConfig")
|
|
||||||
// cmd.Env = append(os.Environ(), "FLAG=1")
|
|
||||||
// err := cmd.Run()
|
|
||||||
|
|
||||||
// e, ok := err.(*exec.ExitError)
|
|
||||||
// expectedErrorString := "test fatal"
|
|
||||||
// t.Assert().Equal(true, ok)
|
|
||||||
// t.Assert().Equal(expectedErrorString, e.Error())
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Panic() {
|
|
||||||
defer func() {
|
|
||||||
t.Assert().NotNil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), "panic")
|
|
||||||
}()
|
|
||||||
t.logger.Panic("panic")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Panicf() {
|
|
||||||
defer func() {
|
|
||||||
t.Assert().NotNil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), "panicf")
|
|
||||||
}()
|
|
||||||
t.logger.Panicf("panicf")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Critical() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), "critical")
|
|
||||||
}()
|
|
||||||
t.logger.Critical("critical")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Criticalf() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), "critical")
|
|
||||||
}()
|
|
||||||
t.logger.Criticalf("critical")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Warning() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "WARN")
|
|
||||||
t.Assert().Contains(t.buf.String(), "warning")
|
|
||||||
}()
|
|
||||||
t.logger.Warning("warning")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Notice() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "NOTI")
|
|
||||||
t.Assert().Contains(t.buf.String(), "notice")
|
|
||||||
}()
|
|
||||||
t.logger.Notice("notice")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Info() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), "info")
|
|
||||||
}()
|
|
||||||
t.logger.Info("info")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Debug() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "DEBU")
|
|
||||||
t.Assert().Contains(t.buf.String(), "debug")
|
|
||||||
}()
|
|
||||||
t.logger.Debug("debug")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Warningf() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "WARN")
|
|
||||||
t.Assert().Contains(t.buf.String(), "warning")
|
|
||||||
}()
|
|
||||||
t.logger.Warningf("%s", "warning")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Noticef() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "NOTI")
|
|
||||||
t.Assert().Contains(t.buf.String(), "notice")
|
|
||||||
}()
|
|
||||||
t.logger.Noticef("%s", "notice")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Infof() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), "info")
|
|
||||||
}()
|
|
||||||
t.logger.Infof("%s", "info")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *StandardLoggerTest) Test_Debugf() {
|
|
||||||
defer func() {
|
|
||||||
t.Require().Nil(recover())
|
|
||||||
t.Assert().Contains(t.buf.String(), "DEBU")
|
|
||||||
t.Assert().Contains(t.buf.String(), "debug")
|
|
||||||
}()
|
|
||||||
t.logger.Debugf("%s", "debug")
|
|
||||||
}
|
|
60
core/logger/mg_transport_client_adapter.go
Normal file
60
core/logger/mg_transport_client_adapter.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mgDebugLogReq = "MG TRANSPORT API Request: %s %s %s %v"
|
||||||
|
mgDebugLogReqFile = "MG TRANSPORT API Request: %s %s %s [file data]"
|
||||||
|
mgDebugLogResp = "MG TRANSPORT API Response: %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mgTransportClientAdapter struct {
|
||||||
|
log Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGTransportClientAdapter constructs an adapter that will log MG requests and responses.
|
||||||
|
func MGTransportClientAdapter(log Logger) v1.BasicLogger {
|
||||||
|
return &mgTransportClientAdapter{log: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf writes a message with Debug level.
|
||||||
|
func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) {
|
||||||
|
var body interface{}
|
||||||
|
switch msg {
|
||||||
|
case mgDebugLogReqFile:
|
||||||
|
body = "[file data]"
|
||||||
|
fallthrough
|
||||||
|
case mgDebugLogReq:
|
||||||
|
var method, uri, token string
|
||||||
|
if len(args) > 0 {
|
||||||
|
method = fmt.Sprint(args[0])
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
uri = fmt.Sprint(args[1])
|
||||||
|
}
|
||||||
|
if len(args) > 2 { // nolint:gomnd
|
||||||
|
token = fmt.Sprint(args[2])
|
||||||
|
}
|
||||||
|
if len(args) > 3 { // nolint:gomnd
|
||||||
|
body = args[3]
|
||||||
|
}
|
||||||
|
m.log.Debug("MG TRANSPORT API Request",
|
||||||
|
zap.String(HTTPMethodAttr, method), zap.String("url", uri),
|
||||||
|
zap.String("token", token), Body(body))
|
||||||
|
case mgDebugLogResp:
|
||||||
|
m.log.Debug("MG TRANSPORT API Response", Body(args[0]))
|
||||||
|
default:
|
||||||
|
m.log.Debug(fmt.Sprintf(msg, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a v1.BasicLogger implementation.
|
||||||
|
func (m *mgTransportClientAdapter) Printf(msg string, args ...interface{}) {
|
||||||
|
m.Debugf(msg, args...)
|
||||||
|
}
|
45
core/logger/mg_transport_client_adapter_test.go
Normal file
45
core/logger/mg_transport_client_adapter_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMGTransportClientAdapter(t *testing.T) {
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
log := newJSONBufferedLogger(nil)
|
||||||
|
client := v1.NewWithClient("https://mg.dev", "test_token", httpClient).
|
||||||
|
WithLogger(MGTransportClientAdapter(log.Logger()))
|
||||||
|
client.Debug = true
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://mg.dev").
|
||||||
|
Get("/api/transport/v1/channels").
|
||||||
|
Reply(http.StatusOK).
|
||||||
|
JSON([]v1.ChannelListItem{{ID: 123}})
|
||||||
|
|
||||||
|
_, _, err := client.TransportChannels(v1.Channels{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entries, err := log.ScanAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, entries, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, "DEBUG", entries[0].LevelName)
|
||||||
|
assert.True(t, entries[0].DateTime.Valid)
|
||||||
|
assert.Equal(t, "MG TRANSPORT API Request", entries[0].Message)
|
||||||
|
assert.Equal(t, http.MethodGet, entries[0].Context["method"])
|
||||||
|
assert.Equal(t, "test_token", entries[0].Context["token"])
|
||||||
|
assert.Equal(t, "https://mg.dev/api/transport/v1/channels?", entries[0].Context["url"])
|
||||||
|
|
||||||
|
assert.Equal(t, "DEBUG", entries[1].LevelName)
|
||||||
|
assert.True(t, entries[1].DateTime.Valid)
|
||||||
|
assert.Equal(t, "MG TRANSPORT API Response", entries[1].Message)
|
||||||
|
assert.Equal(t, float64(123), entries[1].Context["body"].([]interface{})[0].(map[string]interface{})["id"])
|
||||||
|
}
|
62
core/logger/nil.go
Normal file
62
core/logger/nil.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nil logger doesn't do anything.
|
||||||
|
type Nil struct{}
|
||||||
|
|
||||||
|
// NewNil constructs new *Nil.
|
||||||
|
func NewNil() Logger {
|
||||||
|
return &Nil{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) With(_ ...zap.Field) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) WithLazy(_ ...zap.Field) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) Level() zapcore.Level {
|
||||||
|
return zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) Check(_ zapcore.Level, _ string) *zapcore.CheckedEntry {
|
||||||
|
return &zapcore.CheckedEntry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) Log(_ zapcore.Level, _ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Debug(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Info(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Warn(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Error(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) DPanic(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Panic(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) Fatal(_ string, _ ...zap.Field) {}
|
||||||
|
|
||||||
|
func (l *Nil) ForHandler(_ any) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) ForConnection(_ any) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) ForAccount(_ any) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Nil) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Nil provides Logger implementation that does almost nothing when called.
|
|
||||||
// Panic, Panicf, Fatal and Fatalf methods still cause panic and immediate program termination respectively.
|
|
||||||
// All other methods won't do anything at all.
|
|
||||||
type Nil struct{}
|
|
||||||
|
|
||||||
func (n Nil) Fatal(args ...interface{}) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Nil) Fatalf(format string, args ...interface{}) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Nil) Panic(args ...interface{}) {
|
|
||||||
panic(fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Nil) Panicf(format string, args ...interface{}) {
|
|
||||||
panic(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Nil) Critical(args ...interface{}) {}
|
|
||||||
func (n Nil) Criticalf(format string, args ...interface{}) {}
|
|
||||||
func (n Nil) Error(args ...interface{}) {}
|
|
||||||
func (n Nil) Errorf(format string, args ...interface{}) {}
|
|
||||||
func (n Nil) Warning(args ...interface{}) {}
|
|
||||||
func (n Nil) Warningf(format string, args ...interface{}) {}
|
|
||||||
func (n Nil) Notice(args ...interface{}) {}
|
|
||||||
func (n Nil) Noticef(format string, args ...interface{}) {}
|
|
||||||
func (n Nil) Info(args ...interface{}) {}
|
|
||||||
func (n Nil) Infof(format string, args ...interface{}) {}
|
|
||||||
func (n Nil) Debug(args ...interface{}) {}
|
|
||||||
func (n Nil) Debugf(format string, args ...interface{}) {}
|
|
||||||
|
|
||||||
// NewNil is a Nil logger constructor.
|
|
||||||
func NewNil() Logger {
|
|
||||||
return &Nil{}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NilTest struct {
|
|
||||||
suite.Suite
|
|
||||||
logger Logger
|
|
||||||
realStdout *os.File
|
|
||||||
r *os.File
|
|
||||||
w *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNilLogger(t *testing.T) {
|
|
||||||
suite.Run(t, new(NilTest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) SetupSuite() {
|
|
||||||
t.logger = NewNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) SetupTest() {
|
|
||||||
t.realStdout = os.Stdout
|
|
||||||
t.r, t.w, _ = os.Pipe()
|
|
||||||
os.Stdout = t.w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) TearDownTest() {
|
|
||||||
if t.realStdout != nil {
|
|
||||||
t.Require().NoError(t.w.Close())
|
|
||||||
os.Stdout = t.realStdout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) readStdout() string {
|
|
||||||
outC := make(chan string)
|
|
||||||
go func() {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_, err := io.Copy(&buf, t.r)
|
|
||||||
t.Require().NoError(err)
|
|
||||||
outC <- buf.String()
|
|
||||||
close(outC)
|
|
||||||
}()
|
|
||||||
|
|
||||||
t.Require().NoError(t.w.Close())
|
|
||||||
os.Stdout = t.realStdout
|
|
||||||
t.realStdout = nil
|
|
||||||
|
|
||||||
select {
|
|
||||||
case c := <-outC:
|
|
||||||
return c
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) Test_Noop() {
|
|
||||||
t.logger.Critical("message")
|
|
||||||
t.logger.Criticalf("message")
|
|
||||||
t.logger.Error("message")
|
|
||||||
t.logger.Errorf("message")
|
|
||||||
t.logger.Warning("message")
|
|
||||||
t.logger.Warningf("message")
|
|
||||||
t.logger.Notice("message")
|
|
||||||
t.logger.Noticef("message")
|
|
||||||
t.logger.Info("message")
|
|
||||||
t.logger.Infof("message")
|
|
||||||
t.logger.Debug("message")
|
|
||||||
t.logger.Debugf("message")
|
|
||||||
|
|
||||||
t.Assert().Empty(t.readStdout())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) Test_Panic() {
|
|
||||||
t.Assert().Panics(func() {
|
|
||||||
t.logger.Panic("")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *NilTest) Test_Panicf() {
|
|
||||||
t.Assert().Panics(func() {
|
|
||||||
t.logger.Panicf("")
|
|
||||||
})
|
|
||||||
}
|
|
57
core/logger/pool.go
Normal file
57
core/logger/pool.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2023 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed
|
||||||
|
// object pooling.
|
||||||
|
//
|
||||||
|
// Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will
|
||||||
|
// not be detected, so all internal pool use must take care to only store
|
||||||
|
// pointer types.
|
||||||
|
type Pool[T any] struct {
|
||||||
|
pool sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool returns a new [Pool] for T, and will use fn to construct new Ts when
|
||||||
|
// the pool is empty.
|
||||||
|
func NewPool[T any](fn func() T) *Pool[T] {
|
||||||
|
return &Pool[T]{
|
||||||
|
pool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return fn()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a T from the pool, or creates a new one if the pool is empty.
|
||||||
|
func (p *Pool[T]) Get() T {
|
||||||
|
return p.pool.Get().(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put returns x into the pool.
|
||||||
|
func (p *Pool[T]) Put(x T) {
|
||||||
|
p.pool.Put(x)
|
||||||
|
}
|
19
core/logger/pool_test.go
Normal file
19
core/logger/pool_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPool(t *testing.T) {
|
||||||
|
p := NewPool[*uint8](func() *uint8 {
|
||||||
|
item := uint8(22)
|
||||||
|
return &item
|
||||||
|
})
|
||||||
|
|
||||||
|
val := p.Get()
|
||||||
|
assert.Equal(t, uint8(22), *val)
|
||||||
|
assert.Equal(t, uint8(22), *p.Get())
|
||||||
|
p.Put(val)
|
||||||
|
}
|
@ -1,111 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import "github.com/op/go-logging"
|
|
||||||
|
|
||||||
// PrefixAware is implemented if the logger allows you to change the prefix.
|
|
||||||
type PrefixAware interface {
|
|
||||||
SetPrefix(string)
|
|
||||||
Prefix() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrefixedLogger is a base interface for the logger with prefix.
|
|
||||||
type PrefixedLogger interface {
|
|
||||||
Logger
|
|
||||||
PrefixAware
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrefixDecorator is an implementation of the PrefixedLogger. It will allow you to decorate any Logger with
|
|
||||||
// the provided predefined prefix.
|
|
||||||
type PrefixDecorator struct {
|
|
||||||
backend Logger
|
|
||||||
prefix []interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecorateWithPrefix using provided base logger and provided prefix.
|
|
||||||
// No internal state of the base logger will be touched.
|
|
||||||
func DecorateWithPrefix(backend Logger, prefix string) PrefixedLogger {
|
|
||||||
return &PrefixDecorator{backend: backend, prefix: []interface{}{prefix}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithPrefix returns logger with prefix. It uses StandardLogger under the hood.
|
|
||||||
func NewWithPrefix(transportCode, prefix string, logLevel logging.Level, logFormat logging.Formatter) PrefixedLogger {
|
|
||||||
return DecorateWithPrefix(NewStandard(transportCode, logLevel, logFormat), prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPrefix will replace existing prefix with the provided value.
|
|
||||||
// Use this format for prefixes: "prefix here:" - omit space at the end (it will be inserted automatically).
|
|
||||||
func (p *PrefixDecorator) SetPrefix(prefix string) {
|
|
||||||
p.prefix = []interface{}{prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) getFormat(fmt string) string {
|
|
||||||
return p.prefix[0].(string) + " " + fmt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Prefix() string {
|
|
||||||
return p.prefix[0].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Fatal(args ...interface{}) {
|
|
||||||
p.backend.Fatal(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Fatalf(format string, args ...interface{}) {
|
|
||||||
p.backend.Fatalf(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Panic(args ...interface{}) {
|
|
||||||
p.backend.Panic(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Panicf(format string, args ...interface{}) {
|
|
||||||
p.backend.Panicf(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Critical(args ...interface{}) {
|
|
||||||
p.backend.Critical(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Criticalf(format string, args ...interface{}) {
|
|
||||||
p.backend.Criticalf(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Error(args ...interface{}) {
|
|
||||||
p.backend.Error(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Errorf(format string, args ...interface{}) {
|
|
||||||
p.backend.Errorf(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Warning(args ...interface{}) {
|
|
||||||
p.backend.Warning(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Warningf(format string, args ...interface{}) {
|
|
||||||
p.backend.Warningf(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Notice(args ...interface{}) {
|
|
||||||
p.backend.Notice(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Noticef(format string, args ...interface{}) {
|
|
||||||
p.backend.Noticef(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Info(args ...interface{}) {
|
|
||||||
p.backend.Info(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Infof(format string, args ...interface{}) {
|
|
||||||
p.backend.Infof(p.getFormat(format), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Debug(args ...interface{}) {
|
|
||||||
p.backend.Debug(append(p.prefix, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PrefixDecorator) Debugf(format string, args ...interface{}) {
|
|
||||||
p.backend.Debugf(p.getFormat(format), args...)
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const testPrefix = "TestPrefix:"
|
|
||||||
|
|
||||||
type PrefixDecoratorTest struct {
|
|
||||||
suite.Suite
|
|
||||||
buf *bytes.Buffer
|
|
||||||
logger PrefixedLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrefixDecorator(t *testing.T) {
|
|
||||||
suite.Run(t, new(PrefixDecoratorTest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewWithPrefix(t *testing.T) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
logger := NewWithPrefix("code", "Prefix:", logging.DEBUG, DefaultLogFormatter())
|
|
||||||
logger.(*PrefixDecorator).backend.(*StandardLogger).
|
|
||||||
SetBaseLogger(NewBase(buf, "code", logging.DEBUG, DefaultLogFormatter()))
|
|
||||||
logger.Debugf("message %s", "text")
|
|
||||||
|
|
||||||
assert.Contains(t, buf.String(), "Prefix: message text")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) SetupSuite() {
|
|
||||||
t.buf = &bytes.Buffer{}
|
|
||||||
t.logger = DecorateWithPrefix((&StandardLogger{}).
|
|
||||||
SetBaseLogger(NewBase(t.buf, "code", logging.DEBUG, DefaultLogFormatter())), testPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) SetupTest() {
|
|
||||||
t.buf.Reset()
|
|
||||||
t.logger.SetPrefix(testPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_SetPrefix() {
|
|
||||||
t.logger.Info("message")
|
|
||||||
t.Assert().Equal(testPrefix, t.logger.Prefix())
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
|
|
||||||
t.logger.SetPrefix(testPrefix + testPrefix)
|
|
||||||
t.logger.Info("message")
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Panic() {
|
|
||||||
t.Require().Panics(func() {
|
|
||||||
t.logger.Panic("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Panicf() {
|
|
||||||
t.Require().Panics(func() {
|
|
||||||
t.logger.Panicf("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Critical() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Critical("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Criticalf() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Criticalf("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "CRIT")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Error() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Error("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "ERRO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Errorf() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Errorf("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "ERRO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Warning() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Warning("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "WARN")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Warningf() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Warningf("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "WARN")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Notice() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Notice("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "NOTI")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Noticef() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Noticef("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "NOTI")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Info() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Info("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Infof() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Infof("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "INFO")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Debug() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Debug("message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "DEBU")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PrefixDecoratorTest) Test_Debugf() {
|
|
||||||
t.Require().NotPanics(func() {
|
|
||||||
t.logger.Debugf("%s", "message")
|
|
||||||
})
|
|
||||||
t.Assert().Contains(t.buf.String(), "DEBU")
|
|
||||||
t.Assert().Contains(t.buf.String(), testPrefix+" message")
|
|
||||||
}
|
|
22
core/logger/writer_adapter.go
Normal file
22
core/logger/writer_adapter.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writerAdapter struct {
|
||||||
|
log Logger
|
||||||
|
level zapcore.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriterAdapter returns an io.Writer that can be used to write log messages. Message level is preconfigured.
|
||||||
|
func WriterAdapter(log Logger, level zapcore.Level) io.Writer {
|
||||||
|
return &writerAdapter{log: log, level: level}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writerAdapter) Write(p []byte) (n int, err error) {
|
||||||
|
w.log.Log(w.level, string(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
25
core/logger/writer_adapter_test.go
Normal file
25
core/logger/writer_adapter_test.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriterAdapter(t *testing.T) {
|
||||||
|
log := newBufferLogger()
|
||||||
|
adapter := WriterAdapter(log, zap.InfoLevel)
|
||||||
|
|
||||||
|
msg := []byte("hello world")
|
||||||
|
total, err := adapter.Write(msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, total, len(msg))
|
||||||
|
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, items, 1)
|
||||||
|
assert.Equal(t, "hello world", items[0].Message)
|
||||||
|
assert.Equal(t, "INFO", items[0].LevelName)
|
||||||
|
}
|
34
core/logger/zabbix_collector_adapter.go
Normal file
34
core/logger/zabbix_collector_adapter.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metrics "github.com/retailcrm/zabbix-metrics-collector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type zabbixCollectorAdapter struct {
|
||||||
|
log Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) {
|
||||||
|
baseMsg := "cannot send metrics to Zabbix"
|
||||||
|
switch format {
|
||||||
|
case "cannot stop collector: %s":
|
||||||
|
baseMsg = "cannot stop Zabbix collector"
|
||||||
|
fallthrough
|
||||||
|
case "cannot send metrics to Zabbix: %v":
|
||||||
|
var err interface{}
|
||||||
|
if len(args) > 0 {
|
||||||
|
err = args[0]
|
||||||
|
}
|
||||||
|
a.log.Error(baseMsg, Err(err))
|
||||||
|
default:
|
||||||
|
a.log.Error(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZabbixCollectorAdapter works as a logger adapter for Zabbix metrics collector.
|
||||||
|
// It can extract error messages from Zabbix collector and convert them to structured format.
|
||||||
|
func ZabbixCollectorAdapter(log Logger) metrics.ErrorLogger {
|
||||||
|
return &zabbixCollectorAdapter{log: log}
|
||||||
|
}
|
26
core/logger/zabbix_collector_adapter_test.go
Normal file
26
core/logger/zabbix_collector_adapter_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZabbixCollectorAdapter(t *testing.T) {
|
||||||
|
log := newBufferLogger()
|
||||||
|
adapter := ZabbixCollectorAdapter(log)
|
||||||
|
adapter.Errorf("highly unexpected error: %s", "unexpected error")
|
||||||
|
adapter.Errorf("cannot stop collector: %s", "app error")
|
||||||
|
adapter.Errorf("cannot send metrics to Zabbix: %v", errors.New("send error"))
|
||||||
|
|
||||||
|
items, err := newJSONBufferedLogger(log).ScanAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, items, 3)
|
||||||
|
assert.Equal(t, "highly unexpected error: unexpected error", items[0].Message)
|
||||||
|
assert.Equal(t, "cannot stop Zabbix collector", items[1].Message)
|
||||||
|
assert.Equal(t, "app error", items[1].Context[ErrorAttr])
|
||||||
|
assert.Equal(t, "cannot send metrics to Zabbix", items[2].Message)
|
||||||
|
assert.Equal(t, "send error", items[2].Context[ErrorAttr])
|
||||||
|
}
|
102
core/logger/zap.go
Normal file
102
core/logger/zap.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewZap(format string, debug bool) *zap.Logger {
|
||||||
|
switch format {
|
||||||
|
case "json":
|
||||||
|
return NewZapJSON(debug)
|
||||||
|
case "console":
|
||||||
|
return NewZapConsole(debug)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown logger format: %s", format))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZapConsole(debug bool) *zap.Logger {
|
||||||
|
level := zapcore.InfoLevel
|
||||||
|
if debug {
|
||||||
|
level = zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
log, err := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(level),
|
||||||
|
Development: debug,
|
||||||
|
Encoding: "console",
|
||||||
|
EncoderConfig: EncoderConfigConsole(),
|
||||||
|
OutputPaths: []string{"stdout"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}.Build()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncoderConfigConsole() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
MessageKey: "message",
|
||||||
|
LevelKey: "level",
|
||||||
|
TimeKey: "datetime",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
StacktraceKey: "",
|
||||||
|
LineEnding: "\n",
|
||||||
|
EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
|
||||||
|
encoder.AppendString("level_name=" + level.CapitalString())
|
||||||
|
},
|
||||||
|
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
|
||||||
|
encoder.AppendString("datetime=" + t.Format(time.RFC3339))
|
||||||
|
},
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
|
||||||
|
encoder.AppendString("caller=" + caller.TrimmedPath())
|
||||||
|
},
|
||||||
|
EncodeName: zapcore.FullNameEncoder,
|
||||||
|
ConsoleSeparator: " ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZapJSON(debug bool) *zap.Logger {
|
||||||
|
level := zapcore.InfoLevel
|
||||||
|
if debug {
|
||||||
|
level = zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
log, err := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(level),
|
||||||
|
Development: debug,
|
||||||
|
Encoding: "json-with-context",
|
||||||
|
EncoderConfig: EncoderConfigJSON(),
|
||||||
|
OutputPaths: []string{"stdout"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}.Build()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncoderConfigJSON() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
MessageKey: "message",
|
||||||
|
LevelKey: "level_name",
|
||||||
|
TimeKey: "datetime",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
StacktraceKey: "",
|
||||||
|
LineEnding: "\n",
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
EncodeTime: zapcore.RFC3339TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
EncodeName: zapcore.FullNameEncoder,
|
||||||
|
ConsoleSeparator: " ",
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -57,10 +56,10 @@ var DefaultCSRFTokenGetter = func(c *gin.Context) string {
|
|||||||
} else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 {
|
} else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 {
|
||||||
return t
|
return t
|
||||||
} else if c.Request.Body != nil {
|
} else if c.Request.Body != nil {
|
||||||
data, _ := ioutil.ReadAll(c.Request.Body)
|
data, _ := io.ReadAll(c.Request.Body)
|
||||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
|
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||||
t := r.FormValue("csrf_token")
|
t := r.FormValue("csrf_token")
|
||||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
|
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||||
|
|
||||||
if len(t) > 0 {
|
if len(t) > 0 {
|
||||||
return t
|
return t
|
||||||
@ -91,17 +90,20 @@ type CSRF struct {
|
|||||||
// csrfTokenGetter will be used to obtain token.
|
// csrfTokenGetter will be used to obtain token.
|
||||||
//
|
//
|
||||||
// Usage (with random salt):
|
// Usage (with random salt):
|
||||||
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) {
|
//
|
||||||
// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"})
|
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) {
|
||||||
// }, core.DefaultCSRFTokenGetter)
|
// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"})
|
||||||
|
// }, core.DefaultCSRFTokenGetter)
|
||||||
//
|
//
|
||||||
// Note for csrfTokenGetter: if you want to read token from request body (for example, from form field)
|
// Note for csrfTokenGetter: if you want to read token from request body (for example, from form field)
|
||||||
// - don't forget to restore Body data!
|
// - don't forget to restore Body data!
|
||||||
//
|
//
|
||||||
// Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that:
|
// Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that:
|
||||||
// if t := r.FormValue("csrf_token"); len(t) > 0 {
|
//
|
||||||
// return t
|
// if t := r.FormValue("csrf_token"); len(t) > 0 {
|
||||||
// }
|
// return t
|
||||||
|
// }
|
||||||
|
//
|
||||||
// will close body - and all next middlewares won't be able to read body at all!
|
// will close body - and all next middlewares won't be able to read body at all!
|
||||||
//
|
//
|
||||||
// Use DefaultCSRFTokenGetter as example to implement your own token getter.
|
// Use DefaultCSRFTokenGetter as example to implement your own token getter.
|
||||||
@ -185,11 +187,11 @@ func (x *CSRF) generateSalt() string {
|
|||||||
|
|
||||||
// pseudoRandomString generates pseudo-random string with specified length.
|
// pseudoRandomString generates pseudo-random string with specified length.
|
||||||
func (x *CSRF) pseudoRandomString(length int) string {
|
func (x *CSRF) pseudoRandomString(length int) string {
|
||||||
rand.Seed(time.Now().UnixNano())
|
r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint:gosec
|
||||||
data := make([]byte, length)
|
data := make([]byte, length)
|
||||||
|
|
||||||
for i := 0; i < length; i++ { // it is supposed to use pseudo-random data.
|
for i := 0; i < length; i++ { // it is supposed to use pseudo-random data.
|
||||||
data[i] = byte(65 + rand.Intn(90-65)) // nolint:gosec,gomnd
|
data[i] = byte(65 + r.Intn(90-65)) // nolint:gosec,gomnd
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(data)
|
return string(data)
|
||||||
@ -209,15 +211,16 @@ func (x *CSRF) CSRFFromContext(c *gin.Context) string {
|
|||||||
|
|
||||||
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
|
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
|
||||||
// Usage:
|
// Usage:
|
||||||
// engine := gin.New()
|
//
|
||||||
// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer)
|
// engine := gin.New()
|
||||||
// engine.Use(csrf.GenerateCSRFMiddleware())
|
// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer)
|
||||||
|
// engine.Use(csrf.GenerateCSRFMiddleware())
|
||||||
func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
|
func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
session, _ := x.store.Get(c.Request, x.sessionName)
|
session, _ := x.store.Get(c.Request, x.sessionName)
|
||||||
|
|
||||||
if i, ok := session.Values["csrf_token"]; ok {
|
if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif
|
||||||
if i, ok := i.(string); !ok || i == "" {
|
if i, ok := i.(string); !ok || i == "" { // nolint:nestif
|
||||||
if x.fillToken(session, c) != nil {
|
if x.fillToken(session, c) != nil {
|
||||||
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
|
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
@ -243,8 +246,9 @@ func (x *CSRF) fillToken(s *sessions.Session, c *gin.Context) error {
|
|||||||
|
|
||||||
// VerifyCSRFMiddleware verifies CSRF token
|
// VerifyCSRFMiddleware verifies CSRF token
|
||||||
// Usage:
|
// Usage:
|
||||||
// engine := gin.New()
|
//
|
||||||
// engine.Use(csrf.VerifyCSRFMiddleware())
|
// engine := gin.New()
|
||||||
|
// engine.Use(csrf.VerifyCSRFMiddleware())
|
||||||
func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
|
func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if x.strInSlice(ignoredMethods, c.Request.Method) {
|
if x.strInSlice(ignoredMethods, c.Request.Method) {
|
||||||
@ -254,9 +258,9 @@ func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
|
|||||||
var token string
|
var token string
|
||||||
session, _ := x.store.Get(c.Request, x.sessionName)
|
session, _ := x.store.Get(c.Request, x.sessionName)
|
||||||
|
|
||||||
if i, ok := session.Values["csrf_token"]; ok {
|
if i, ok := session.Values["csrf_token"]; ok { // nolint:nestif
|
||||||
var v string
|
var v string
|
||||||
if v, ok = i.(string); !ok || v == "" {
|
if v, ok = i.(string); !ok || v == "" { // nolint:nestif
|
||||||
if !ok {
|
if !ok {
|
||||||
x.abortFunc(c, CSRFErrorIncorrectTokenType)
|
x.abortFunc(c, CSRFErrorIncorrectTokenType)
|
||||||
} else if v == "" {
|
} else if v == "" {
|
||||||
|
@ -3,7 +3,6 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -32,7 +31,7 @@ func TestCSRF_DefaultCSRFTokenGetter_Empty(t *testing.T) {
|
|||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
RawQuery: "",
|
RawQuery: "",
|
||||||
},
|
},
|
||||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
assert.Empty(t, DefaultCSRFTokenGetter(c))
|
assert.Empty(t, DefaultCSRFTokenGetter(c))
|
||||||
@ -85,14 +84,14 @@ func TestCSRF_DefaultCSRFTokenGetter_Form(t *testing.T) {
|
|||||||
RawQuery: "",
|
RawQuery: "",
|
||||||
},
|
},
|
||||||
Header: headers,
|
Header: headers,
|
||||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
Body: io.NopCloser(bytes.NewReader([]byte(""))),
|
||||||
}}
|
}}
|
||||||
c.Request.PostForm = url.Values{"csrf_token": {"token"}}
|
c.Request.PostForm = url.Values{"csrf_token": {"token"}}
|
||||||
|
|
||||||
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
|
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
|
||||||
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
|
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
|
||||||
|
|
||||||
_, err := ioutil.ReadAll(c.Request.Body)
|
_, err := io.ReadAll(c.Request.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
|
"go.uber.org/zap"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
@ -64,14 +65,13 @@ func NewModuleFeaturesUploader(
|
|||||||
awsConfig.WithCredentialsProvider(customProvider),
|
awsConfig.WithCredentialsProvider(customProvider),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Error("cannot load S3 configuration", logger.Err(err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client := manager.NewUploader(s3.NewFromConfig(cfg))
|
client := manager.NewUploader(s3.NewFromConfig(cfg))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Error("cannot load S3 configuration", logger.Err(err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,18 +87,18 @@ func NewModuleFeaturesUploader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ModuleFeaturesUploader) Upload() {
|
func (s *ModuleFeaturesUploader) Upload() {
|
||||||
s.log.Debugf("upload module features started...")
|
s.log.Debug("upload module features started...")
|
||||||
|
|
||||||
content, err := os.ReadFile(s.featuresFilename)
|
content, err := os.ReadFile(s.featuresFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Errorf("cannot read markdown file %s %s", s.featuresFilename, err.Error())
|
s.log.Error("cannot read markdown file %s %s", zap.String("fileName", s.featuresFilename), logger.Err(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, lang := range languages {
|
for _, lang := range languages {
|
||||||
translated, err := s.translate(content, lang)
|
translated, err := s.translate(content, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Errorf("cannot translate module features file to %s: %s", lang.String(), err.Error())
|
s.log.Error("cannot translate module features file", zap.String("lang", lang.String()), logger.Err(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() {
|
|||||||
resp, err := s.uploadFile(html, lang.String())
|
resp, err := s.uploadFile(html, lang.String())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Errorf("cannot upload file %s: %s", lang.String(), err.Error())
|
s.log.Error("cannot upload file", zap.String("lang", lang.String()), logger.Err(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ func (s *ModuleFeaturesUploader) Upload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
s.log.Debugf("upload module features finished")
|
s.log.Debug("upload module features finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ModuleFeaturesUploader) translate(content []byte, lang language.Tag) ([]byte, error) {
|
func (s *ModuleFeaturesUploader) translate(content []byte, lang language.Tag) ([]byte, error) {
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||||
|
"github.com/retailcrm/mg-transport-core/v2/core/util/testutil"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModuleFeaturesUploaderTest struct {
|
type ModuleFeaturesUploaderTest struct {
|
||||||
@ -36,8 +35,7 @@ func (t *ModuleFeaturesUploaderTest) TearDownSuite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() {
|
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeaturesUploader() {
|
||||||
logs := &bytes.Buffer{}
|
log := testutil.NewBufferedLogger()
|
||||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
|
||||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||||
|
|
||||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
||||||
@ -50,8 +48,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_NewModuleFeature
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
|
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
|
||||||
logs := &bytes.Buffer{}
|
log := testutil.NewBufferedLogger()
|
||||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
|
||||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "filename.txt")
|
||||||
content := "test content " + t.localizer.GetLocalizedMessage("message")
|
content := "test content " + t.localizer.GetLocalizedMessage("message")
|
||||||
@ -62,8 +59,7 @@ func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_translate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() {
|
func (t *ModuleFeaturesUploaderTest) TestModuleFeaturesUploader_uploadFile() {
|
||||||
logs := &bytes.Buffer{}
|
log := testutil.NewBufferedLogger()
|
||||||
log := logger.NewBase(logs, "code", logging.DEBUG, logger.DefaultLogFormatter())
|
|
||||||
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
conf := config.AWS{Bucket: "bucketName", FolderName: "folder/name"}
|
||||||
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md")
|
uploader := NewModuleFeaturesUploader(log, conf, t.localizer, "source.md")
|
||||||
content := "test content"
|
content := "test content"
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
sentrygin "github.com/getsentry/sentry-go/gin"
|
sentrygin "github.com/getsentry/sentry-go/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/stacktrace"
|
"github.com/retailcrm/mg-transport-core/v2/core/stacktrace"
|
||||||
@ -22,8 +23,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// reset is borrowed directly from the gin.
|
const recoveryMiddlewareSkipFrames = 3
|
||||||
const reset = "\033[0m"
|
|
||||||
|
|
||||||
// ErrorHandlerFunc will handle errors.
|
// ErrorHandlerFunc will handle errors.
|
||||||
type ErrorHandlerFunc func(recovery interface{}, c *gin.Context)
|
type ErrorHandlerFunc func(recovery interface{}, c *gin.Context)
|
||||||
@ -142,17 +142,17 @@ func (s *Sentry) SentryMiddlewares() []gin.HandlerFunc {
|
|||||||
|
|
||||||
// obtainErrorLogger extracts logger from the context or builds it right here from tags used in Sentry events
|
// obtainErrorLogger extracts logger from the context or builds it right here from tags used in Sentry events
|
||||||
// Those tags can be configured with SentryLoggerConfig field.
|
// Those tags can be configured with SentryLoggerConfig field.
|
||||||
func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger {
|
func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.Logger {
|
||||||
if item, ok := c.Get("logger"); ok {
|
if item, ok := c.Get("logger"); ok {
|
||||||
if accountLogger, ok := item.(logger.AccountLogger); ok {
|
if ctxLogger, ok := item.(logger.Logger); ok {
|
||||||
return accountLogger
|
return ctxLogger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionID := "{no connection ID}"
|
connectionID := "{no connection ID}"
|
||||||
accountID := "{no account ID}"
|
accountID := "{no account ID}"
|
||||||
if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" {
|
if s.SentryLoggerConfig.TagForConnection == "" && s.SentryLoggerConfig.TagForAccount == "" {
|
||||||
return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID)
|
return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag := range s.tagsFromContext(c) {
|
for tag := range s.tagsFromContext(c) {
|
||||||
@ -164,7 +164,7 @@ func (s *Sentry) obtainErrorLogger(c *gin.Context) logger.AccountLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return logger.DecorateForAccount(s.Logger, "Sentry", connectionID, accountID)
|
return s.Logger.ForHandler("Sentry").ForConnection(connectionID).ForAccount(accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tagsSetterMiddleware sets event tags into Sentry events.
|
// tagsSetterMiddleware sets event tags into Sentry events.
|
||||||
@ -201,13 +201,13 @@ func (s *Sentry) exceptionCaptureMiddleware() gin.HandlerFunc { // nolint:gocogn
|
|||||||
for _, err := range publicErrors {
|
for _, err := range publicErrors {
|
||||||
messages[index] = err.Error()
|
messages[index] = err.Error()
|
||||||
s.CaptureException(c, err)
|
s.CaptureException(c, err)
|
||||||
l.Error(err)
|
l.Error(err.Error())
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, err := range privateErrors {
|
for _, err := range privateErrors {
|
||||||
s.CaptureException(c, err)
|
s.CaptureException(c, err)
|
||||||
l.Error(err)
|
l.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if privateLen > 0 || recovery != nil {
|
if privateLen > 0 || recovery != nil {
|
||||||
@ -250,8 +250,9 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l != nil {
|
if l != nil {
|
||||||
stack := stacktrace.FormattedStack(3, l.Prefix()+" ")
|
// TODO: Check if we can output stacktraces with prefix data like before if we really need it.
|
||||||
formattedErr := fmt.Sprintf("%s %s", l.Prefix(), err)
|
stack := stacktrace.FormattedStack(recoveryMiddlewareSkipFrames, "trace: ")
|
||||||
|
formattedErr := logger.Err(err)
|
||||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||||
headers := strings.Split(string(httpRequest), "\r\n")
|
headers := strings.Split(string(httpRequest), "\r\n")
|
||||||
for idx, header := range headers {
|
for idx, header := range headers {
|
||||||
@ -259,18 +260,17 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
|
|||||||
if current[0] == "Authorization" {
|
if current[0] == "Authorization" {
|
||||||
headers[idx] = current[0] + ": *"
|
headers[idx] = current[0] + ": *"
|
||||||
}
|
}
|
||||||
headers[idx] = l.Prefix() + " " + headers[idx]
|
headers[idx] = "header: " + headers[idx]
|
||||||
}
|
}
|
||||||
headersToStr := strings.Join(headers, "\r\n")
|
headersToStr := zap.String("headers", strings.Join(headers, "\r\n"))
|
||||||
|
formattedStack := zap.String("stacktrace", string(stack))
|
||||||
switch {
|
switch {
|
||||||
case brokenPipe:
|
case brokenPipe:
|
||||||
l.Errorf("%s\n%s%s", formattedErr, headersToStr, reset)
|
l.Error("error", formattedErr, headersToStr)
|
||||||
case gin.IsDebugging():
|
case gin.IsDebugging():
|
||||||
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
l.Error("[Recovery] panic recovered", headersToStr, formattedErr, formattedStack)
|
||||||
timeFormat(time.Now()), headersToStr, formattedErr, stack, reset)
|
|
||||||
default:
|
default:
|
||||||
l.Errorf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
l.Error("[Recovery] panic recovered", formattedErr, formattedStack)
|
||||||
timeFormat(time.Now()), formattedErr, stack, reset)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if brokenPipe {
|
if brokenPipe {
|
||||||
|
@ -2,7 +2,6 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sync"
|
"sync"
|
||||||
@ -32,12 +31,12 @@ type sentryMockTransport struct {
|
|||||||
sending sync.RWMutex
|
sending sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sentryMockTransport) Flush(timeout time.Duration) bool {
|
func (s *sentryMockTransport) Flush(_ time.Duration) bool {
|
||||||
// noop
|
// noop
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sentryMockTransport) Configure(options sentry.ClientOptions) {
|
func (s *sentryMockTransport) Configure(_ sentry.ClientOptions) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +277,7 @@ func (s *SentryTest) TestSentry_CaptureException() {
|
|||||||
|
|
||||||
func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() {
|
func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() {
|
||||||
ctx, _ := s.ginCtxMock()
|
ctx, _ := s.ginCtxMock()
|
||||||
log := logger.DecorateForAccount(testutil.NewBufferedLogger(), "component", "conn", "acc")
|
log := testutil.NewBufferedLogger().ForHandler("component").ForConnection("conn").ForAccount("acc")
|
||||||
ctx.Set("logger", log)
|
ctx.Set("logger", log)
|
||||||
|
|
||||||
s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx))
|
s.Assert().Equal(log, s.sentry.obtainErrorLogger(ctx))
|
||||||
@ -299,12 +298,8 @@ func (s *SentryTest) TestSentry_obtainErrorLogger_Constructed() {
|
|||||||
|
|
||||||
s.Assert().NotNil(log)
|
s.Assert().NotNil(log)
|
||||||
s.Assert().NotNil(logNoConfig)
|
s.Assert().NotNil(logNoConfig)
|
||||||
s.Assert().Implements((*logger.AccountLogger)(nil), log)
|
s.Assert().Implements((*logger.Logger)(nil), log)
|
||||||
s.Assert().Implements((*logger.AccountLogger)(nil), logNoConfig)
|
s.Assert().Implements((*logger.Logger)(nil), logNoConfig)
|
||||||
s.Assert().Equal(
|
|
||||||
fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "{no connection ID}", "{no account ID}"),
|
|
||||||
logNoConfig.Prefix())
|
|
||||||
s.Assert().Equal(fmt.Sprintf(logger.DefaultAccountLoggerFormat, "Sentry", "conn_url", "acc_name"), log.Prefix())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SentryTest) TestSentry_MiddlewaresError() {
|
func (s *SentryTest) TestSentry_MiddlewaresError() {
|
||||||
|
@ -18,7 +18,7 @@ func TestError(t *testing.T) {
|
|||||||
|
|
||||||
func (t *ErrorTest) TestAppendToError() {
|
func (t *ErrorTest) TestAppendToError() {
|
||||||
err := errors.New("test error")
|
err := errors.New("test error")
|
||||||
_, ok := err.(StackTraced)
|
_, ok := err.(StackTraced) // nolint:errorlint
|
||||||
|
|
||||||
t.Assert().False(ok)
|
t.Assert().False(ok)
|
||||||
|
|
||||||
@ -28,16 +28,16 @@ func (t *ErrorTest) TestAppendToError() {
|
|||||||
t.Assert().Nil(AppendToError(nil))
|
t.Assert().Nil(AppendToError(nil))
|
||||||
t.Assert().Implements((*StackTraced)(nil), withTrace)
|
t.Assert().Implements((*StackTraced)(nil), withTrace)
|
||||||
t.Assert().Implements((*StackTraced)(nil), twiceTrace)
|
t.Assert().Implements((*StackTraced)(nil), twiceTrace)
|
||||||
t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace())
|
t.Assert().Equal(withTrace.(StackTraced).StackTrace(), twiceTrace.(StackTraced).StackTrace()) // nolint:errorlint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ErrorTest) TestCauseUnwrap() {
|
func (t *ErrorTest) TestCauseUnwrap() {
|
||||||
err := errors.New("test error")
|
err := errors.New("test error")
|
||||||
wrapped := AppendToError(err)
|
wrapped := AppendToError(err)
|
||||||
|
|
||||||
t.Assert().Equal(err, wrapped.(*withStack).Cause())
|
t.Assert().Equal(err, wrapped.(*withStack).Cause()) // nolint:errorlint
|
||||||
t.Assert().Equal(err, errors.Unwrap(wrapped))
|
t.Assert().Equal(err, errors.Unwrap(wrapped))
|
||||||
t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped))
|
t.Assert().Equal(wrapped.(*withStack).Cause(), errors.Unwrap(wrapped)) // nolint:errorlint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ErrorTest) TestFormat() {
|
func (t *ErrorTest) TestFormat() {
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -63,34 +63,34 @@ func (f Frame) name() string {
|
|||||||
|
|
||||||
// Format formats the frame according to the fmt.Formatter interface.
|
// Format formats the frame according to the fmt.Formatter interface.
|
||||||
//
|
//
|
||||||
// %s source file
|
// %s source file
|
||||||
// %d source line
|
// %d source line
|
||||||
// %n function name
|
// %n function name
|
||||||
// %v equivalent to %s:%d
|
// %v equivalent to %s:%d
|
||||||
//
|
//
|
||||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
//
|
//
|
||||||
// %+s function name and path of source file relative to the compile time
|
// %+s function name and path of source file relative to the compile time
|
||||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||||
// %+v equivalent to %+s:%d
|
// %+v equivalent to %+s:%d
|
||||||
func (f Frame) Format(s fmt.State, verb rune) {
|
func (f Frame) Format(s fmt.State, verb rune) {
|
||||||
switch verb {
|
switch verb {
|
||||||
case 's':
|
case 's':
|
||||||
switch {
|
switch {
|
||||||
case s.Flag('+'):
|
case s.Flag('+'):
|
||||||
io.WriteString(s, f.name())
|
_, _ = io.WriteString(s, f.name())
|
||||||
io.WriteString(s, "\n\t")
|
_, _ = io.WriteString(s, "\n\t")
|
||||||
io.WriteString(s, f.file())
|
_, _ = io.WriteString(s, f.file())
|
||||||
default:
|
default:
|
||||||
io.WriteString(s, path.Base(f.file()))
|
_, _ = io.WriteString(s, path.Base(f.file()))
|
||||||
}
|
}
|
||||||
case 'd':
|
case 'd':
|
||||||
io.WriteString(s, strconv.Itoa(f.line()))
|
_, _ = io.WriteString(s, strconv.Itoa(f.line()))
|
||||||
case 'n':
|
case 'n':
|
||||||
io.WriteString(s, funcname(f.name()))
|
_, _ = io.WriteString(s, funcname(f.name()))
|
||||||
case 'v':
|
case 'v':
|
||||||
f.Format(s, 's')
|
f.Format(s, 's')
|
||||||
io.WriteString(s, ":")
|
_, _ = io.WriteString(s, ":")
|
||||||
f.Format(s, 'd')
|
f.Format(s, 'd')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,23 +110,23 @@ type StackTrace []Frame
|
|||||||
|
|
||||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||||
//
|
//
|
||||||
// %s lists source files for each Frame in the stack
|
// %s lists source files for each Frame in the stack
|
||||||
// %v lists the source file and line number for each Frame in the stack
|
// %v lists the source file and line number for each Frame in the stack
|
||||||
//
|
//
|
||||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
//
|
//
|
||||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||||
switch verb {
|
switch verb {
|
||||||
case 'v':
|
case 'v':
|
||||||
switch {
|
switch {
|
||||||
case s.Flag('+'):
|
case s.Flag('+'):
|
||||||
for _, f := range st {
|
for _, f := range st {
|
||||||
io.WriteString(s, "\n")
|
_, _ = io.WriteString(s, "\n")
|
||||||
f.Format(s, verb)
|
f.Format(s, verb)
|
||||||
}
|
}
|
||||||
case s.Flag('#'):
|
case s.Flag('#'):
|
||||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
_, _ = fmt.Fprintf(s, "%#v", []Frame(st))
|
||||||
default:
|
default:
|
||||||
st.formatSlice(s, verb)
|
st.formatSlice(s, verb)
|
||||||
}
|
}
|
||||||
@ -138,14 +138,14 @@ func (st StackTrace) Format(s fmt.State, verb rune) {
|
|||||||
// formatSlice will format this StackTrace into the given buffer as a slice of
|
// formatSlice will format this StackTrace into the given buffer as a slice of
|
||||||
// Frame, only valid when called with '%s' or '%v'.
|
// Frame, only valid when called with '%s' or '%v'.
|
||||||
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
|
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
|
||||||
io.WriteString(s, "[")
|
_, _ = io.WriteString(s, "[")
|
||||||
for i, f := range st {
|
for i, f := range st {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
io.WriteString(s, " ")
|
_, _ = io.WriteString(s, " ")
|
||||||
}
|
}
|
||||||
f.Format(s, verb)
|
f.Format(s, verb)
|
||||||
}
|
}
|
||||||
io.WriteString(s, "]")
|
_, _ = io.WriteString(s, "]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// stack represents a stack of program counters.
|
// stack represents a stack of program counters.
|
||||||
@ -159,7 +159,7 @@ func (s *stack) Format(st fmt.State, verb rune) {
|
|||||||
if verb == 'v' && st.Flag('+') {
|
if verb == 'v' && st.Flag('+') {
|
||||||
for _, pc := range *s {
|
for _, pc := range *s {
|
||||||
f := Frame(pc)
|
f := Frame(pc)
|
||||||
fmt.Fprintf(st, "\n%+v", f)
|
_, _ = fmt.Fprintf(st, "\n%+v", f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,16 +185,16 @@ func FormattedStack(skip int, prefix string) []byte {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Print this much at least. If we can't find the source, it won't show.
|
// Print this much at least. If we can't find the source, it won't show.
|
||||||
fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc)
|
_, _ = fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc)
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
lines = bytes.Split(data, []byte{'\n'})
|
||||||
lastFile = file
|
lastFile = file
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line))
|
_, _ = fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line))
|
||||||
}
|
}
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package core
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@ -37,8 +36,8 @@ func (t *TemplateTest) initTestData() {
|
|||||||
require.Nil(t.T(), err)
|
require.Nil(t.T(), err)
|
||||||
data1 := []byte(`data {{template "body" .}}`)
|
data1 := []byte(`data {{template "body" .}}`)
|
||||||
data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`)
|
data2 := []byte(`{{define "body"}}test {{"test" | trans}}{{end}}`)
|
||||||
err1 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm)
|
err1 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 1), data1, os.ModePerm)
|
||||||
err2 := ioutil.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm)
|
err2 := os.WriteFile(fmt.Sprintf(testTemplatesFile, 2), data2, os.ModePerm)
|
||||||
require.Nil(t.T(), err1)
|
require.Nil(t.T(), err1)
|
||||||
require.Nil(t.T(), err2)
|
require.Nil(t.T(), err2)
|
||||||
}
|
}
|
||||||
|
@ -18,36 +18,41 @@ import (
|
|||||||
// because AsError() returns nil if there are no errors in the list.
|
// because AsError() returns nil if there are no errors in the list.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
// err := errorutil.NewCollector().
|
//
|
||||||
// Do(errors.New("error 1")).
|
// err := errorutil.NewCollector().
|
||||||
// Do(errors.New("error 2"), errors.New("error 3"))
|
// Do(errors.New("error 1")).
|
||||||
// // Will print error message.
|
// Do(errors.New("error 2"), errors.New("error 3"))
|
||||||
// fmt.Println(err)
|
// // Will print error message.
|
||||||
|
// fmt.Println(err)
|
||||||
//
|
//
|
||||||
// This code will produce something like this:
|
// This code will produce something like this:
|
||||||
// #1 err at /home/user/main.go:62: error 1
|
//
|
||||||
// #2 err at /home/user/main.go:63: error 2
|
// #1 err at /home/user/main.go:62: error 1
|
||||||
// #3 err at /home/user/main.go:64: error 3
|
// #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:
|
// You can also iterate over the error to use their data instead of using predefined message:
|
||||||
// err := errorutil.NewCollector().
|
|
||||||
// Do(errors.New("error 1")).
|
|
||||||
// Do(errors.New("error 2"), errors.New("error 3"))
|
|
||||||
//
|
//
|
||||||
// for err := range c.Iterate() {
|
// err := errorutil.NewCollector().
|
||||||
// fmt.Printf("Error at %s:%d: %v\n", err.File, err.Line, err)
|
// Do(errors.New("error 1")).
|
||||||
// }
|
// Do(errors.New("error 2"), errors.New("error 3"))
|
||||||
|
//
|
||||||
|
// for err := range c.Iterate() {
|
||||||
|
// fmt.Printf("Error at %s:%d: %v\n", err.File, err.Line, err)
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// This code will produce output that looks like this:
|
// 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 0
|
||||||
// Error at /home/user/main.go:164: error 2
|
// 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).
|
// Example with GORM migration (Collector is returned as an error here).
|
||||||
// return errorutil.NewCollector().Do(
|
//
|
||||||
// db.CreateTable(models.Account{}, models.Connection{}).Error,
|
// return errorutil.NewCollector().Do(
|
||||||
// db.Table("account").AddUniqueIndex("account_key", "channel").Error,
|
// db.CreateTable(models.Account{}, models.Connection{}).Error,
|
||||||
// ).AsError()
|
// db.Table("account").AddUniqueIndex("account_key", "channel").Error,
|
||||||
|
// ).AsError()
|
||||||
type Collector struct {
|
type Collector struct {
|
||||||
errors *errList
|
errors *errList
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ type ListResponse struct {
|
|||||||
|
|
||||||
// GetErrorResponse returns ErrorResponse with specified status code
|
// GetErrorResponse returns ErrorResponse with specified status code
|
||||||
// Usage (with gin):
|
// Usage (with gin):
|
||||||
// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money"))
|
//
|
||||||
|
// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money"))
|
||||||
func GetErrorResponse(statusCode int, err string) (int, interface{}) {
|
func GetErrorResponse(statusCode int, err string) (int, interface{}) {
|
||||||
return statusCode, Response{
|
return statusCode, Response{
|
||||||
Error: err,
|
Error: err,
|
||||||
@ -23,28 +24,32 @@ func GetErrorResponse(statusCode int, err string) (int, interface{}) {
|
|||||||
|
|
||||||
// BadRequest returns ErrorResponse with code 400
|
// BadRequest returns ErrorResponse with code 400
|
||||||
// Usage (with gin):
|
// Usage (with gin):
|
||||||
// context.JSON(BadRequest("invalid data"))
|
//
|
||||||
|
// context.JSON(BadRequest("invalid data"))
|
||||||
func BadRequest(err string) (int, interface{}) {
|
func BadRequest(err string) (int, interface{}) {
|
||||||
return GetErrorResponse(http.StatusBadRequest, err)
|
return GetErrorResponse(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unauthorized returns ErrorResponse with code 401
|
// Unauthorized returns ErrorResponse with code 401
|
||||||
// Usage (with gin):
|
// Usage (with gin):
|
||||||
// context.JSON(Unauthorized("invalid credentials"))
|
//
|
||||||
|
// context.JSON(Unauthorized("invalid credentials"))
|
||||||
func Unauthorized(err string) (int, interface{}) {
|
func Unauthorized(err string) (int, interface{}) {
|
||||||
return GetErrorResponse(http.StatusUnauthorized, err)
|
return GetErrorResponse(http.StatusUnauthorized, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forbidden returns ErrorResponse with code 403
|
// Forbidden returns ErrorResponse with code 403
|
||||||
// Usage (with gin):
|
// Usage (with gin):
|
||||||
// context.JSON(Forbidden("forbidden"))
|
//
|
||||||
|
// context.JSON(Forbidden("forbidden"))
|
||||||
func Forbidden(err string) (int, interface{}) {
|
func Forbidden(err string) (int, interface{}) {
|
||||||
return GetErrorResponse(http.StatusForbidden, err)
|
return GetErrorResponse(http.StatusForbidden, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InternalServerError returns ErrorResponse with code 500
|
// InternalServerError returns ErrorResponse with code 500
|
||||||
// Usage (with gin):
|
// Usage (with gin):
|
||||||
// context.JSON(BadRequest("invalid data"))
|
//
|
||||||
|
// context.JSON(BadRequest("invalid data"))
|
||||||
func InternalServerError(err string) (int, interface{}) {
|
func InternalServerError(err string) (int, interface{}) {
|
||||||
return GetErrorResponse(http.StatusInternalServerError, err)
|
return GetErrorResponse(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -230,12 +230,12 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
|||||||
return errors.New("dialer must be built first")
|
return errors.New("dialer must be built first")
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 {
|
if b.mockHost != "" && b.mockPort != "" && len(b.mockedDomains) > 0 { // nolint:nestif
|
||||||
b.logf("Mock address is \"%s\"\n", net.JoinHostPort(b.mockHost, b.mockPort))
|
b.log("Mock address has been set", zap.String("address", net.JoinHostPort(b.mockHost, b.mockPort)))
|
||||||
b.logf("Mocked domains: ")
|
b.log("Mocked domains: ")
|
||||||
|
|
||||||
for _, domain := range b.mockedDomains {
|
for _, domain := range b.mockedDomains {
|
||||||
b.logf(" - %s\n", domain)
|
b.log(fmt.Sprintf(" - %s\n", domain))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.httpTransport.Proxy = nil
|
b.httpTransport.Proxy = nil
|
||||||
@ -259,7 +259,7 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
|||||||
addr = net.JoinHostPort(b.mockHost, b.mockPort)
|
addr = net.JoinHostPort(b.mockHost, b.mockPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.logf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr)
|
b.log(fmt.Sprintf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,13 +270,13 @@ func (b *HTTPClientBuilder) buildMocks() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// logf prints logs via Engine or via fmt.Printf.
|
// log prints logs via Engine or via fmt.Println.
|
||||||
func (b *HTTPClientBuilder) logf(format string, args ...interface{}) {
|
func (b *HTTPClientBuilder) log(msg string, args ...interface{}) {
|
||||||
if b.logging {
|
if b.logging {
|
||||||
if b.logger != nil {
|
if b.logger != nil {
|
||||||
b.logger.Infof(format, args...)
|
b.logger.Info(msg, logger.AnyZapFields(args)...)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(format, args...)
|
fmt.Println(append([]any{msg}, args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -16,7 +15,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -137,14 +135,14 @@ func (t *HTTPClientBuilderTest) Test_buildMocks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *HTTPClientBuilderTest) Test_WithLogger() {
|
func (t *HTTPClientBuilderTest) Test_WithLogger() {
|
||||||
logger := logger.NewStandard("telegram", logging.ERROR, logger.DefaultLogFormatter())
|
|
||||||
builder := NewHTTPClientBuilder()
|
builder := NewHTTPClientBuilder()
|
||||||
require.Nil(t.T(), builder.logger)
|
require.Nil(t.T(), builder.logger)
|
||||||
|
|
||||||
builder.WithLogger(nil)
|
builder.WithLogger(nil)
|
||||||
assert.Nil(t.T(), builder.logger)
|
assert.Nil(t.T(), builder.logger)
|
||||||
|
|
||||||
builder.WithLogger(logger)
|
log := logger.NewDefault("json", true)
|
||||||
|
builder.WithLogger(log)
|
||||||
assert.NotNil(t.T(), builder.logger)
|
assert.NotNil(t.T(), builder.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +151,7 @@ func (t *HTTPClientBuilderTest) Test_logf() {
|
|||||||
assert.Nil(t.T(), recover())
|
assert.Nil(t.T(), recover())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
t.builder.logf("test %s", "string")
|
t.builder.log(fmt.Sprintf("test %s", "string"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HTTPClientBuilderTest) Test_Build() {
|
func (t *HTTPClientBuilderTest) Test_Build() {
|
||||||
@ -210,6 +208,7 @@ x5porosgI2RgOTTwmiYOcYQTS2650jYydHhK16Gu2b3UKernO16mAWXNDWfvS2bk
|
|||||||
nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI
|
nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI
|
||||||
saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB
|
saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
|
// nolint:gosec
|
||||||
keyFileData := `-----BEGIN RSA PRIVATE KEY-----
|
keyFileData := `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC
|
MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC
|
||||||
5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn
|
5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn
|
||||||
@ -238,9 +237,9 @@ weywTxDl/OD5ybNkZIRKsIXciFYG1VCGO2HNGN9qJcV+nJ63kyrIBauwUkuEhiN5
|
|||||||
uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
certFile, err := ioutil.TempFile("/tmp", "cert_")
|
certFile, err := os.CreateTemp("/tmp", "cert_")
|
||||||
require.NoError(t.T(), err, "cannot create temp cert file")
|
require.NoError(t.T(), err, "cannot create temp cert file")
|
||||||
keyFile, err := ioutil.TempFile("/tmp", "key_")
|
keyFile, err := os.CreateTemp("/tmp", "key_")
|
||||||
require.NoError(t.T(), err, "cannot create temp key file")
|
require.NoError(t.T(), err, "cannot create temp key file")
|
||||||
|
|
||||||
_, err = certFile.WriteString(certFileData)
|
_, err = certFile.WriteString(certFileData)
|
||||||
@ -253,7 +252,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
|||||||
errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file")
|
errorutil.Collect(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file")
|
||||||
|
|
||||||
mux := &http.ServeMux{}
|
mux := &http.ServeMux{}
|
||||||
srv := &http.Server{Addr: mockServerAddr, Handler: mux}
|
srv := &http.Server{Addr: mockServerAddr, Handler: mux, ReadHeaderTimeout: defaultDialerTimeout}
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
_, _ = io.WriteString(w, "ok")
|
_, _ = io.WriteString(w, "ok")
|
||||||
@ -308,7 +307,7 @@ uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
|
|||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t.T(), err, "error while reading body")
|
require.NoError(t.T(), err, "error while reading body")
|
||||||
|
|
||||||
assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code")
|
assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code")
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package testutil
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReadBuffer is implemented by the BufferLogger.
|
// ReadBuffer is implemented by the BufferLogger.
|
||||||
@ -28,120 +26,75 @@ type BufferedLogger interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BufferLogger is an implementation of the BufferedLogger.
|
// BufferLogger is an implementation of the BufferedLogger.
|
||||||
|
//
|
||||||
|
// BufferLogger can be used in tests to match specific log messages. It uses JSON by default (hardcoded for now).
|
||||||
|
// It implements fmt.Stringer and provides an adapter to the underlying buffer, which means it can also return
|
||||||
|
// Bytes(), can be used like io.Reader and can be cleaned using Reset() method.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// log := NewBufferedLogger()
|
||||||
|
// // Some other code that works with logger.
|
||||||
|
// fmt.Println(log.String())
|
||||||
type BufferLogger struct {
|
type BufferLogger struct {
|
||||||
buf bytes.Buffer
|
logger.Default
|
||||||
rw sync.RWMutex
|
buf LockableBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBufferedLogger returns new BufferedLogger instance.
|
// NewBufferedLogger returns new BufferedLogger instance.
|
||||||
func NewBufferedLogger() BufferedLogger {
|
func NewBufferedLogger() BufferedLogger {
|
||||||
return &BufferLogger{}
|
bl := &BufferLogger{}
|
||||||
|
bl.Logger = zap.New(
|
||||||
|
zapcore.NewCore(
|
||||||
|
logger.NewJSONWithContextEncoder(
|
||||||
|
logger.EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
|
||||||
|
return bl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BufferLogger) With(fields ...zapcore.Field) logger.Logger {
|
||||||
|
return &BufferLogger{
|
||||||
|
Default: logger.Default{
|
||||||
|
Logger: l.Logger.With(fields...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BufferLogger) WithLazy(fields ...zapcore.Field) logger.Logger {
|
||||||
|
return &BufferLogger{
|
||||||
|
Default: logger.Default{
|
||||||
|
Logger: l.Logger.WithLazy(fields...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BufferLogger) ForHandler(handler any) logger.Logger {
|
||||||
|
return l.WithLazy(zap.Any(logger.HandlerAttr, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BufferLogger) ForConnection(conn any) logger.Logger {
|
||||||
|
return l.WithLazy(zap.Any(logger.ConnectionAttr, conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BufferLogger) ForAccount(acc any) logger.Logger {
|
||||||
|
return l.WithLazy(zap.Any(logger.AccountAttr, acc))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read bytes from the logger buffer. io.Reader implementation.
|
// Read bytes from the logger buffer. io.Reader implementation.
|
||||||
func (l *BufferLogger) Read(p []byte) (n int, err error) {
|
func (l *BufferLogger) Read(p []byte) (n int, err error) {
|
||||||
defer l.rw.RUnlock()
|
|
||||||
l.rw.RLock()
|
|
||||||
return l.buf.Read(p)
|
return l.buf.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String contents of the logger buffer. fmt.Stringer implementation.
|
// String contents of the logger buffer. fmt.Stringer implementation.
|
||||||
func (l *BufferLogger) String() string {
|
func (l *BufferLogger) String() string {
|
||||||
defer l.rw.RUnlock()
|
|
||||||
l.rw.RLock()
|
|
||||||
return l.buf.String()
|
return l.buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
|
// Bytes is a shorthand for the underlying bytes.Buffer method. Returns byte slice with the buffer contents.
|
||||||
func (l *BufferLogger) Bytes() []byte {
|
func (l *BufferLogger) Bytes() []byte {
|
||||||
defer l.rw.RUnlock()
|
|
||||||
l.rw.RLock()
|
|
||||||
return l.buf.Bytes()
|
return l.buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
|
// Reset is a shorthand for the underlying bytes.Buffer method. It will reset buffer contents.
|
||||||
func (l *BufferLogger) Reset() {
|
func (l *BufferLogger) Reset() {
|
||||||
defer l.rw.Unlock()
|
|
||||||
l.rw.Lock()
|
|
||||||
l.buf.Reset()
|
l.buf.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *BufferLogger) write(level logging.Level, args ...interface{}) {
|
|
||||||
defer l.rw.Unlock()
|
|
||||||
l.rw.Lock()
|
|
||||||
l.buf.WriteString(fmt.Sprintln(append([]interface{}{level.String(), "=>"}, args...)...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) writef(level logging.Level, format string, args ...interface{}) {
|
|
||||||
defer l.rw.Unlock()
|
|
||||||
l.rw.Lock()
|
|
||||||
l.buf.WriteString(fmt.Sprintf(level.String()+" => "+format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Fatal(args ...interface{}) {
|
|
||||||
l.write(logging.CRITICAL, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Fatalf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.CRITICAL, format, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Panic(args ...interface{}) {
|
|
||||||
l.write(logging.CRITICAL, args...)
|
|
||||||
panic(fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Panicf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.CRITICAL, format, args...)
|
|
||||||
panic(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Critical(args ...interface{}) {
|
|
||||||
l.write(logging.CRITICAL, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Criticalf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.CRITICAL, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Error(args ...interface{}) {
|
|
||||||
l.write(logging.ERROR, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Errorf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.ERROR, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Warning(args ...interface{}) {
|
|
||||||
l.write(logging.WARNING, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Warningf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.WARNING, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Notice(args ...interface{}) {
|
|
||||||
l.write(logging.NOTICE, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Noticef(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.NOTICE, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Info(args ...interface{}) {
|
|
||||||
l.write(logging.INFO, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Infof(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.INFO, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Debug(args ...interface{}) {
|
|
||||||
l.write(logging.DEBUG, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BufferLogger) Debugf(format string, args ...interface{}) {
|
|
||||||
l.writef(logging.DEBUG, format, args...)
|
|
||||||
}
|
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,17 +29,20 @@ func (t *BufferLoggerTest) Test_Read() {
|
|||||||
|
|
||||||
data, err := io.ReadAll(t.logger)
|
data, err := io.ReadAll(t.logger)
|
||||||
t.Require().NoError(err)
|
t.Require().NoError(err)
|
||||||
t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), data)
|
t.Assert().Contains(string(data), "\"level_name\":\"DEBUG\"")
|
||||||
|
t.Assert().Contains(string(data), "\"message\":\"test\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BufferLoggerTest) Test_Bytes() {
|
func (t *BufferLoggerTest) Test_Bytes() {
|
||||||
t.logger.Debug("test")
|
t.logger.Debug("test")
|
||||||
t.Assert().Equal([]byte(logging.DEBUG.String()+" => test\n"), t.logger.Bytes())
|
t.Assert().Contains(string(t.logger.Bytes()), "\"level_name\":\"DEBUG\"")
|
||||||
|
t.Assert().Contains(string(t.logger.Bytes()), "\"message\":\"test\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BufferLoggerTest) Test_String() {
|
func (t *BufferLoggerTest) Test_String() {
|
||||||
t.logger.Debug("test")
|
t.logger.Debug("test")
|
||||||
t.Assert().Equal(logging.DEBUG.String()+" => test\n", t.logger.String())
|
t.Assert().Contains(t.logger.String(), "\"level_name\":\"DEBUG\"")
|
||||||
|
t.Assert().Contains(t.logger.String(), "\"message\":\"test\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BufferLoggerTest) TestRace() {
|
func (t *BufferLoggerTest) TestRace() {
|
||||||
|
@ -46,7 +46,7 @@ func printRequestData(t UnmatchedRequestsTestingT, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil { // nolint:nestif
|
||||||
t.Log("No body is present.")
|
t.Log("No body is present.")
|
||||||
} else {
|
} else {
|
||||||
data, err := io.ReadAll(r.Body)
|
data, err := io.ReadAll(r.Body)
|
||||||
|
@ -11,11 +11,11 @@ import (
|
|||||||
// remove all the entities created after the hook was set up.
|
// remove all the entities created after the hook was set up.
|
||||||
// You can use it like this:
|
// You can use it like this:
|
||||||
//
|
//
|
||||||
// func TestSomething(t *testing.T){
|
// func TestSomething(t *testing.T){
|
||||||
// db, _ := gorm.Open(...)
|
// db, _ := gorm.Open(...)
|
||||||
// cleaner := DeleteCreatedEntities(db)
|
// cleaner := DeleteCreatedEntities(db)
|
||||||
// defer cleaner()
|
// defer cleaner()
|
||||||
// }.
|
// }.
|
||||||
func DeleteCreatedEntities(db *gorm.DB) func() { // nolint
|
func DeleteCreatedEntities(db *gorm.DB) func() { // nolint
|
||||||
type entity struct {
|
type entity struct {
|
||||||
key interface{}
|
key interface{}
|
||||||
|
52
core/util/testutil/json_record_scanner.go
Normal file
52
core/util/testutil/json_record_scanner.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/guregu/null/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogRecord struct {
|
||||||
|
LevelName string `json:"level_name"`
|
||||||
|
DateTime null.Time `json:"datetime"`
|
||||||
|
Caller string `json:"caller"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Handler string `json:"handler,omitempty"`
|
||||||
|
Connection string `json:"connection,omitempty"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
Context map[string]interface{} `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONRecordScanner struct {
|
||||||
|
r *bufio.Scanner
|
||||||
|
e LogRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONRecordScanner(entryProvider io.Reader) *JSONRecordScanner {
|
||||||
|
return &JSONRecordScanner{r: bufio.NewScanner(entryProvider)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JSONRecordScanner) Scan() error {
|
||||||
|
if s.r.Scan() {
|
||||||
|
return json.Unmarshal(s.r.Bytes(), &s.e)
|
||||||
|
}
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JSONRecordScanner) ScanAll() ([]LogRecord, error) {
|
||||||
|
var entries []LogRecord
|
||||||
|
for s.r.Scan() {
|
||||||
|
entry := LogRecord{}
|
||||||
|
if err := json.Unmarshal(s.r.Bytes(), &entry); err != nil {
|
||||||
|
return entries, err
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JSONRecordScanner) Entry() LogRecord {
|
||||||
|
return s.e
|
||||||
|
}
|
108
core/util/testutil/json_record_scanner_test.go
Normal file
108
core/util/testutil/json_record_scanner_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONRecordScannerTest struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONRecordScanner(t *testing.T) {
|
||||||
|
suite.Run(t, new(JSONRecordScannerTest))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) new(lines []string) *JSONRecordScanner {
|
||||||
|
return NewJSONRecordScanner(bytes.NewReader([]byte(strings.Join(lines, "\n"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) newPredefined() *JSONRecordScanner {
|
||||||
|
return t.new([]string{strings.ReplaceAll(`{
|
||||||
|
"level_name": "ERROR",
|
||||||
|
"datetime": "2024-06-07T13:49:17+03:00",
|
||||||
|
"caller": "handlers/account_middleware.go:147",
|
||||||
|
"message": "Cannot add account",
|
||||||
|
"handler": "handlers.addAccount",
|
||||||
|
"connection": "https://fake-uri.retailcrm.pro",
|
||||||
|
"account": "@username",
|
||||||
|
"context": {
|
||||||
|
"body": "[]string{\"integration_read\", \"integration_write\"}",
|
||||||
|
"statusCode": 500
|
||||||
|
}
|
||||||
|
}`, "\n", "")})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) assertPredefined(record LogRecord) {
|
||||||
|
ts, err := time.Parse(time.RFC3339, "2024-06-07T13:49:17+03:00")
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Assert().True(record.DateTime.Valid)
|
||||||
|
t.Assert().Equal(ts, record.DateTime.Time)
|
||||||
|
t.Assert().Equal("ERROR", record.LevelName)
|
||||||
|
t.Assert().Equal("handlers/account_middleware.go:147", record.Caller)
|
||||||
|
t.Assert().Equal("Cannot add account", record.Message)
|
||||||
|
t.Assert().Equal("handlers.addAccount", record.Handler)
|
||||||
|
t.Assert().Equal("https://fake-uri.retailcrm.pro", record.Connection)
|
||||||
|
t.Assert().Equal("@username", record.Account)
|
||||||
|
t.Assert().Equal("[]string{\"integration_read\", \"integration_write\"}", record.Context["body"])
|
||||||
|
t.Assert().Equal(float64(500), record.Context["statusCode"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScan_NotJSON() {
|
||||||
|
rs := t.new([]string{"this is", "not json"})
|
||||||
|
t.Assert().Error(rs.Scan())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScan_PartialJSON() {
|
||||||
|
rs := t.new([]string{"{}", "not json"})
|
||||||
|
t.Assert().NoError(rs.Scan())
|
||||||
|
t.Assert().Error(rs.Scan())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScan_JSON() {
|
||||||
|
rs := t.new([]string{"{}", "{}"})
|
||||||
|
t.Assert().NoError(rs.Scan())
|
||||||
|
t.Assert().NoError(rs.Scan())
|
||||||
|
t.Assert().ErrorIs(rs.Scan(), io.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScan_JSONRecord() {
|
||||||
|
rs := t.newPredefined()
|
||||||
|
t.Assert().NoError(rs.Scan())
|
||||||
|
t.Assert().ErrorIs(rs.Scan(), io.EOF)
|
||||||
|
t.assertPredefined(rs.Entry())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScanAll_NotJSON() {
|
||||||
|
rs := t.new([]string{"this is", "not json"})
|
||||||
|
records, err := rs.ScanAll()
|
||||||
|
t.Assert().Error(err)
|
||||||
|
t.Assert().Empty(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScanAll_PartialJSON() {
|
||||||
|
rs := t.new([]string{"{}", "not json"})
|
||||||
|
records, err := rs.ScanAll()
|
||||||
|
t.Assert().Error(err)
|
||||||
|
t.Assert().Len(records, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScanAll_JSON() {
|
||||||
|
rs := t.new([]string{"{}", "{}"})
|
||||||
|
records, err := rs.ScanAll()
|
||||||
|
t.Assert().NoError(err)
|
||||||
|
t.Assert().Len(records, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JSONRecordScannerTest) TestScanAll_JSONRecord() {
|
||||||
|
rs := t.newPredefined()
|
||||||
|
records, err := rs.ScanAll()
|
||||||
|
t.Assert().NoError(err)
|
||||||
|
t.Assert().Len(records, 1)
|
||||||
|
t.assertPredefined(records[0])
|
||||||
|
}
|
155
core/util/testutil/lockable_buffer.go
Normal file
155
core/util/testutil/lockable_buffer.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LockableBuffer struct {
|
||||||
|
buf bytes.Buffer
|
||||||
|
rw sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Bytes() []byte {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) AvailableBuffer() []byte {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.AvailableBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) String() string {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Len() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Cap() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Cap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Available() int {
|
||||||
|
defer b.rw.RUnlock()
|
||||||
|
b.rw.RLock()
|
||||||
|
return b.buf.Available()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Truncate(n int) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Truncate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Reset() {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Grow(n int) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
b.buf.Grow(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) WriteString(s string) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadFrom(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteTo(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) WriteByte(c byte) error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteByte(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) WriteRune(r rune) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Read(p []byte) (n int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) Next(n int) []byte {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.Next(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) ReadByte() (byte, error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) ReadRune() (r rune, size int, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) UnreadRune() error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.UnreadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) UnreadByte() error {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.UnreadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) ReadBytes(delim byte) (line []byte, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadBytes(delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LockableBuffer) ReadString(delim byte) (line string, err error) {
|
||||||
|
defer b.rw.Unlock()
|
||||||
|
b.rw.Lock()
|
||||||
|
return b.buf.ReadString(delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync is a no-op.
|
||||||
|
func (b *LockableBuffer) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -3,7 +3,6 @@ package testutil
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -63,8 +62,8 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if info, err = os.Stat(fileName); err == nil {
|
if info, err = os.Stat(fileName); err == nil { // nolint:nestif
|
||||||
if !info.IsDir() {
|
if !info.IsDir() { // nolint:nestif
|
||||||
var (
|
var (
|
||||||
path string
|
path string
|
||||||
source []byte
|
source []byte
|
||||||
@ -75,7 +74,7 @@ func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interf
|
|||||||
return dataMap, err
|
return dataMap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if source, err = ioutil.ReadFile(path); err != nil {
|
if source, err = os.ReadFile(path); err != nil {
|
||||||
return dataMap, err
|
return dataMap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package testutil
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
@ -40,7 +39,7 @@ func (t *TranslationsExtractorTest) SetupSuite() {
|
|||||||
data, _ := yaml.Marshal(translation)
|
data, _ := yaml.Marshal(translation)
|
||||||
// It's not regular temporary file. Little hack in order to test translations extractor.
|
// It's not regular temporary file. Little hack in order to test translations extractor.
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
errWrite := ioutil.WriteFile("/tmp/translate.en.yml", data, os.ModePerm)
|
errWrite := os.WriteFile("/tmp/translate.en.yml", data, os.ModePerm)
|
||||||
require.NoError(t.T(), errWrite)
|
require.NoError(t.T(), errWrite)
|
||||||
|
|
||||||
t.extractor = NewTranslationsExtractor("translate.{}.yml")
|
t.extractor = NewTranslationsExtractor("translate.{}.yml")
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@ -16,11 +18,12 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
retailcrm "github.com/retailcrm/api-client-go/v2"
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
|
|
||||||
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/util/errorutil"
|
"github.com/retailcrm/mg-transport-core/v2/core/util/errorutil"
|
||||||
@ -132,7 +135,7 @@ func (u *Utils) GenerateToken() string {
|
|||||||
func (u *Utils) GetAPIClient(
|
func (u *Utils) GetAPIClient(
|
||||||
url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) {
|
url, key string, scopes []string, credentials ...[]string) (*retailcrm.Client, int, error) {
|
||||||
client := retailcrm.New(url, key).
|
client := retailcrm.New(url, key).
|
||||||
WithLogger(retailcrm.DebugLoggerAdapter(u.Logger))
|
WithLogger(logger.APIClientAdapter(u.Logger))
|
||||||
client.Debug = u.IsDebug
|
client.Debug = u.IsDebug
|
||||||
|
|
||||||
cr, status, err := client.APICredentials()
|
cr, status, err := client.APICredentials()
|
||||||
@ -142,12 +145,12 @@ func (u *Utils) GetAPIClient(
|
|||||||
|
|
||||||
if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 {
|
if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 {
|
||||||
if len(credentials) == 0 || len(cr.Scopes) > 0 {
|
if len(credentials) == 0 || len(cr.Scopes) > 0 {
|
||||||
u.Logger.Error(url, status, res)
|
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
|
||||||
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 {
|
if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 {
|
||||||
u.Logger.Error(url, status, res)
|
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
|
||||||
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,3 +284,15 @@ func GetCurrencySymbol(code string) string {
|
|||||||
func FormatCurrencyValue(value float32) string {
|
func FormatCurrencyValue(value float32) string {
|
||||||
return fmt.Sprintf("%.2f", value)
|
return fmt.Sprintf("%.2f", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindJSONWithRaw will perform usual ShouldBindJSON and will return the original body data.
|
||||||
|
func BindJSONWithRaw(c *gin.Context, obj any) ([]byte, error) {
|
||||||
|
closer := c.Request.Body
|
||||||
|
defer func() { _ = closer.Close() }()
|
||||||
|
data, err := io.ReadAll(closer)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||||
|
return data, c.ShouldBindJSON(obj)
|
||||||
|
}
|
||||||
|
@ -9,13 +9,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h2non/gock"
|
"github.com/h2non/gock"
|
||||||
"github.com/op/go-logging"
|
|
||||||
retailcrm "github.com/retailcrm/api-client-go/v2"
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
"github.com/retailcrm/mg-transport-core/v2/core/config"
|
||||||
|
|
||||||
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
"github.com/retailcrm/mg-transport-core/v2/core/logger"
|
||||||
@ -38,7 +39,7 @@ func mgClient() *v1.MgClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UtilsTest) SetupSuite() {
|
func (u *UtilsTest) SetupSuite() {
|
||||||
logger := logger.NewStandard("code", logging.DEBUG, logger.DefaultLogFormatter())
|
logger := logger.NewDefault("json", true)
|
||||||
awsConfig := config.AWS{
|
awsConfig := config.AWS{
|
||||||
AccessKeyID: "access key id (will be removed)",
|
AccessKeyID: "access key id (will be removed)",
|
||||||
SecretAccessKey: "secret access key",
|
SecretAccessKey: "secret access key",
|
||||||
|
9
go.mod
9
go.mod
@ -1,6 +1,8 @@
|
|||||||
module github.com/retailcrm/mg-transport-core/v2
|
module github.com/retailcrm/mg-transport-core/v2
|
||||||
|
|
||||||
go 1.18
|
go 1.22
|
||||||
|
|
||||||
|
toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||||
@ -17,6 +19,7 @@ require (
|
|||||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
|
github.com/guregu/null/v5 v5.0.0
|
||||||
github.com/h2non/gock v1.2.0
|
github.com/h2non/gock v1.2.0
|
||||||
github.com/jessevdk/go-flags v1.4.0
|
github.com/jessevdk/go-flags v1.4.0
|
||||||
github.com/jinzhu/gorm v1.9.11
|
github.com/jinzhu/gorm v1.9.11
|
||||||
@ -24,10 +27,11 @@ require (
|
|||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/retailcrm/api-client-go/v2 v2.1.3
|
github.com/retailcrm/api-client-go/v2 v2.1.3
|
||||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32
|
github.com/retailcrm/mg-transport-api-client-go v1.3.4
|
||||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0
|
github.com/retailcrm/zabbix-metrics-collector v1.0.0
|
||||||
github.com/stretchr/testify v1.8.3
|
github.com/stretchr/testify v1.8.3
|
||||||
go.uber.org/atomic v1.10.0
|
go.uber.org/atomic v1.10.0
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
gopkg.in/gormigrate.v1 v1.6.0
|
gopkg.in/gormigrate.v1 v1.6.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
@ -77,6 +81,7 @@ require (
|
|||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
golang.org/x/net v0.23.0 // indirect
|
golang.org/x/net v0.23.0 // indirect
|
||||||
|
16
go.sum
16
go.sum
@ -157,6 +157,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
|||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
@ -237,6 +238,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
|||||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw=
|
||||||
|
github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU=
|
||||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@ -296,8 +299,9 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
@ -376,8 +380,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
|||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKKB//MUAe0Zb0=
|
github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKKB//MUAe0Zb0=
|
||||||
github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ=
|
github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ=
|
||||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs=
|
github.com/retailcrm/mg-transport-api-client-go v1.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE=
|
||||||
github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
|
github.com/retailcrm/mg-transport-api-client-go v1.3.4/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro=
|
||||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA=
|
github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA=
|
||||||
github.com/retailcrm/zabbix-metrics-collector v1.0.0/go.mod h1:3Orc+gfSg1tXj89QNvOn22t0cO1i2whR/4NJUGonWJA=
|
github.com/retailcrm/zabbix-metrics-collector v1.0.0/go.mod h1:3Orc+gfSg1tXj89QNvOn22t0cO1i2whR/4NJUGonWJA=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
@ -441,6 +445,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
Loading…
Reference in New Issue
Block a user