integrate zap logger

This commit is contained in:
Pavel 2023-11-24 15:23:07 +03:00
parent 2455c11704
commit 96ac6a6940
27 changed files with 330 additions and 286 deletions

View File

@ -5,7 +5,6 @@ import (
"fmt"
"html/template"
"io/fs"
"log/slog"
"net/http"
"sync"
@ -15,6 +14,7 @@ import (
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/retailcrm/zabbix-metrics-collector"
"go.uber.org/zap"
"golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config"
@ -141,13 +141,9 @@ func (e *Engine) Prepare() *Engine {
e.Localizer.Preload(e.PreloadLanguages)
}
if !e.Config.IsDebug() {
logger.DefaultOpts.Level = slog.LevelInfo
}
e.CreateDB(e.Config.GetDBConfig())
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
e.SetLogger(logger.NewDefaultText())
e.SetLogger(logger.NewDefault(e.Config.IsDebug()))
e.Sentry.Localizer = &e.Localizer
e.Utils.Logger = e.Logger()
e.Sentry.Logger = e.Logger()
@ -180,14 +176,14 @@ func (e *Engine) HijackGinLogs() *Engine {
if e.Logger() == nil {
return e
}
gin.DefaultWriter = logger.WriterAdapter(e.Logger(), slog.LevelDebug)
gin.DefaultErrorWriter = logger.WriterAdapter(e.Logger(), slog.LevelError)
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",
slog.String(logger.HTTPMethodAttr, httpMethod),
slog.String("path", absolutePath),
slog.String(logger.HandlerAttr, handlerName),
slog.Int("handlerCount", nuHandlers))
zap.String(logger.HTTPMethodAttr, httpMethod),
zap.String("path", absolutePath),
zap.String(logger.HandlerAttr, handlerName),
zap.Int("handlerCount", nuHandlers))
}
return e
}

View File

@ -252,7 +252,7 @@ func (e *EngineTest) Test_SetLogger() {
defer func() {
e.engine.logger = origLogger
}()
e.engine.logger = logger.NewDefaultNil()
e.engine.logger = logger.NewNil()
e.engine.SetLogger(nil)
assert.NotNil(e.T(), e.engine.logger)
}

View File

