From 6ba266ad3ab1d6ea5f09c2869daa8dd502bfdaea Mon Sep 17 00:00:00 2001 From: "mayur.popli" Date: Fri, 21 Mar 2025 12:17:35 +0530 Subject: [PATCH] fix: logging module comments --- pkg/log/log.go | 256 ++++++++++++++++++++++++++++++++++++++ pkg/log/log_test.go | 297 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 553 insertions(+) create mode 100644 pkg/log/log.go create mode 100644 pkg/log/log_test.go diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..6318589 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,256 @@ + package log + + import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "sync" + "time" + + "github.com/rs/zerolog" + "gopkg.in/natefinch/lumberjack.v2" + ) + + type Level string + type DestinationType string + type Destination struct { + Type DestinationType `yaml:"type"` + Config map[string]string `yaml:"config"` + } + + const ( + Stdout DestinationType = "stdout" + File DestinationType = "file" + ) + + const ( + DebugLevel Level = "debug" + InfoLevel Level = "info" + WarnLevel Level = "warn" + ErrorLevel Level = "error" + FatalLevel Level = "fatal" + PanicLevel Level = "panic" + ) + + var logLevels = map[Level]zerolog.Level{ + DebugLevel: zerolog.DebugLevel, + InfoLevel: zerolog.InfoLevel, + WarnLevel: zerolog.WarnLevel, + ErrorLevel: zerolog.ErrorLevel, + FatalLevel: zerolog.FatalLevel, + PanicLevel: zerolog.PanicLevel, + } + + type Config struct { + level Level `yaml:"level"` + destinations []Destination `yaml:"destinations"` + contextKeys []any `yaml:"contextKeys"` + } + + var ( + logger zerolog.Logger + cfg Config + once sync.Once + ) + + var ( + ErrInvalidLogLevel = errors.New("invalid log level") + ErrLogDestinationNil = errors.New("log Destinations cant be empty") + ErrMissingFilePath = errors.New("file path missing in destination config for file logging") + ) + + func (config *Config) validate() error { + if _, exists := logLevels[config.level]; !exists { + return ErrInvalidLogLevel + } + + if len(config.destinations) == 0 { + return ErrLogDestinationNil + } + + for _, dest := range config.destinations { + switch dest.Type { + case Stdout: + case File: + if _, exists := dest.Config["path"]; !exists { + return ErrMissingFilePath + } + + for _, key := range []string{"maxSize", "maxBackups", "maxAge"} { + if valStr, ok := dest.Config[key]; ok { + if _, err := strconv.Atoi(valStr); err != nil { + return fmt.Errorf("invalid %s: %w", key, err) + } + } + } + default: + return fmt.Errorf("invalid destination type '%s'", dest.Type) + } + } + return nil + } + + var defaultConfig = Config{ + level: InfoLevel, + destinations: []Destination{ + {Type: Stdout}, + }, + contextKeys: []any{"userID", "requestID"}, + } + + func init() { + logger, _ = getLogger(defaultConfig) + } + + func getLogger(config Config) (zerolog.Logger, error) { + var newLogger zerolog.Logger + var writers []io.Writer + for _, dest := range config.destinations { + switch dest.Type { + case Stdout: + writers = append(writers, os.Stdout) + case File: + filePath := dest.Config["path"] + fmt.Printf("writing test log to file: %v\n", filePath) + lumberjackLogger := &lumberjack.Logger{ + Filename: filePath, + } + setConfigValue := func(key string, target *int) { + if valStr, ok := dest.Config[key]; ok { + if val, err := strconv.Atoi(valStr); err == nil { + *target = val + } + } + } + setConfigValue("maxSize", &lumberjackLogger.MaxSize) + setConfigValue("maxBackups", &lumberjackLogger.MaxBackups) + setConfigValue("maxAge", &lumberjackLogger.MaxAge) + if compress, ok := dest.Config["compress"]; ok { + lumberjackLogger.Compress = compress == "true" + } + Info(context.Background(),"here") + writers = append(writers, lumberjackLogger) + + } + } + multiwriter := io.MultiWriter(writers...) + newLogger = zerolog.New(multiwriter). + Level(logLevels[config.level]). + With(). + Timestamp(). + Caller(). + Logger() + + cfg = config + return newLogger, nil + } + func InitLogger(c Config) error { + + if err := c.validate(); err != nil { + return err + } + + var initErr error + once.Do(func() { + + logger, initErr = getLogger(c) + }) + return initErr + } + func Debug(ctx context.Context, msg string) { + logEvent(ctx, zerolog.DebugLevel, msg, nil) + } + + func Debugf(ctx context.Context, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.DebugLevel, msg, nil) + } + + func Info(ctx context.Context, msg string) { + logEvent(ctx, zerolog.InfoLevel, msg, nil) + } + + func Infof(ctx context.Context, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.InfoLevel, msg, nil) + } + + func Warn(ctx context.Context, msg string) { + logEvent(ctx, zerolog.WarnLevel, msg, nil) + } + + func Warnf(ctx context.Context, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.WarnLevel, msg, nil) + } + + func Error(ctx context.Context, err error, msg string) { + logEvent(ctx, zerolog.ErrorLevel, msg, err) + } + + func Errorf(ctx context.Context, err error, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.ErrorLevel, msg, err) + } + + func Fatal(ctx context.Context, err error, msg string) { + logEvent(ctx, zerolog.FatalLevel, msg, err) + } + + func Fatalf(ctx context.Context, err error, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.FatalLevel, msg, err) + } + + func Panic(ctx context.Context, err error, msg string) { + logEvent(ctx, zerolog.PanicLevel, msg, err) + } + + func Panicf(ctx context.Context, err error, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.PanicLevel, msg, err) + } + + func logEvent(ctx context.Context, level zerolog.Level, msg string, err error) { + event := logger.WithLevel(level) + + if err != nil { + event = event.Err(err) + } + addCtx(ctx, event) + event.Msg(msg) + } + func Request(ctx context.Context, r *http.Request, body []byte) { + event := logger.Info() + addCtx(ctx, event) + event.Str("method", r.Method). + Str("url", r.URL.String()). + Str("body", string(body)). + Str("remoteAddr", r.RemoteAddr). + Msg("HTTP Request") + } + + func addCtx(ctx context.Context, event *zerolog.Event) { + for _, key := range cfg.contextKeys { + val, ok := ctx.Value(key).(string) + if !ok { + continue + } + keyStr := key.(string) + event.Str(keyStr, val) + } + } + + func Response(ctx context.Context, r *http.Request, statusCode int, responseTime time.Duration) { + event := logger.Info() + addCtx(ctx, event) + event.Str("method", r.Method). + Str("url", r.URL.String()). + Int("statusCode", statusCode). + Dur("responseTime", responseTime). + Msg("HTTP Response") + } diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go new file mode 100644 index 0000000..26c2815 --- /dev/null +++ b/pkg/log/log_test.go @@ -0,0 +1,297 @@ +package log + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/rs/zerolog" +) + +func TestLogFunctions(t *testing.T) { + testConfig := Config{ + level: DebugLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "log/app.txt", + "maxSize": "500", + "maxBackups": "15", + "maxAge": "30", + }, + }, + }, + contextKeys: []any{"userID", "requestID"}, + } + err := InitLogger(testConfig) + if err != nil { + t.Fatalf("Failed to initialize logger: %v", err) + } + + tests := []struct { + name string + logFunc func(ctx context.Context) + expectedOutput string + }{ + { + name: "Debug log with context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Debug(ctx, "debug message") + }, + expectedOutput: `{"level":"debug","requestID":"12345","message":"debug message"}`, + }, + { + name: "Debugf with context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Debugf(ctx, "formatted %s", "debug message") + }, + expectedOutput: `{"level":"debug","requestID":"12345","message":"formatted debug message"}`, + }, + { + name: "Info log with message", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Info(ctx, "info message") + }, + expectedOutput: `{"level":"info","requestID":"12345","message":"info message"}`, + }, + + { + name: "Info log with formatted message", + logFunc: func(ctx context.Context) { + Infof(ctx, "formatted %s", "info message") + }, + expectedOutput: `{"level":"info","message":"formatted info message"}`, + }, + { + name: "Warn log with context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Warn(ctx, "warning message") + }, + expectedOutput: `{"level":"warn","requestID":"12345","message":"warning message"}`, + }, + { + name: "Warnf with context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Warnf(ctx, "formatted %s", "warning message") + }, + expectedOutput: `{"level":"warn","requestID":"12345","message":"formatted warning message"}`, + }, + { + name: "Error log with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var userID ctxKey = "userID" + + ctx = context.WithValue(ctx, userID, "67890") + Error(ctx, errors.New("something went wrong"), "error message") + }, + expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"error message"}`, + }, + { + name: "Errorf with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var userID ctxKey = "userID" + + ctx = context.WithValue(ctx, userID, "67890") + Errorf(ctx, errors.New("something went wrong"), "formatted %s", "error message") + }, + expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"formatted error message"}`, + }, + { + name: "Fatal log with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Fatal(ctx, errors.New("fatal error"), "fatal message") + }, + expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"fatal message"}`, + }, + { + name: "Fatalf with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var requestID ctxKey = "requestID" + + ctx = context.WithValue(ctx, requestID, "12345") + Fatalf(ctx, errors.New("fatal error"), "formatted %s", "fatal message") + }, + expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"formatted fatal message"}`, + }, + { + name: "Panic log with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var userID ctxKey = "userID" + + ctx = context.WithValue(ctx, userID, "67890") + Panic(ctx, errors.New("panic error"), "panic message") + }, + expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"panic message"}`, + }, + { + name: "Panicf with error and context", + logFunc: func(ctx context.Context) { + type ctxKey any + var userID ctxKey = "userID" + + ctx = context.WithValue(ctx, userID, "67890") + Panicf(ctx, errors.New("panic error"), "formatted %s", "panic message") + }, + expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"formatted panic message"}`, + }, + { + name: "Request log", + logFunc: func(ctx context.Context) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.RemoteAddr = "127.0.0.1:8080" + Request(ctx, req, []byte("request body")) + }, + expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","body":"request body","remoteAddr":"127.0.0.1:8080","message":"HTTP Request"}`, + }, + { + name: "Response log", + logFunc: func(ctx context.Context) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + Response(ctx, req, 200, 100*time.Millisecond) + }, + expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","statusCode":200,"responseTime":100,"message":"HTTP Response"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + logger = zerolog.New(&buf).With().Timestamp().Logger() + tt.logFunc(context.Background()) + output := buf.String() + t.Logf("Log output: %s", output) + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) == 0 { + t.Fatal("No log output found") + } + lastLine := lines[len(lines)-1] + var logOutput map[string]interface{} + if err := json.Unmarshal([]byte(lastLine), &logOutput); err != nil { + t.Fatalf("Failed to unmarshal log output: %v", err) + } + delete(logOutput, "time") + delete(logOutput, "caller") + var expectedOutput map[string]interface{} + if err := json.Unmarshal([]byte(tt.expectedOutput), &expectedOutput); err != nil { + t.Fatalf("Failed to unmarshal expected output: %v", err) + } + for key, expectedValue := range expectedOutput { + actualValue, ok := logOutput[key] + if !ok { + t.Errorf("Expected key %q not found in log output", key) + continue + } + if actualValue != expectedValue { + t.Errorf("Mismatch for key %q: expected %v, got %v", key, expectedValue, actualValue) + } + } + }) + } +} +func TestDestinationValidation(t *testing.T) { + tests := []struct { + name string + config Config + expectedError error + }{ + // Missing `path` for File destination + { + name: "Missing file path", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "maxSize": "500", + "maxBackups": "15", + "maxAge": "30", + }, + }, + }, + }, + expectedError: ErrMissingFilePath, + }, + { + name: "Invalid maxAge", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "log/app.txt", + "maxSize": "500", + "maxBackups": "15", + "maxAge": "invalid", + }, + }, + }, + }, + expectedError: errors.New("invalid maxAge"), + }, + { + name: "Valid file destination", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "log/app.txt", + "maxSize": "500", + "maxBackups": "15", + "maxAge": "30", + }, + }, + }, + }, + expectedError: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Print(tt.config) + err := InitLogger(tt.config) + if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) { + t.Errorf("Expected error: %v, got: %v", tt.expectedError, err) + } else if err != nil && tt.expectedError != nil && !strings.Contains(err.Error(), tt.expectedError.Error()) { + t.Errorf("Expected error to contain: %v, got: %v", tt.expectedError, err) + } + }) + } +}