diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..078fcd1 --- /dev/null +++ b/conf.go @@ -0,0 +1,26 @@ +package logger + +import ( + "time" +) + +type Configuration struct { + Stdout struct { + Level string `yaml:"level"` + } `yaml:"console"` + File struct { + Enabled bool `yaml:"enabled"` + Level string `yaml:"level"` + Filename string `yaml:"filename"` + MaxSize int `yaml:"maxsize"` + MaxAge int `yaml:"maxage"` + MaxBackups int `yaml:"maxbackups"` + LocalTime bool `yaml:"localtime"` + Compress bool `yaml:"compress"` + } + Sentry struct { + Level string `yaml:"level"` + DSN string `yaml:"dsn"` + Timeout time.Duration `yaml:"timeout"` + } `yaml:"sentry"` +} diff --git a/conf_test.go b/conf_test.go new file mode 100644 index 0000000..faffc9a --- /dev/null +++ b/conf_test.go @@ -0,0 +1,24 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_buildErrorsFilename(t *testing.T) { + testCases := []struct { + logFileName string + expectedFileName string + }{ + {"logs/logs.log", "logs/logs-errors.log"}, + {"logs/advisor.log", "logs/advisor-errors.log"}, + {"logs/country.log", "logs/country-errors.log"}, + {"logs/country.old.log", "logs/country.old-errors.log"}, + } + + for _, test := range testCases { + t.Run(test.logFileName, func(t *testing.T) { + assert.Equal(t, test.expectedFileName, buildErrorsFilename(test.logFileName), "result") + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..36c1c94 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module go-logger + +go 1.23 + +require ( + github.com/tchap/zapext v1.0.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/getsentry/raven-go v0.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a0f1bb5 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= +github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tchap/zapext v1.0.0 h1:qPxfRLzqYzemT+Pgs5VoH8NGU5YS7cgCnhcqRGkmrXc= +github.com/tchap/zapext v1.0.0/go.mod h1:0VgDSQ0xHJRqkxrwu3G2i2762jSnAJMz7rYxiZGpW1U= +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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..f32c3c8 --- /dev/null +++ b/logger.go @@ -0,0 +1,141 @@ +package logger + +import ( + "fmt" + "log" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/getsentry/raven-go" + "github.com/tchap/zapext/zapsentry" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var zapLogger *zap.Logger + +func Named(name string) *zap.Logger { + return zapLogger.Named(name) +} + +func With(fields ...zap.Field) *zap.Logger { + return zapLogger.With(fields...) +} + +func LogPanic(msg string) { + if r := recover(); r != nil { + buf := make([]byte, 10240) + n := runtime.Stack(buf, false) + err := fmt.Sprintf("%v\n\n%s", r, buf[:n]) + Error(msg, zap.String("error", err)) + time.Sleep(5 * time.Second) // wait to write logs and sent to sentry + } +} + +func Fatal(msg string, fields ...zap.Field) { + zapLogger.Fatal(msg, fields...) +} + +func Error(msg string, fields ...zap.Field) { + zapLogger.Error(msg, fields...) +} + +func Warn(msg string, fields ...zap.Field) { + zapLogger.Warn(msg, fields...) +} + +func Info(msg string, fields ...zap.Field) { + zapLogger.Info(msg, fields...) +} + +func Debug(msg string, fields ...zap.Field) { + zapLogger.Debug(msg, fields...) +} + +func Sync() error { + return zapLogger.Sync() +} + +func buildErrorsFilename(logFileName string) string { + ext := path.Ext(logFileName) + errFileName := strings.TrimSuffix(path.Base(logFileName), ext) + errFileName += "-errors" + ext + return filepath.Join(path.Dir(logFileName), errFileName) +} + +func Init(conf *Configuration) { + cfg := zap.NewProductionEncoderConfig() + cfg.EncodeTime = zapcore.RFC3339NanoTimeEncoder + + cores := []zapcore.Core{ + zapcore.NewCore(zapcore.NewJSONEncoder(cfg), zapcore.Lock(os.Stdout), loggingLevelGTE(conf.Stdout.Level)), + } + + // file logger + if conf.File.Enabled { + loggerConf := conf.File + fileLevelEnabler := loggingLevelGTE(loggerConf.Level) + writer := zapcore.AddSync( + &lumberjack.Logger{ + Filename: loggerConf.Filename, + LocalTime: loggerConf.LocalTime, + MaxSize: loggerConf.MaxSize, + MaxBackups: loggerConf.MaxBackups, + }, + ) + cores = append(cores, zapcore.NewCore(zapcore.NewJSONEncoder(cfg), writer, fileLevelEnabler)) + + redirectStderrToFile(conf) + } + + // sentry + if conf.Sentry.DSN != "" { + loggerConf := conf.Sentry + tags := map[string]string{ + "env": os.Getenv("ENVIRONMENT"), + } + client, err := raven.NewWithTags(loggerConf.DSN, tags) + if err != nil { + log.Fatal("failed to get a Sentry client", err) + } + sentryLevelEnabler := loggingLevelGTE(loggerConf.Level) + cores = append(cores, zapsentry.NewCore(sentryLevelEnabler, client)) + } + + core := zapcore.NewTee(cores...) + + logger := zap.New(core) + defer func() { + _ = logger.Sync() + }() + SetLogger(logger) +} + +type levelComparator func(msgLevel zapcore.Level, loggingLevel zapcore.Level) bool + +func loggingLevelGTE(level string) zap.LevelEnablerFunc { + return loggingLevelEnabledFunc( + level, func(msgLevel zapcore.Level, loggingLevel zapcore.Level) bool { + return msgLevel >= loggingLevel + }, + ) +} + +func loggingLevelEnabledFunc(level string, comparator levelComparator) zap.LevelEnablerFunc { + var loggingLevel zapcore.Level + if err := loggingLevel.UnmarshalText([]byte(level)); err != nil { + log.Fatal("Fail parse console log level", err) + } + return func(msgLevel zapcore.Level) bool { + return comparator(msgLevel, loggingLevel) + } +} + +func SetLogger(logger *zap.Logger) { + zapLogger = logger +} diff --git a/stderrtofile.go b/stderrtofile.go new file mode 100644 index 0000000..2b73093 --- /dev/null +++ b/stderrtofile.go @@ -0,0 +1,20 @@ +//go:build !windows + +package logger + +import ( + "log" + "os" + "syscall" +) + +func redirectStderrToFile(conf *Configuration) { + fileName := buildErrorsFilename(conf.File.Filename) + f, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatalf("open %s: %s", fileName, err) + } + if err = syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd())); err != nil { + log.Fatalf("Failed to redirect stderr to file: %s", err) + } +} diff --git a/stderrtofile_windows.go b/stderrtofile_windows.go new file mode 100644 index 0000000..a0aa86e --- /dev/null +++ b/stderrtofile_windows.go @@ -0,0 +1,6 @@ +//go:build windows + +package logger + +func redirectStderrToFile(conf *Configuration) { +}