@ -1,9 +1,8 @@
package healthcheck
import (
"log/slog"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
"go.uber.org/zap"
)
const (
@ -31,19 +30,19 @@ type CounterProcessor struct {
func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varnamelen
if counter.IsFailed() {
if counter.IsFailureProcessed() {
c.debugLog("skipping counter because its failure is already processed", slog.Int(logger.CounterIDAttr, id))
c.debugLog("skipping counter because its failure is already processed", zap.Int(logger.CounterIDAttr, id))
return true
}
apiURL, apiKey, _, exists := c.ConnectionDataProvider(id)
if !exists {
c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id))
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
return true
}
err := c.Notifier(apiURL, apiKey, counter.Message())
if err != nil {
c.debugLog("cannot send notification for counter",
slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message()))
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
}
counter.FailureProcessed()
return true
@ -56,7 +55,7 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
// The results may not be representative.
if (succeeded + failed) < c.MinRequests {
c.debugLog("skipping counter because it has too few requests",
slog.Int(logger.CounterIDAttr, id), slog.Any("minRequests", c.MinRequests))
zap.Int(logger.CounterIDAttr, id), zap.Any("minRequests", c.MinRequests))
return true
}
@ -75,13 +74,13 @@ func (c CounterProcessor) Process(id int, counter Counter) bool { // nolint:varn
apiURL, apiKey, lang, exists := c.ConnectionDataProvider(id)
if !exists {
c.debugLog("cannot find connection data for counter", slog.Int(logger.CounterIDAttr, id))
c.debugLog("cannot find connection data for counter", zap.Int(logger.CounterIDAttr, id))
return true
}
err := c.Notifier(apiURL, apiKey, c.getErrorText(counter.Name(), c.Error, lang))
if err != nil {
c.debugLog("cannot send notification for counter",
slog.Int(logger.CounterIDAttr, id), logger.Err(err), slog.String(logger.FailureMessageAttr, counter.Message()))
zap.Int(logger.CounterIDAttr, id), logger.Err(err), zap.String(logger.FailureMessageAttr, counter.Message()))
}
counter.CountersProcessed()
return true
@ -99,6 +98,6 @@ func (c CounterProcessor) getErrorText(name, msg, lang string) string {
func (c CounterProcessor) debugLog(msg string, args ...interface{}) {
if c.Debug {
c.Logger.Debug(msg, args...)
c.Logger.Debug(msg, logger.AnyZapFields(args)...)
}
}

View File

@ -96,7 +96,7 @@ func (t *CounterProcessorTest) Test_FailureProcessed() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "skipping counter because its failure is already processed")
t.Assert().Contains(log.String(), "counterId=1")
t.Assert().Contains(log.String(), `"counterId": 1`)
}
func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
@ -109,7 +109,7 @@ func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot find connection data for counter")
t.Assert().Contains(log.String(), "counterId=1")
t.Assert().Contains(log.String(), `"counterId": 1`)
}
func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
@ -124,9 +124,9 @@ func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot send notification for counter")
t.Assert().Contains(log.String(), "counterId=1")
t.Assert().Contains(log.String(), `error="http status code: 500"`)
t.Assert().Contains(log.String(), `failureMessage="error message"`)
t.Assert().Contains(log.String(), `"counterId": 1`)
t.Assert().Contains(log.String(), `"error": "http status code: 500"`)
t.Assert().Contains(log.String(), `"failureMessage": "error message"`)
t.Assert().Equal(t.apiURL, n.apiURL)
t.Assert().Equal(t.apiKey, n.apiKey)
t.Assert().Equal("error message", n.message)
@ -160,7 +160,7 @@ func (t *CounterProcessorTest) Test_TooFewRequests() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(),
fmt.Sprintf(`msg="skipping counter because it has too few requests" counterId=%d minRequests=%d`, 1, DefaultMinRequests))
fmt.Sprintf(`skipping counter because it has too few requests {"counterId": %d, "minRequests": %d}`, 1, DefaultMinRequests))
}
func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
@ -206,7 +206,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot find connection data for counter")
t.Assert().Contains(log.String(), "counterId=1")
t.Assert().Contains(log.String(), `"counterId": 1`)
t.Assert().Empty(n.message)
}
@ -224,7 +224,7 @@ func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() {
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), `msg="cannot send notification for counter" counterId=1 error="unknown error" failureMessage=""`)
t.Assert().Contains(log.String(), `cannot send notification for counter {"counterId": 1, "error": "unknown error", "failureMessage": ""}`)
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
}

View File

