From c315cd2c77bb893c7a1ab859bf2649bc2033c30f Mon Sep 17 00:00:00 2001 From: "mayur.popli" Date: Tue, 18 Mar 2025 11:05:15 +0530 Subject: [PATCH] feat: logging added --- log/log.go | 279 +++++++++++++++++++---------- log/log.yaml | 10 +- log/log_test.go | 273 ++++++++++++++++++++++++++++ log/logpackage_test.go | 391 ----------------------------------------- 4 files changed, 468 insertions(+), 485 deletions(-) create mode 100644 log/log_test.go delete mode 100644 log/logpackage_test.go diff --git a/log/log.go b/log/log.go index 070ba76..37511ea 100644 --- a/log/log.go +++ b/log/log.go @@ -1,150 +1,251 @@ -package logpackage +package log import ( "context" + "errors" "fmt" + "io" + "net/http" "os" - "path/filepath" - "runtime" + "strconv" "sync" "time" "github.com/rs/zerolog" "gopkg.in/natefinch/lumberjack.v2" - "gopkg.in/yaml.v2" + // "gopkg.in/yaml.v2" ) -type LoggerConfig struct { - Level string `yaml:"level"` - FilePath string `yaml:"file_path"` - MaxSize int `yaml:"max_size"` - MaxBackups int `yaml:"max_backups"` - MaxAge int `yaml:"max_age"` - ContextKeys []string `yaml:"context_keys"` +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 []string `yaml:"contextKeys"` } var ( logger zerolog.Logger - cfg LoggerConfig + cfg Config once sync.Once - - getConfigPath = func() (string, error) { - _, file, _, ok := runtime.Caller(0) - if !ok { - return "", fmt.Errorf("failed to get runtime caller") - } - dir := filepath.Dir(file) - return filepath.Join(dir, "log.yaml"), nil - } ) -func loadConfig() (LoggerConfig, error) { - var config LoggerConfig +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") +) - configPath, err := getConfigPath() - if err != nil { - return config, fmt.Errorf("error finding config path: %w", err) +func (config *Config) validate() error { + if _, exists := logLevels[config.level]; !exists { + return ErrInvalidLogLevel } - data, err := os.ReadFile(configPath) - if err != nil { - return config, fmt.Errorf("failed to read config file: %w", err) + if len(config.destinations) == 0 { + return ErrLogDestinationNil } - err = yaml.Unmarshal(data, &config) - if err != nil { - return config, fmt.Errorf("failed to parse YAML: %w", err) - } - - return config, nil -} - -func InitLogger(configs ...LoggerConfig) { - once.Do(func() { - var err error - - if len(configs) > 0 { - cfg = configs[0] - } else { - cfg, err = loadConfig() - if err != nil { - fmt.Println("Logger initialization failed:", err) - return + 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) } - - level, err := zerolog.ParseLevel(cfg.Level) - if err != nil { - level = zerolog.InfoLevel - } - - zerolog.SetGlobalLevel(level) - zerolog.TimeFieldFormat = time.RFC3339 - - logWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} - fileWriter := &lumberjack.Logger{ - Filename: cfg.FilePath, - MaxSize: cfg.MaxSize, - MaxBackups: cfg.MaxBackups, - MaxAge: cfg.MaxAge, - } - - multi := zerolog.MultiLevelWriter(logWriter, fileWriter) - logger = zerolog.New(multi).With().Timestamp().Logger() - }) + } + return nil } +var defaultConfig = Config{ + level: InfoLevel, + destinations: []Destination{ + {Type: Stdout}, + }, + contextKeys: []string{"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, logger.Debug(), msg) + logEvent(ctx, zerolog.DebugLevel, msg, nil) } func Debugf(ctx context.Context, format string, v ...any) { - logEvent(ctx, logger.Debug(), fmt.Sprintf(format, v...)) + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.DebugLevel, msg, nil) } func Info(ctx context.Context, msg string) { - logEvent(ctx, logger.Info(), msg) + logEvent(ctx, zerolog.InfoLevel, msg, nil) } func Infof(ctx context.Context, format string, v ...any) { - logEvent(ctx, logger.Info(), fmt.Sprintf(format, v...)) + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.InfoLevel, msg, nil) } func Warn(ctx context.Context, msg string) { - logEvent(ctx, logger.Warn(), msg) + logEvent(ctx, zerolog.WarnLevel, msg, nil) } func Warnf(ctx context.Context, format string, v ...any) { - logEvent(ctx, logger.Warn(), fmt.Sprintf(format, v...)) + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.WarnLevel, msg, nil) } func Error(ctx context.Context, err error, msg string) { - logEvent(ctx, logger.Error().Err(err), msg) + logEvent(ctx, zerolog.ErrorLevel, msg, err) } func Errorf(ctx context.Context, err error, format string, v ...any) { - logEvent(ctx, logger.Error().Err(err), fmt.Sprintf(format, v...)) + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.ErrorLevel, msg, err) } -var ExitFunc = func(code int) { - os.Exit(code) +func Fatal(ctx context.Context, err error, msg string) { + logEvent(ctx, zerolog.FatalLevel, msg, err) } -func Fatal(ctx context.Context, msg string) { - logEvent(ctx, logger.Error(), msg) - ExitFunc(1) +func Fatalf(ctx context.Context, err error, format string, v ...any) { + msg := fmt.Sprintf(format, v...) + logEvent(ctx, zerolog.FatalLevel, msg, err) } -func Fatalf(ctx context.Context, format string, v ...any) { - logEvent(ctx, logger.Fatal(), fmt.Sprintf(format, v...)) - ExitFunc(1) +func Panic(ctx context.Context, err error, msg string) { + logEvent(ctx, zerolog.PanicLevel, msg, err) } -func logEvent(ctx context.Context, event *zerolog.Event, msg string) { - for _, key := range cfg.ContextKeys { - if val, ok := ctx.Value(key).(string); ok && val != "" { - event.Str(key, val) - } +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 + } + event.Str(key, 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.yaml b/log/log.yaml index ba00e7e..ea47e34 100644 --- a/log/log.yaml +++ b/log/log.yaml @@ -3,10 +3,10 @@ destinations: - type: stdout - type: file config: - path: logs/app.log - maxSize: "500" + path: my_log_file.log + maxSize: "500" maxBackups: "15" maxAge: "30" -context_keys: - - requestId - - userId \ No newline at end of file +contextKeys: + - requestID + - userID diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..8f396ac --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,273 @@ +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: []string{"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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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"), + }, + + // Valid File destination (no error expected) + { + 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, + }, + + // Invalid destination type + { + 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) + } + }) + } +} diff --git a/log/logpackage_test.go b/log/logpackage_test.go deleted file mode 100644 index e301b70..0000000 --- a/log/logpackage_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package logpackage - -import ( - "context" - "errors" - "os" - "path/filepath" - "strings" - "sync" - "testing" - - "github.com/rs/zerolog" -) - -func setupTest(t *testing.T) string { - once = sync.Once{} - - tempDir, err := os.MkdirTemp("", "logger-test") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - - return tempDir -} - -func TestInitLoggerWithValidConfig(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "test.log") - - InitLogger(LoggerConfig{ - Level: "debug", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id", "user_id"}, - }) - - if logger.GetLevel() == zerolog.Disabled { - t.Error("Logger was not initialized") - } - - ctx := context.WithValue(context.Background(), "request_id", "test-123") - Debug(ctx, "debug message") - Info(ctx, "info message") - Warn(ctx, "warning message") - Error(ctx, errors.New("test error"), "error message") - - if _, err := os.Stat(testLogPath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", testLogPath) - } -} - -func TestInitLoggerWithInvalidLevel(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "test.log") - - InitLogger(LoggerConfig{ - Level: "invalid_level", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id"}, - }) - - if logger.GetLevel() == zerolog.Disabled { - t.Error("Logger was not initialized") - } - - ctx := context.WithValue(context.Background(), "request_id", "test-123") - Info(ctx, "info message") - - if _, err := os.Stat(testLogPath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", testLogPath) - } -} - -func TestLoadConfigFromFile(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - configPath := filepath.Join(tempDir, "log.yaml") - configContent := `level: debug -file_path: /tmp/test.log -max_size: 10 -max_backups: 3 -max_age: 7 -context_keys: - - request_id - - user_id` - - err := os.WriteFile(configPath, []byte(configContent), 0644) - if err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - originalGetConfigPath := getConfigPath - defer func() { getConfigPath = originalGetConfigPath }() - getConfigPath = func() (string, error) { - return configPath, nil - } - - config, err := loadConfig() - if err != nil { - t.Errorf("loadConfig() error = %v", err) - } - - if config.Level != "debug" { - t.Errorf("Expected level 'debug', got '%s'", config.Level) - } - if config.FilePath != "/tmp/test.log" { - t.Errorf("Expected file_path '/tmp/test.log', got '%s'", config.FilePath) - } - if len(config.ContextKeys) != 2 { - t.Errorf("Expected 2 context keys, got %d", len(config.ContextKeys)) - } -} - -func TestLoadConfigNonExistent(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - configPath := filepath.Join(tempDir, "non-existent.yaml") - - originalGetConfigPath := getConfigPath - defer func() { getConfigPath = originalGetConfigPath }() - getConfigPath = func() (string, error) { - return configPath, nil - } - - _, err := loadConfig() - if err == nil { - t.Error("Expected error for non-existent config file, got nil") - } -} - -func TestLoadConfigInvalidYAML(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - configPath := filepath.Join(tempDir, "invalid.yaml") - configContent := `level: debug -file_path: /tmp/test.log -max_size: invalid -max_backups: 3` - - err := os.WriteFile(configPath, []byte(configContent), 0644) - if err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - originalGetConfigPath := getConfigPath - defer func() { getConfigPath = originalGetConfigPath }() - getConfigPath = func() (string, error) { - return configPath, nil - } - - _, err = loadConfig() - if err == nil { - t.Error("Expected error for invalid YAML, got nil") - } -} - -func TestFatal(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "fatal.log") - - originalExitFunc := ExitFunc - defer func() { ExitFunc = originalExitFunc }() - - var exitCalled bool - var exitCode int - ExitFunc = func(code int) { - exitCalled = true - exitCode = code - - } - - InitLogger(LoggerConfig{ - Level: "debug", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id"}, - }) - - ctx := context.WithValue(context.Background(), "request_id", "test-fatal") - Fatal(ctx, "fatal message") - - if !exitCalled { - t.Error("ExitFunc was not called") - } - if exitCode != 1 { - t.Errorf("Expected exit code 1, got %d", exitCode) - } - - if _, err := os.Stat(testLogPath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", testLogPath) - } - - content, err := os.ReadFile(testLogPath) - if err != nil { - t.Errorf("Failed to read log file: %v", err) - } - if !strings.Contains(string(content), "fatal message") { - t.Error("Log file does not contain fatal message") - } -} - -func TestLoggingWithContext(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "context.log") - - // Initialize logger with context keys - InitLogger(LoggerConfig{ - Level: "debug", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id", "user_id", "session_id"}, - }) - - ctx := context.Background() - ctx = context.WithValue(ctx, "request_id", "req-123") - ctx = context.WithValue(ctx, "user_id", "user-456") - Info(ctx, "message with context") - - if _, err := os.Stat(testLogPath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", testLogPath) - } - - content, err := os.ReadFile(testLogPath) - if err != nil { - t.Errorf("Failed to read log file: %v", err) - } - - contentStr := string(content) - if !strings.Contains(contentStr, "req-123") { - t.Error("Log file does not contain request_id") - } - if !strings.Contains(contentStr, "user-456") { - t.Error("Log file does not contain user_id") - } -} - -func TestFormattedLogging(t *testing.T) { - once = sync.Once{} - - tempDir, err := os.MkdirTemp("", "logger-format-test") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "formatted.log") - - InitLogger(LoggerConfig{ - Level: "debug", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id"}, - }) - - ctx := context.WithValue(context.Background(), "request_id", "format-test-123") - - testValues := []struct { - name string - number int - text string - expected string - }{ - { - name: "debug", - number: 42, - text: "formatted debug", - expected: "formatted debug message #42", - }, - { - name: "info", - number: 100, - text: "formatted info", - expected: "formatted info message #100", - }, - { - name: "warn", - number: 200, - text: "formatted warning", - expected: "formatted warning message #200", - }, - { - name: "error", - number: 500, - text: "formatted error", - expected: "formatted error message #500", - }, - } - - for _, tv := range testValues { - t.Run(tv.name+"f", func(t *testing.T) { - format := "%s message #%d" - - switch tv.name { - case "debug": - Debugf(ctx, format, tv.text, tv.number) - case "info": - Infof(ctx, format, tv.text, tv.number) - case "warn": - Warnf(ctx, format, tv.text, tv.number) - case "error": - testErr := errors.New("test error") - Errorf(ctx, testErr, format, tv.text, tv.number) - } - }) - } -} - -func TestLoggingWithNonStringContext(t *testing.T) { - tempDir := setupTest(t) - defer os.RemoveAll(tempDir) - - testLogPath := filepath.Join(tempDir, "non-string.log") - - InitLogger(LoggerConfig{ - Level: "debug", - FilePath: testLogPath, - MaxSize: 10, - MaxBackups: 3, - MaxAge: 7, - ContextKeys: []string{"request_id", "count"}, - }) - - ctx := context.Background() - ctx = context.WithValue(ctx, "request_id", "req-123") - ctx = context.WithValue(ctx, "count", 42) - - Info(ctx, "message with non-string context") - - if _, err := os.Stat(testLogPath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", testLogPath) - } - - content, err := os.ReadFile(testLogPath) - if err != nil { - t.Errorf("Failed to read log file: %v", err) - } - - contentStr := string(content) - if !strings.Contains(contentStr, "req-123") { - t.Error("Log file does not contain request_id") - } - -} - -func TestGetConfigPath(t *testing.T) { - path, err := getConfigPath() - if err != nil { - t.Errorf("getConfigPath() error = %v", err) - } - if path == "" { - t.Error("getConfigPath() returned empty path") - } -} - -func TestGetConfigPathError(t *testing.T) { - - originalGetConfigPath := getConfigPath - defer func() { getConfigPath = originalGetConfigPath }() - - expectedErr := errors.New("runtime caller error") - getConfigPath = func() (string, error) { - return "", expectedErr - } - - once = sync.Once{} - InitLogger() - ctx := context.Background() - Info(ctx, "info after config failure") -} \ No newline at end of file