diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 53d4a3f..04733b0 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.0" + ".": "0.13.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e4e6b..d50a0d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.13.0](https://github.com/vitalvas/gokit/compare/v0.12.0...v0.13.0) (2024-09-30) + + +### Features + +* add xlogger ([eecec7d](https://github.com/vitalvas/gokit/commit/eecec7d4790b7c53b74dc14da95e52bcc982644b)) + ## [0.12.0](https://github.com/vitalvas/gokit/compare/v0.11.0...v0.12.0) (2024-09-10) diff --git a/go.mod b/go.mod index 223841b..d4eb89e 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,13 @@ module github.com/vitalvas/gokit go 1.22 -require golang.org/x/sys v0.24.0 +require ( + github.com/stretchr/testify v1.9.0 + golang.org/x/sys v0.24.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index d88e7bd..8487a78 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ +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/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= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/xlogger/logger.go b/xlogger/logger.go new file mode 100644 index 0000000..7becff3 --- /dev/null +++ b/xlogger/logger.go @@ -0,0 +1,77 @@ +package xlogger + +import ( + "fmt" + "log/slog" + "os" + "strings" +) + +type Config struct { + Level string + LogType string + AddSource bool + SourcePath string +} + +func New(conf Config) *slog.Logger { + opts := &slog.HandlerOptions{ + AddSource: conf.AddSource, + Level: getLogLevel(conf.Level), + ReplaceAttr: replaceAttr(conf), + } + + handler := getHandler(conf.LogType, opts) + + return slog.New(handler) + +} + +func getLogLevel(logLevel string) slog.Level { + switch strings.ToLower(logLevel) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func getHandler(logType string, opts *slog.HandlerOptions) slog.Handler { + switch strings.ToLower(logType) { + case "json": + return slog.NewJSONHandler(os.Stdout, opts) + + default: + return slog.NewTextHandler(os.Stdout, opts) + } +} + +func replaceAttr(conf Config) func(groups []string, a slog.Attr) slog.Attr { + return func(_ []string, attr slog.Attr) slog.Attr { + if attr.Key == slog.SourceKey { + if source, ok := attr.Value.Any().(*slog.Source); ok && source != nil { + sourceFile := fmt.Sprintf("%s:%d", source.File, source.Line) + + if len(conf.SourcePath) > 0 { + if strings.HasPrefix(source.File, conf.SourcePath) { + sourceFile = fmt.Sprintf("%s:%d", strings.TrimPrefix(source.File, conf.SourcePath), source.Line) + + } else if index := strings.Index(source.File, conf.SourcePath); index > 0 { + sourceFileSuffix := source.File[index+len(conf.SourcePath):] + sourceFile = fmt.Sprintf("%s:%d", sourceFileSuffix, source.Line) + } + } + + return slog.String(slog.SourceKey, sourceFile) + } + } + + return attr + } +} diff --git a/xlogger/logger_test.go b/xlogger/logger_test.go new file mode 100644 index 0000000..995c6e0 --- /dev/null +++ b/xlogger/logger_test.go @@ -0,0 +1,255 @@ +package xlogger + +import ( + "log/slog" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + conf Config + expected slog.Handler + }{ + { + name: "Create logger with JSON handler and debug level", + conf: Config{ + Level: "debug", + LogType: "json", + AddSource: true, + }, + expected: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), + }, + { + name: "Create logger with text handler and info level", + conf: Config{ + Level: "info", + LogType: "text", + AddSource: false, + }, + expected: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelInfo, + }), + }, + { + name: "Create logger with default handler for unknown log type", + conf: Config{ + Level: "warn", + LogType: "unknown", + AddSource: true, + }, + expected: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelWarn, + }), + }, + { + name: "Create logger with error level and source path replacement", + conf: Config{ + Level: "error", + LogType: "json", + AddSource: true, + SourcePath: "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/", + }, + expected: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelError, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := New(tt.conf) + assert.NotNil(t, logger) + + handler := logger.Handler() + assert.NotNil(t, handler) + assert.IsType(t, tt.expected, handler) + }) + } +} + +func TestGetLogLevel(t *testing.T) { + tests := []struct { + name string + logLevel string + expected slog.Level + }{ + { + name: "Debug level", + logLevel: "debug", + expected: slog.LevelDebug, + }, + { + name: "Info level", + logLevel: "info", + expected: slog.LevelInfo, + }, + { + name: "Warn level", + logLevel: "warn", + expected: slog.LevelWarn, + }, + { + name: "Error level", + logLevel: "error", + expected: slog.LevelError, + }, + { + name: "Default level for unknown input", + logLevel: "unknown", + expected: slog.LevelInfo, + }, + { + name: "Case insensitive level", + logLevel: "DEBUG", + expected: slog.LevelDebug, + }, + { + name: "Empty level string", + logLevel: "", + expected: slog.LevelInfo, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getLogLevel(tt.logLevel) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetHandler(t *testing.T) { + tests := []struct { + name string + logType string + opts *slog.HandlerOptions + expected slog.Handler + }{ + { + name: "Get JSON handler", + logType: "json", + opts: &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }, + expected: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }), + }, + { + name: "Get text handler", + logType: "text", + opts: &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }, + expected: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }), + }, + { + name: "Get default handler for unknown type", + logType: "unknown", + opts: &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }, + expected: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := getHandler(tt.logType, tt.opts) + assert.NotNil(t, handler) + assert.IsType(t, tt.expected, handler) + }) + } +} + +func TestReplaceAttr(t *testing.T) { + tests := []struct { + name string + conf Config + attr slog.Attr + expected slog.Attr + }{ + { + name: "Replace source attribute with source path", + conf: Config{ + SourcePath: "github.com/vitalvas/gokit/", + }, + attr: slog.Attr{ + Key: slog.SourceKey, + Value: slog.AnyValue(&slog.Source{ + File: "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/xlogger/logger.go", + Line: 42, + }), + }, + expected: slog.String("source", "xlogger/logger.go:42"), + }, + { + name: "Replace source attribute with full source path", + conf: Config{ + SourcePath: "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/", + }, + attr: slog.Attr{ + Key: slog.SourceKey, + Value: slog.AnyValue(&slog.Source{ + File: "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/xlogger/logger.go", + Line: 42, + }), + }, + expected: slog.String("source", "xlogger/logger.go:42"), + }, + { + name: "Replace source attribute without source path", + conf: Config{}, + attr: slog.Attr{ + Key: slog.SourceKey, + Value: slog.AnyValue(&slog.Source{ + File: "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/xlogger/logger.go", + Line: 42, + }), + }, + expected: slog.String("source", "/Users/vitalvas/workspace/go/src/github.com/vitalvas/gokit/xlogger/logger.go:42"), + }, + { + name: "Non-source attribute remains unchanged", + conf: Config{}, + attr: slog.Attr{ + Key: "non-source", + Value: slog.StringValue("test"), + }, + expected: slog.Attr{ + Key: "non-source", + Value: slog.StringValue("test"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + replaceFunc := replaceAttr(tt.conf) + assert.NotNil(t, replaceFunc) + + result := replaceFunc(nil, tt.attr) + assert.Equal(t, tt.expected, result) + }) + } +}