@ -3,11 +3,11 @@ package core
import (
"errors"
"fmt"
"log/slog"
"sync"
"time"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
"go.uber.org/zap"
)
// JobFunc is empty func which should be executed in a parallel goroutine.
@ -74,7 +74,7 @@ func (j *Job) getWrappedFunc(name string, log logger.Logger) func(callback JobAf
}
}()
log = log.With(logger.HandlerAttr, name)
log = log.With(logger.Handler(name))
err := j.Command(log)
if err != nil && j.ErrorHandler != nil {
j.ErrorHandler(name, err, log)
@ -148,14 +148,14 @@ func (j *Job) runOnceSync(name string, log logger.Logger) {
// NewJobManager is a JobManager constructor.
func NewJobManager() *JobManager {
return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewDefaultNil()}
return &JobManager{jobs: &sync.Map{}, nilLogger: logger.NewNil()}
}
// DefaultJobErrorHandler returns default error handler for a job.
func DefaultJobErrorHandler() JobErrorHandler {
return func(name string, err error, log logger.Logger) {
if err != nil && name != "" {
log.Error("job failed with an error", slog.String("job", name), logger.Err(err))
log.Error("job failed with an error", zap.String("job", name), logger.Err(err))
}
}
}
@ -164,7 +164,7 @@ func DefaultJobErrorHandler() JobErrorHandler {
func DefaultJobPanicHandler() JobPanicHandler {
return func(name string, recoverValue interface{}, log logger.Logger) {
if recoverValue != nil && name != "" {
log.Error("job panicked with the value", slog.String("job", name), slog.Any("value", recoverValue))
log.Error("job panicked with the value", zap.String("job", name), zap.Any("value", recoverValue))
}
}
}

View File

@ -1,10 +1,8 @@
package core
import (
"context"
"errors"
"fmt"
"log/slog"
"math/rand"
"strings"
"sync"
@ -14,6 +12,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
)
@ -26,7 +26,7 @@ type JobTest struct {
executeErr chan error
panicValue chan interface{}
lastLog string
lastMsgLevel slog.Level
lastMsgLevel zapcore.Level
syncBool bool
}
@ -37,18 +37,58 @@ type JobManagerTest struct {
syncRunnerFlag bool
}
type callbackLoggerFunc func(ctx context.Context, level slog.Level, msg string, args ...any)
type callbackLoggerFunc func(level zapcore.Level, msg string, args ...zap.Field)
type callbackLogger struct {
fields []zap.Field
fn callbackLoggerFunc
}
func (n *callbackLogger) Handler() slog.Handler {
return logger.NilHandler
func (n *callbackLogger) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
return &zapcore.CheckedEntry{}
}
func (n *callbackLogger) With(args ...any) logger.Logger {
return n
func (n *callbackLogger) DPanic(msg string, fields ...zap.Field) {
n.fn(zap.PanicLevel, msg, fields...)
}
func (n *callbackLogger) Panic(msg string, fields ...zap.Field) {
n.fn(zap.PanicLevel, msg, fields...)
}
func (n *callbackLogger) Fatal(msg string, fields ...zap.Field) {
n.fn(zap.FatalLevel, msg, fields...)
}
func (n *callbackLogger) Sync() error {
return nil
}
func (n *callbackLogger) clone() *callbackLogger {
return &callbackLogger{fn: n.fn, fields: n.fields}
}
func (n *callbackLogger) cloneWithFields(fields []zap.Field) *callbackLogger {
cl := &callbackLogger{fn: n.fn, fields: n.fields}
existing := cl.fields
if len(existing) == 0 {
cl.fields = fields
return cl
}
cl.fields = append(existing, fields...)
return cl
}
func (n *callbackLogger) Level() zapcore.Level {
return zapcore.DebugLevel
}
func (n *callbackLogger) With(args ...zap.Field) logger.Logger {
return n.cloneWithFields(args)
}
func (n *callbackLogger) WithLazy(args ...zap.Field) logger.Logger {
return n.cloneWithFields(args)
}
func (n *callbackLogger) WithGroup(name string) logger.Logger {
@ -59,47 +99,24 @@ func (n *callbackLogger) ForAccount(handler, conn, acc any) logger.Logger {
return n
}
func (n *callbackLogger) Enabled(ctx context.Context, level slog.Level) bool {
return true
func (n *callbackLogger) Log(level zapcore.Level, msg string, args ...zap.Field) {
n.fn(level, msg, args...)
}
func (n *callbackLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) {
n.fn(ctx, level, msg, args...)
func (n *callbackLogger) Debug(msg string, args ...zap.Field) {
n.Log(zap.DebugLevel, msg, args...)
}
func (n *callbackLogger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
func (n *callbackLogger) Info(msg string, args ...zap.Field) {
n.Log(zap.InfoLevel, msg, args...)
}
func (n *callbackLogger) Debug(msg string, args ...any) {
n.Log(nil, slog.LevelDebug, msg, args...)
func (n *callbackLogger) Warn(msg string, args ...zap.Field) {
n.Log(zap.WarnLevel, msg, args...)
}
func (n *callbackLogger) DebugContext(ctx context.Context, msg string, args ...any) {
n.Log(ctx, slog.LevelDebug, msg, args...)
}
func (n *callbackLogger) Info(msg string, args ...any) {
n.Log(nil, slog.LevelInfo, msg, args...)
}
func (n *callbackLogger) InfoContext(ctx context.Context, msg string, args ...any) {
n.Log(ctx, slog.LevelInfo, msg, args...)
}
func (n *callbackLogger) Warn(msg string, args ...any) {
n.Log(nil, slog.LevelWarn, msg, args...)
}
func (n *callbackLogger) WarnContext(ctx context.Context, msg string, args ...any) {
n.Log(ctx, slog.LevelWarn, msg, args...)
}
func (n *callbackLogger) Error(msg string, args ...any) {
n.Log(nil, slog.LevelError, msg, args...)
}
func (n *callbackLogger) ErrorContext(ctx context.Context, msg string, args ...any) {
n.Log(ctx, slog.LevelError, msg, args...)
func (n *callbackLogger) Error(msg string, args ...zap.Field) {
n.Log(zap.ErrorLevel, msg, args...)
}
func TestJob(t *testing.T) {
@ -117,9 +134,9 @@ func TestDefaultJobErrorHandler(t *testing.T) {
fn := DefaultJobErrorHandler()
require.NotNil(t, fn)
fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) {
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
require.Len(t, i, 2)
assert.Equal(t, "error=test", fmt.Sprintf("%s", i[1]))
assert.Equal(t, "error=test", fmt.Sprintf("%s=%v", i[1].Key, i[1].Interface))
}})
}
@ -130,9 +147,9 @@ func TestDefaultJobPanicHandler(t *testing.T) {
fn := DefaultJobPanicHandler()
require.NotNil(t, fn)
fn("job", errors.New("test"), &callbackLogger{fn: func(_ context.Context, level slog.Level, s string, i ...interface{}) {
fn("job", errors.New("test"), &callbackLogger{fn: func(level zapcore.Level, s string, i ...zap.Field) {
require.Len(t, i, 2)
assert.Equal(t, "value=test", fmt.Sprintf("%s", i[1]))
assert.Equal(t, "value=test", fmt.Sprintf("%s=%s", i[1].Key, i[1].Interface))
}})
}
@ -149,7 +166,7 @@ func (t *JobTest) testPanicHandler() JobPanicHandler {
}
func (t *JobTest) testLogger() logger.Logger {
return &callbackLogger{fn: func(_ context.Context, level slog.Level, format string, args ...interface{}) {
return &callbackLogger{fn: func(level zapcore.Level, format string, args ...zap.Field) {
if format == "" {
var sb strings.Builder
sb.Grow(3 * len(args)) // nolint:gomnd
@ -161,7 +178,12 @@ func (t *JobTest) testLogger() logger.Logger {
format = strings.TrimRight(sb.String(), " ")
}
t.lastLog = fmt.Sprintf(format, args...)
anyFields := []any{}
for _, item := range args {
anyFields = append(anyFields, item.Key+"="+fmt.Sprint(item.Interface))
}
t.lastLog = fmt.Sprintf(format, anyFields...)
t.lastMsgLevel = level
}}
}
@ -406,7 +428,7 @@ func (t *JobManagerTest) WaitForJob() bool {
func (t *JobManagerTest) Test_SetLogger() {
t.manager.logger = nil
t.manager.SetLogger(logger.NewDefaultText())
t.manager.SetLogger(logger.NewDefault(true))
assert.IsType(t.T(), &logger.Default{}, t.manager.logger)
t.manager.SetLogger(nil)

View File

@ -2,8 +2,9 @@ package logger
import (
"fmt"
"log/slog"
"net/http"
"go.uber.org/zap"
)
const (
@ -19,28 +20,32 @@ const (
HTTPStatusNameAttr = "statusName"
)
func Err(err any) slog.Attr {
func Err(err any) zap.Field {
if err == nil {
return slog.String(ErrorAttr, "<nil>")
return zap.String(ErrorAttr, "<nil>")
}
return slog.Any(ErrorAttr, err)
return zap.Any(ErrorAttr, err)
}
func HTTPStatusCode(code int) slog.Attr {
return slog.Int(HTTPStatusAttr, code)
func Handler(name string) zap.Field {
return zap.String(HandlerAttr, name)
}
func HTTPStatusName(code int) slog.Attr {
return slog.String(HTTPStatusNameAttr, http.StatusText(code))
func HTTPStatusCode(code int) zap.Field {
return zap.Int(HTTPStatusAttr, code)
}
func Body(val any) slog.Attr {
func HTTPStatusName(code int) zap.Field {
return zap.String(HTTPStatusNameAttr, http.StatusText(code))
}
func Body(val any) zap.Field {
switch item := val.(type) {
case string:
return slog.String(BodyAttr, item)
return zap.String(BodyAttr, item)
case []byte:
return slog.String(BodyAttr, string(item))
return zap.String(BodyAttr, string(item))
default:
return slog.String(BodyAttr, fmt.Sprintf("%#v", val))
return zap.String(BodyAttr, fmt.Sprintf("%#v", val))
}
}

View File

@ -1,87 +1,59 @@
package logger
import (
"context"
"log/slog"
"os"
"strconv"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type Logger interface {
With(fields ...zap.Field) Logger
WithLazy(fields ...zap.Field) Logger
Level() zapcore.Level
Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry
Log(lvl zapcore.Level, msg string, fields ...zap.Field)
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
DPanic(msg string, fields ...zap.Field)
Panic(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field)
ForAccount(handler, conn, acc any) Logger
Sync() error
}
type Default struct {
Logger *slog.Logger
*zap.Logger
}
func NewDefault(log *slog.Logger) Logger {
return &Default{Logger: log}
func NewDefault(debug bool) Logger {
return &Default{
Logger: NewZap(debug),
}
}
func NewDefaultText() Logger {
return NewDefault(slog.New(slog.NewTextHandler(os.Stdout, DefaultOpts)))
func (l *Default) With(fields ...zap.Field) Logger {
return l.With(fields...).(Logger)
}
func NewDefaultJSON() Logger {
return NewDefault(slog.New(slog.NewJSONHandler(os.Stdout, DefaultOpts)))
func (l *Default) WithLazy(fields ...zap.Field) Logger {
return l.WithLazy(fields...).(Logger)
}
func NewDefaultNil() Logger {
return NewDefault(slog.New(NilHandler))
func (l *Default) ForAccount(handler, conn, acc any) Logger {
return l.WithLazy(zap.Any(HandlerAttr, handler), zap.Any(ConnectionAttr, conn), zap.Any(AccountAttr, acc))
}
func (d *Default) Handler() slog.Handler {
return d.Logger.Handler()
}
func (d *Default) ForAccount(handler, conn, acc any) Logger {
return d.With(slog.Any(HandlerAttr, handler), slog.Any(ConnectionAttr, conn), slog.Any(AccountAttr, acc))
}
func (d *Default) With(args ...any) Logger {
return &Default{Logger: d.Logger.With(args...)}
}
func (d *Default) WithGroup(name string) Logger {
return &Default{Logger: d.Logger.WithGroup(name)}
}
func (d *Default) Enabled(ctx context.Context, level slog.Level) bool {
return d.Logger.Enabled(ctx, level)
}
func (d *Default) Log(ctx context.Context, level slog.Level, msg string, args ...any) {
d.Logger.Log(ctx, level, msg, args...)
}
func (d *Default) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
d.Logger.LogAttrs(ctx, level, msg, attrs...)
}
func (d *Default) Debug(msg string, args ...any) {
d.Logger.Debug(msg, args...)
}
func (d *Default) DebugContext(ctx context.Context, msg string, args ...any) {
d.Logger.DebugContext(ctx, msg, args...)
}
func (d *Default) Info(msg string, args ...any) {
d.Logger.Info(msg, args...)
}
func (d *Default) InfoContext(ctx context.Context, msg string, args ...any) {
d.Logger.InfoContext(ctx, msg, args...)
}
func (d *Default) Warn(msg string, args ...any) {
d.Logger.Warn(msg, args...)
}
func (d *Default) WarnContext(ctx context.Context, msg string, args ...any) {
d.Logger.WarnContext(ctx, msg, args...)
}
func (d *Default) Error(msg string, args ...any) {
d.Logger.Error(msg, args...)
}
func (d *Default) ErrorContext(ctx context.Context, msg string, args ...any) {
d.Logger.ErrorContext(ctx, msg, args...)
func AnyZapFields(args []interface{}) []zap.Field {
fields := make([]zap.Field, len(args))
for i := 0; i < len(fields); i++ {
if val, ok := args[i].(zap.Field); ok {
fields[i] = val
continue
}
fields[i] = zap.Any("arg"+strconv.Itoa(i), args[i])
}
return fields
}

View File

@ -1,9 +1,10 @@
package logger
import (
"github.com/gin-gonic/gin"
"log/slog"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// GinMiddleware will construct Gin middleware which will log requests.
@ -23,14 +24,14 @@ func GinMiddleware(log Logger) gin.HandlerFunc {
}
log.Info("request",
slog.String(HandlerAttr, "GIN"),
slog.String("startTime", start.Format(time.RFC3339)),
slog.String("endTime", end.Format(time.RFC3339)),
slog.Any("latency", end.Sub(start)/time.Millisecond),
slog.String("remoteAddress", c.ClientIP()),
slog.String(HTTPMethodAttr, c.Request.Method),
slog.String("path", path),
slog.Int("bodySize", c.Writer.Size()),
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()),
)
}
}

View File

@ -1,8 +0,0 @@
package logger
import "log/slog"
var DefaultOpts = &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}

View File

@ -1,44 +0,0 @@
package logger
import (
"context"
"log/slog"
)
// LoggerOld contains methods which should be present in logger implementation.
type LoggerOld interface {
Fatal(args ...any)
Fatalf(format string, args ...any)
Panic(args ...any)
Panicf(format string, args ...any)
Critical(args ...any)
Criticalf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Warning(args ...any)
Warningf(format string, args ...any)
Notice(args ...any)
Noticef(format string, args ...any)
Info(args ...any)
Infof(format string, args ...any)
Debug(args ...any)
Debugf(format string, args ...any)
}
type Logger interface {
Handler() slog.Handler
With(args ...any) Logger
WithGroup(name string) Logger
ForAccount(handler, conn, acc any) Logger
Enabled(ctx context.Context, level slog.Level) bool
Log(ctx context.Context, level slog.Level, msg string, args ...any)
LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr)
Debug(msg string, args ...any)
DebugContext(ctx context.Context, msg string, args ...any)
Info(msg string, args ...any)
InfoContext(ctx context.Context, msg string, args ...any)
Warn(msg string, args ...any)
WarnContext(ctx context.Context, msg string, args ...any)
Error(msg string, args ...any)
ErrorContext(ctx context.Context, msg string, args ...any)
}

View File

@ -2,6 +2,7 @@ package logger
import (
"fmt"
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
)

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

@ -0,0 +1,52 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type Nil struct{}
func NewNil() Logger {
return &Nil{}
}
func (l *Nil) With(fields ...zap.Field) Logger {
return l
}
func (l *Nil) WithLazy(fields ...zap.Field) Logger {
return l
}
func (l *Nil) Level() zapcore.Level {
return zapcore.DebugLevel
}
func (l *Nil) Check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
return &zapcore.CheckedEntry{}
}
func (l *Nil) Log(lvl zapcore.Level, msg string, fields ...zap.Field) {}
func (l *Nil) Debug(msg string, fields ...zap.Field) {}
func (l *Nil) Info(msg string, fields ...zap.Field) {}
func (l *Nil) Warn(msg string, fields ...zap.Field) {}
func (l *Nil) Error(msg string, fields ...zap.Field) {}
func (l *Nil) DPanic(msg string, fields ...zap.Field) {}
func (l *Nil) Panic(msg string, fields ...zap.Field) {}
func (l *Nil) Fatal(msg string, fields ...zap.Field) {}
func (l *Nil) ForAccount(handler, conn, acc any) Logger {
return l
}
func (l *Nil) Sync() error {
return nil
}

View File

@ -1,26 +0,0 @@
package logger
import (
"context"
"log/slog"
)
var NilHandler slog.Handler = &nilHandler{}
type nilHandler struct{}
func (n *nilHandler) Enabled(ctx context.Context, level slog.Level) bool {
return false
}
func (n *nilHandler) Handle(ctx context.Context, record slog.Record) error {
return nil
}
func (n *nilHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return n
}
func (n *nilHandler) WithGroup(name string) slog.Handler {
return n
}

View File

@ -1,21 +1,21 @@
package logger
import (
"context"
"io"
"log/slog"
"go.uber.org/zap/zapcore"
)
type writerAdapter struct {
log Logger
level slog.Level
level zapcore.Level
}
func WriterAdapter(log Logger, level slog.Level) io.Writer {
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(context.Background(), w.level, string(p))
w.log.Log(w.level, string(p))
return len(p), nil
}

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

@ -0,0 +1,52 @@
package logger
import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func NewZap(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: EncoderConfig(),
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}.Build()
if err != nil {
panic(err)
}
return log
}
func EncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
TimeKey: "timestamp",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "",
LineEnding: "\n",
EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("level=" + level.String())
},
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("time=" + t.Format(time.RFC3339))
},
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString("caller=" + caller.TrimmedPath())
},
EncodeName: zapcore.FullNameEncoder,
ConsoleSeparator: " ",
}
}

View File

@ -5,7 +5,6 @@ import (
"context"
"fmt"
"html/template"
"log/slog"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
@ -13,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gomarkdown/markdown"
"go.uber.org/zap"
"golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config"
@ -91,14 +91,14 @@ func (s *ModuleFeaturesUploader) Upload() {
content, err := os.ReadFile(s.featuresFilename)
if err != nil {
s.log.Error("cannot read markdown file %s %s", slog.String("fileName", s.featuresFilename), logger.Err(err))
s.log.Error("cannot read markdown file %s %s", zap.String("fileName", s.featuresFilename), logger.Err(err))
return
}
for _, lang := range languages {
translated, err := s.translate(content, lang)
if err != nil {
s.log.Error("cannot translate module features file", slog.String("lang", lang.String()), logger.Err(err))
s.log.Error("cannot translate module features file", zap.String("lang", lang.String()), logger.Err(err))
continue
}
@ -106,7 +106,7 @@ func (s *ModuleFeaturesUploader) Upload() {
resp, err := s.uploadFile(html, lang.String())
if err != nil {
s.log.Error("cannot upload file", slog.String("lang", lang.String()), logger.Err(err))
s.log.Error("cannot upload file", zap.String("lang", lang.String()), logger.Err(err))
continue
}

View File

@ -2,7 +2,6 @@ package core
import (
"fmt"
"log/slog"
"net"
"net/http"
"net/http/httputil"
@ -16,6 +15,7 @@ import (
"github.com/getsentry/sentry-go"
sentrygin "github.com/getsentry/sentry-go/gin"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
"github.com/retailcrm/mg-transport-core/v2/core/stacktrace"
@ -260,8 +260,8 @@ func (s *Sentry) recoveryMiddleware() gin.HandlerFunc { // nolint
}
headers[idx] = "header: " + headers[idx]
}
headersToStr := slog.String("headers", strings.Join(headers, "\r\n"))
formattedStack := slog.String("stacktrace", string(stack))
headersToStr := zap.String("headers", strings.Join(headers, "\r\n"))
formattedStack := zap.String("stacktrace", string(stack))
switch {
case brokenPipe:
l.Error("error", formattedErr, headersToStr)

View File

@ -12,9 +12,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/retailcrm/mg-transport-core/v2/core/config"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
)
@ -275,7 +273,7 @@ func (b *HTTPClientBuilder) buildMocks() error {
func (b *HTTPClientBuilder) log(msg string, args ...interface{}) {
if b.logging {
if b.logger != nil {
b.logger.Info(msg, args...)
b.logger.Info(msg, logger.AnyZapFields(args)...)
} else {
fmt.Println(append([]any{msg}, args...))
}

View File

@ -142,7 +142,7 @@ func (t *HTTPClientBuilderTest) Test_WithLogger() {
builder.WithLogger(nil)
assert.Nil(t.T(), builder.logger)
log := logger.NewDefaultText()
log := logger.NewDefault(true)
builder.WithLogger(log)
assert.NotNil(t.T(), builder.logger)
}

View File

@ -3,9 +3,11 @@ package testutil
import (
"fmt"
"io"
"log/slog"
"os"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ReadBuffer is implemented by the BufferLogger.
@ -32,21 +34,32 @@ type BufferLogger struct {
// NewBufferedLogger returns new BufferedLogger instance.
func NewBufferedLogger() BufferedLogger {
bl := &BufferLogger{}
bl.Logger = slog.New(slog.NewTextHandler(&bl.buf, logger.DefaultOpts))
bl.Logger = zap.New(
zapcore.NewCore(
zapcore.NewConsoleEncoder(
logger.EncoderConfig()), zap.CombineWriteSyncers(os.Stdout, os.Stderr, &bl.buf), zapcore.DebugLevel))
return bl
}
// With doesn't do anything here and only added for backwards compatibility with the interface.
func (l *BufferLogger) With(args ...any) logger.Logger {
func (l *BufferLogger) With(fields ...zapcore.Field) logger.Logger {
return &BufferLogger{
Default: logger.Default{
Logger: l.Logger.With(args...),
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) ForAccount(handler, conn, acc any) logger.Logger {
return l.With(slog.Any(logger.HandlerAttr, handler), slog.Any(logger.ConnectionAttr, conn), slog.Any(logger.AccountAttr, acc))
return l.WithLazy(
zap.Any(logger.HandlerAttr, handler), zap.Any(logger.ConnectionAttr, conn), zap.Any(logger.AccountAttr, acc))
}
// Read bytes from the logger buffer. io.Reader implementation.

View File

@ -29,17 +29,17 @@ func (t *BufferLoggerTest) Test_Read() {
data, err := io.ReadAll(t.logger)
t.Require().NoError(err)
t.Assert().Contains(string(data), "level=DEBUG msg=test")
t.Assert().Contains(string(data), "level=debug test")
}
func (t *BufferLoggerTest) Test_Bytes() {
t.logger.Debug("test")
t.Assert().Contains(string(t.logger.Bytes()), "level=DEBUG msg=test")
t.Assert().Contains(string(t.logger.Bytes()), "level=debug test")
}
func (t *BufferLoggerTest) Test_String() {
t.logger.Debug("test")
t.Assert().Contains(t.logger.String(), "level=DEBUG msg=test")
t.Assert().Contains(t.logger.String(), "level=debug test")
}
func (t *BufferLoggerTest) TestRace() {

View File

@ -148,3 +148,8 @@ func (b *LockableBuffer) ReadString(delim byte) (line string, err error) {
b.rw.Lock()
return b.buf.ReadString(delim)
}
// Sync is a no-op.
func (b *LockableBuffer) Sync() error {
return nil
}

View File

@ -21,7 +21,6 @@ import (
"github.com/gin-gonic/gin"
retailcrm "github.com/retailcrm/api-client-go/v2"
v1 "github.com/retailcrm/mg-transport-api-client-go/v1"
"github.com/retailcrm/mg-transport-core/v2/core/config"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
@ -145,12 +144,12 @@ func (u *Utils) GetAPIClient(
if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 {
if len(credentials) == 0 || len(cr.Scopes) > 0 {
u.Logger.Error(url, status, res)
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
}
if res := u.checkScopes(cr.Credentials, credentials[0]); len(res) != 0 {
u.Logger.Error(url, status, res)
u.Logger.Error(url, logger.HTTPStatusCode(status), logger.Body(res))
return nil, http.StatusBadRequest, errorutil.NewInsufficientScopesErr(res)
}
}

View File

@ -38,7 +38,7 @@ func mgClient() *v1.MgClient {
}
func (u *UtilsTest) SetupSuite() {
logger := logger.NewDefaultText()
logger := logger.NewDefault(true)
awsConfig := config.AWS{
AccessKeyID: "access key id (will be removed)",
SecretAccessKey: "secret access key",

2
go.mod
View File

@ -28,6 +28,7 @@ require (
github.com/retailcrm/zabbix-metrics-collector v1.0.0
github.com/stretchr/testify v1.8.3
go.uber.org/atomic v1.10.0
go.uber.org/zap v1.26.0
golang.org/x/text v0.14.0
gopkg.in/gormigrate.v1 v1.6.0
gopkg.in/yaml.v2 v2.4.0
@ -77,6 +78,7 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect

11
go.sum
View File

@ -297,8 +297,9 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
@ -377,8 +378,6 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/retailcrm/api-client-go/v2 v2.1.3 h1:AVcp9oeSOm6+3EWXCgdQs+XE3PTjzCKKB//MUAe0Zb0=
github.com/retailcrm/api-client-go/v2 v2.1.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ=
github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs=
github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
github.com/retailcrm/mg-transport-api-client-go v1.3.4 h1:HIn4eorABNfudn7hr5Rd6XYC/ieDTqCkaq6wv0AFTBE=
github.com/retailcrm/mg-transport-api-client-go v1.3.4/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro=
github.com/retailcrm/zabbix-metrics-collector v1.0.0 h1:ju3rhpgVoiKII6oXEJEf2eoJy5bNcYAmOPRp1oPWDmA=
@ -444,6 +443,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=