diff --git a/.gitignore b/.gitignore index 09ec337..c841ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,4 @@ bin #env *.env - - +logs/ diff --git a/go.mod b/go.mod index ea6120e..dcf26ac 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.gocasts.ir/ebhomengo/niki go 1.21.3 + +require gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..96b0a10 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +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= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..b9a1288 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,58 @@ +package logger + +import ( + "io" + "log/slog" + "os" + + "gopkg.in/natefinch/lumberjack.v2" +) + +const ( + defaultFilePath = "logs/logs.json" + defaultUseLocalTime = false + defaultFileMaxSizeInMB = 10 + defaultFileAgeInDays = 30 +) + +type Config struct { + FilePath string + UseLocalTime bool + FileMaxSizeInMB int + FileMaxAgeInDays int +} + +var l *slog.Logger + +// init is default logger and Singleton that lets you ensure that a logger has only one instance, while providing a global access point to this instance. +func init() { + fileWriter := &lumberjack.Logger{ + Filename: defaultFilePath, + LocalTime: defaultUseLocalTime, + MaxSize: defaultFileMaxSizeInMB, + MaxAge: defaultFileAgeInDays, + } + l = slog.New( + slog.NewJSONHandler(io.MultiWriter(fileWriter, os.Stdout), &slog.HandlerOptions{}), + ) +} + +func L() *slog.Logger { + return l +} + +// New is constructor logger with special settings +func New(cfg Config, opt *slog.HandlerOptions) *slog.Logger { + fileWriter := &lumberjack.Logger{ + Filename: cfg.FilePath, + LocalTime: cfg.UseLocalTime, + MaxSize: cfg.FileMaxSizeInMB, + MaxAge: cfg.FileMaxAgeInDays, + } + + logger := slog.New( + slog.NewJSONHandler(io.MultiWriter(fileWriter, os.Stdout), opt), + ) + + return logger +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..9b4dac0 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,97 @@ +package logger_test + +import ( + "bufio" + "fmt" + "log/slog" + "os" + "strconv" + "testing" + + "git.gocasts.ir/ebhomengo/niki/logger" +) + +func TestLogger(t *testing.T) { + cfg := logger.Config{ + FilePath: "./logs.json", + UseLocalTime: false, + FileMaxSizeInMB: 10, + FileMaxAgeInDays: 1, + } + opt := slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + // remove time because it makes test wrong `time is so fast :)` + return slog.Attr{} + } + + return a + }, + } + + l := logger.New(cfg, &opt) + tests := []struct { + f func() + want string + }{ + { + f: func() { + l.Info("INFO", "key", "value") + }, + want: `{"level":"INFO","msg":"INFO","key":"value"}`, + }, + { + f: func() { + l.Warn("WARN", "key", "value") + }, + want: `{"level":"WARN","msg":"WARN","key":"value"}`, + }, + { + f: func() { + l.Error("ERROR", "key", "value") + }, + want: `{"level":"ERROR","msg":"ERROR","key":"value"}`, + }, + { + f: func() { + l.With( + slog.Group("user", + slog.String("id", "user-123"), + ), + ). + With("environment", "dev"). + With("error", fmt.Errorf("an error")). + Error("A message") + }, + want: `{"level":"ERROR","msg":"A message","user":{"id":"user-123"},"environment":"dev","error":"an error"}`, + }, + } + + // first run logs + for _, test := range tests { + test.f() + } + + f, err := os.Open(cfg.FilePath) + defer f.Close() + defer os.Remove(cfg.FilePath) + if err != nil { + t.Fatalf("can't open file: %s", err) + } + + scanner := bufio.NewScanner(f) + + var logs []string + + for scanner.Scan() { + logs = append(logs, scanner.Text()) + } + + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if test.want != logs[i] { + t.Fatalf("want: %+v, got: %+v", test.want, logs[i]) + } + }) + } +}