mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2024-11-27 23:46:08 +03:00
fix tests, update for logging
This commit is contained in:
parent
7c784c8310
commit
28b73ae09f
@ -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,8 +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 because its failure is already processed")
|
|
||||||
t.Assert().Contains(log.String(), `"counterId": 1`)
|
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() {
|
||||||
@ -108,8 +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")
|
|
||||||
t.Assert().Contains(log.String(), `"counterId": 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() {
|
||||||
@ -123,10 +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")
|
|
||||||
t.Assert().Contains(log.String(), `"counterId": 1`)
|
logs, err := log.ScanAll()
|
||||||
t.Assert().Contains(log.String(), `"error": "http status code: 500"`)
|
t.Require().NoError(err)
|
||||||
t.Assert().Contains(log.String(), `"failureMessage": "error message"`)
|
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)
|
||||||
@ -143,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)
|
||||||
@ -159,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 because it has too few requests {"counterId": %d, "minRequests": %d}`, 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() {
|
||||||
@ -175,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,8 +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")
|
|
||||||
t.Assert().Contains(log.String(), `"counterId": 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,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 {"counterId": 1, "error": "unknown error", "failureMessage": ""}`)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,7 +436,7 @@ 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.NewDefault("console", true))
|
t.manager.SetLogger(logger.NewDefault("json", true))
|
||||||
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
|
||||||
|
|
||||||
t.manager.SetLogger(nil)
|
t.manager.SetLogger(nil)
|
||||||
|
@ -2,10 +2,16 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
retailcrm "github.com/retailcrm/api-client-go/v2"
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiDebugLogReq = "API Request: %s %s"
|
||||||
|
apiDebugLogResp = "API Response: %s"
|
||||||
|
)
|
||||||
|
|
||||||
type apiClientAdapter struct {
|
type apiClientAdapter struct {
|
||||||
logger Logger
|
logger Logger
|
||||||
}
|
}
|
||||||
@ -17,5 +23,19 @@ func APIClientAdapter(logger Logger) retailcrm.BasicLogger {
|
|||||||
|
|
||||||
// Printf data in the log using Debug method.
|
// Printf data in the log using Debug method.
|
||||||
func (l *apiClientAdapter) Printf(format string, v ...interface{}) {
|
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...))
|
l.logger.Debug(fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
40
core/logger/api_client_adapter_test.go
Normal file
40
core/logger/api_client_adapter_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
retailcrm "github.com/retailcrm/api-client-go/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIClientAdapter(t *testing.T) {
|
||||||
|
log := newJSONBufferedLogger()
|
||||||
|
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"])
|
||||||
|
}
|
@ -2,24 +2,44 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
json "github.com/goccy/go-json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// HandlerAttr represents the attribute name for the handler.
|
||||||
HandlerAttr = "handler"
|
const HandlerAttr = "handler"
|
||||||
ConnectionAttr = "connection"
|
|
||||||
AccountAttr = "account"
|
|
||||||
CounterIDAttr = "counterId"
|
|
||||||
ErrorAttr = "error"
|
|
||||||
FailureMessageAttr = "failureMessage"
|
|
||||||
BodyAttr = "body"
|
|
||||||
HTTPMethodAttr = "method"
|
|
||||||
HTTPStatusAttr = "statusCode"
|
|
||||||
HTTPStatusNameAttr = "statusName"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// 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 {
|
func Err(err any) zap.Field {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return zap.String(ErrorAttr, "<nil>")
|
return zap.String(ErrorAttr, "<nil>")
|
||||||
@ -27,24 +47,46 @@ func Err(err any) zap.Field {
|
|||||||
return zap.Any(ErrorAttr, err)
|
return zap.Any(ErrorAttr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler returns a zap.Field with the given handler name.
|
||||||
func Handler(name string) zap.Field {
|
func Handler(name string) zap.Field {
|
||||||
return zap.String(HandlerAttr, name)
|
return zap.String(HandlerAttr, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPStatusCode returns a zap.Field with the given HTTP status code.
|
||||||
func HTTPStatusCode(code int) zap.Field {
|
func HTTPStatusCode(code int) zap.Field {
|
||||||
return zap.Int(HTTPStatusAttr, code)
|
return zap.Int(HTTPStatusAttr, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPStatusName returns a zap.Field with the given HTTP status name.
|
||||||
func HTTPStatusName(code int) zap.Field {
|
func HTTPStatusName(code int) zap.Field {
|
||||||
return zap.String(HTTPStatusNameAttr, http.StatusText(code))
|
return zap.String(HTTPStatusNameAttr, http.StatusText(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Body returns a zap.Field with the given request body value.
|
||||||
func Body(val any) zap.Field {
|
func Body(val any) zap.Field {
|
||||||
switch item := val.(type) {
|
switch item := val.(type) {
|
||||||
case string:
|
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)
|
return zap.String(BodyAttr, item)
|
||||||
case []byte:
|
case []byte:
|
||||||
|
var m interface{}
|
||||||
|
if err := json.Unmarshal(item, &m); err == nil {
|
||||||
|
return zap.Any(BodyAttr, m)
|
||||||
|
}
|
||||||
return zap.String(BodyAttr, string(item))
|
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:
|
default:
|
||||||
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
|
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
|
||||||
}
|
}
|
||||||
|
263
core/logger/buffer_logger_test.go
Normal file
263
core/logger/buffer_logger_test.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/guregu/null/v5"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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() *jSONRecordScanner {
|
||||||
|
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
|
||||||
|
)
|
@ -7,59 +7,85 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Logger is a logging interface.
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
|
// With adds fields to the logger and returns a new logger with those fields.
|
||||||
With(fields ...zap.Field) Logger
|
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
|
WithLazy(fields ...zap.Field) Logger
|
||||||
|
// Level returns the logging level of the logger.
|
||||||
Level() zapcore.Level
|
Level() zapcore.Level
|
||||||
|
// Check checks if the log message meets the given level.
|
||||||
Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry
|
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)
|
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)
|
Debug(msg string, fields ...zap.Field)
|
||||||
|
// Info logs an info-level message with the given fields.
|
||||||
Info(msg string, fields ...zap.Field)
|
Info(msg string, fields ...zap.Field)
|
||||||
|
// Warn logs a warning-level message with the given fields.
|
||||||
Warn(msg string, fields ...zap.Field)
|
Warn(msg string, fields ...zap.Field)
|
||||||
|
// Error logs an error-level message with the given fields.
|
||||||
Error(msg string, fields ...zap.Field)
|
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)
|
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)
|
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)
|
Fatal(msg string, fields ...zap.Field)
|
||||||
|
// ForHandler returns a new logger that is associated with the given handler.
|
||||||
ForHandler(handler any) Logger
|
ForHandler(handler any) Logger
|
||||||
|
// ForConnection returns a new logger that is associated with the given connection.
|
||||||
ForConnection(conn any) Logger
|
ForConnection(conn any) Logger
|
||||||
|
// ForAccount returns a new logger that is associated with the given account.
|
||||||
ForAccount(acc any) Logger
|
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
|
Sync() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default is a default logger implementation.
|
||||||
type Default struct {
|
type Default struct {
|
||||||
*zap.Logger
|
*zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDefault creates a new default logger with the given format and debug level.
|
||||||
func NewDefault(format string, debug bool) Logger {
|
func NewDefault(format string, debug bool) Logger {
|
||||||
return &Default{
|
return &Default{
|
||||||
Logger: NewZap(format, debug),
|
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 {
|
func (l *Default) With(fields ...zap.Field) Logger {
|
||||||
return l.clone(l.Logger.With(fields...))
|
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 {
|
func (l *Default) WithLazy(fields ...zap.Field) Logger {
|
||||||
return l.clone(l.Logger.WithLazy(fields...))
|
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 {
|
func (l *Default) ForHandler(handler any) Logger {
|
||||||
return l.WithLazy(zap.Any(HandlerAttr, handler))
|
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 {
|
func (l *Default) ForConnection(conn any) Logger {
|
||||||
return l.WithLazy(zap.Any(ConnectionAttr, conn))
|
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 {
|
func (l *Default) ForAccount(acc any) Logger {
|
||||||
return l.WithLazy(zap.Any(AccountAttr, acc))
|
return l.WithLazy(zap.Any(AccountAttr, acc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clone creates a copy of the given logger.
|
||||||
func (l *Default) clone(log *zap.Logger) Logger {
|
func (l *Default) clone(log *zap.Logger) Logger {
|
||||||
return &Default{Logger: log}
|
return &Default{Logger: log}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnyZapFields converts an array of values to zap fields.
|
||||||
func AnyZapFields(args []interface{}) []zap.Field {
|
func AnyZapFields(args []interface{}) []zap.Field {
|
||||||
fields := make([]zap.Field, len(args))
|
fields := make([]zap.Field, len(args))
|
||||||
for i := 0; i < len(fields); i++ {
|
for i := 0; i < len(fields); i++ {
|
||||||
|
638
core/logger/json_with_context_encoder.go
Normal file
638
core/logger/json_with_context_encoder.go
Normal file
@ -0,0 +1,638 @@
|
|||||||
|
// 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 (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"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:])
|
||||||
|
}
|
@ -2,18 +2,57 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
type mgTransportClientAdapter struct {
|
||||||
log Logger
|
log Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func MGTransportClientAdapter(log Logger) v1.DebugLogger {
|
// MGTransportClientAdapter constructs an adapter that will log MG requests and responses.
|
||||||
|
func MGTransportClientAdapter(log Logger) v1.BasicLogger {
|
||||||
return &mgTransportClientAdapter{log: log}
|
return &mgTransportClientAdapter{log: log}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debugf writes a message with Debug level.
|
||||||
func (m *mgTransportClientAdapter) Debugf(msg string, args ...interface{}) {
|
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 {
|
||||||
|
token = fmt.Sprint(args[2])
|
||||||
|
}
|
||||||
|
if len(args) > 3 {
|
||||||
|
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...))
|
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...)
|
||||||
|
}
|
||||||
|
43
core/logger/mg_transport_client_adapter_test.go
Normal file
43
core/logger/mg_transport_client_adapter_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMGTransportClientAdapter(t *testing.T) {
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
log := newJSONBufferedLogger()
|
||||||
|
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"])
|
||||||
|
}
|
@ -5,8 +5,10 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Nil logger doesn't do anything.
|
||||||
type Nil struct{}
|
type Nil struct{}
|
||||||
|
|
||||||
|
// NewNil constructs new *Nil.
|
||||||
func NewNil() Logger {
|
func NewNil() Logger {
|
||||||
return &Nil{}
|
return &Nil{}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
@ -11,6 +11,7 @@ type writerAdapter struct {
|
|||||||
level zapcore.Level
|
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 {
|
func WriterAdapter(log Logger, level zapcore.Level) io.Writer {
|
||||||
return &writerAdapter{log: log, level: level}
|
return &writerAdapter{log: log, level: level}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,24 @@ type zabbixCollectorAdapter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) {
|
func (a *zabbixCollectorAdapter) Errorf(format string, args ...interface{}) {
|
||||||
|
baseMsg := "cannot send metrics to Zabbix"
|
||||||
|
switch format {
|
||||||
|
case "cannot send metrics to Zabbix: %v":
|
||||||
|
baseMsg = "cannot stop collector"
|
||||||
|
fallthrough
|
||||||
|
case "cannot stop collector: %s":
|
||||||
|
var err interface{}
|
||||||
|
if len(args) > 0 {
|
||||||
|
err = args[0]
|
||||||
|
}
|
||||||
|
a.log.Error(baseMsg, Err(err))
|
||||||
|
default:
|
||||||
a.log.Error(fmt.Sprintf(format, args...))
|
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 {
|
func ZabbixCollectorAdapter(log Logger) metrics.ErrorLogger {
|
||||||
return &zabbixCollectorAdapter{log: log}
|
return &zabbixCollectorAdapter{log: log}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ func NewZapJSON(debug bool) *zap.Logger {
|
|||||||
log, err := zap.Config{
|
log, err := zap.Config{
|
||||||
Level: zap.NewAtomicLevelAt(level),
|
Level: zap.NewAtomicLevelAt(level),
|
||||||
Development: debug,
|
Development: debug,
|
||||||
Encoding: "json",
|
Encoding: "json-with-context",
|
||||||
EncoderConfig: EncoderConfigJSON(),
|
EncoderConfig: EncoderConfigJSON(),
|
||||||
OutputPaths: []string{"stdout"},
|
OutputPaths: []string{"stdout"},
|
||||||
ErrorOutputPaths: []string{"stderr"},
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
@ -142,7 +142,7 @@ func (t *HTTPClientBuilderTest) Test_WithLogger() {
|
|||||||
builder.WithLogger(nil)
|
builder.WithLogger(nil)
|
||||||
assert.Nil(t.T(), builder.logger)
|
assert.Nil(t.T(), builder.logger)
|
||||||
|
|
||||||
log := logger.NewDefault("console", true)
|
log := logger.NewDefault("json", true)
|
||||||
builder.WithLogger(log)
|
builder.WithLogger(log)
|
||||||
assert.NotNil(t.T(), builder.logger)
|
assert.NotNil(t.T(), builder.logger)
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ func NewBufferedLogger() BufferedLogger {
|
|||||||
bl := &BufferLogger{}
|
bl := &BufferLogger{}
|
||||||
bl.Logger = zap.New(
|
bl.Logger = zap.New(
|
||||||
zapcore.NewCore(
|
zapcore.NewCore(
|
||||||
zapcore.NewConsoleEncoder(
|
logger.NewJSONWithContextEncoder(
|
||||||
logger.EncoderConfigConsole()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
|
logger.EncoderConfigJSON()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
|
||||||
return bl
|
return bl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,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().Contains(string(data), "level=DEBUG test")
|
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().Contains(string(t.logger.Bytes()), "level=DEBUG test")
|
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().Contains(t.logger.String(), "level=DEBUG test")
|
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() {
|
||||||
|
51
core/util/testutil/json_record_scanner.go
Normal file
51
core/util/testutil/json_record_scanner.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/guregu/null/v5"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
107
core/util/testutil/json_record_scanner_test.go
Normal file
107
core/util/testutil/json_record_scanner_test.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
@ -38,7 +38,7 @@ func mgClient() *v1.MgClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UtilsTest) SetupSuite() {
|
func (u *UtilsTest) SetupSuite() {
|
||||||
logger := logger.NewDefault("console", true)
|
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",
|
||||||
|
5
go.mod
5
go.mod
@ -1,6 +1,8 @@
|
|||||||
module github.com/retailcrm/mg-transport-core/v2
|
module github.com/retailcrm/mg-transport-core/v2
|
||||||
|
|
||||||
go 1.21
|
go 1.21.4
|
||||||
|
|
||||||
|
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
|
||||||
|
2
go.sum
2
go.sum
@ -238,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=
|
||||||
|
Loading…
Reference in New Issue
Block a user