diff --git a/log/log.go b/log/log.go deleted file mode 100644 index 001792d..0000000 --- a/log/log.go +++ /dev/null @@ -1,252 +0,0 @@ -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"] - 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" - } - 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) { - fmt.Print("key=====", cfg.contextKeys) - 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/log/log_test.go b/log/log_test.go deleted file mode 100644 index 44e3dfc..0000000 --- a/log/log_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package log - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "net/http" - "strings" - "testing" - "time" - - "github.com/rs/zerolog" -) - -func TestLogFunctions(t *testing.T) { - testConfig := Config{ - level: DebugLevel, - destinations: []Destination{ - {Type: Stdout}, - }, - 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.log", - "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.log", - "maxSize": "500", - "maxBackups": "15", - "maxAge": "30", - }, - }, - }, - }, - expectedError: nil, - }, - // { - // name: "Invalid destination type", - // config: Config{ - // level: InfoLevel, - // destinations: []Destination{ - // { - // Type: "invalid", - // }, - // }, - // }, - // expectedError: errors.New("Invalid destination type 'invalid'"), - // }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - 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) - } - }) - } -}