package logger import ( "bytes" "encoding/json" "fmt" "io" "os" "strings" "sync" "testing" "time" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // captureOutput captures log output for testing func captureOutput(f func()) string { r, w, err := os.Pipe() if err != nil { panic(err) } // Replace stdout/stderr oldStdout := os.Stdout oldStderr := os.Stderr os.Stdout = w os.Stderr = w // Reset the logger after the test oldLogger := log defer func() { log = oldLogger os.Stdout = oldStdout os.Stderr = oldStderr }() // Create a new logger for testing log = logrus.New() log.SetOutput(w) log.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) // Run the function f() // Close the writer if err := w.Close(); err != nil { panic(fmt.Sprintf("failed to close writer: %v", err)) } // Read the output var buf bytes.Buffer _, err = io.Copy(&buf, r) if err != nil { panic(err) } return buf.String() } func TestLogger_Levels(t *testing.T) { tests := []struct { name string setLevel string logFunc func() expected bool }{ { name: "debug level shows debug logs", setLevel: "debug", logFunc: func() { Debug("test debug") }, expected: true, }, { name: "info level hides debug logs", setLevel: "info", logFunc: func() { Debug("test debug") }, expected: false, }, { name: "error level shows error logs", setLevel: "error", logFunc: func() { Error("test error") }, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{Level: tt.setLevel, Format: "json"}) tt.logFunc() }) if tt.expected { assert.Contains(t, output, "message") } else { assert.Empty(t, output) } }) } } func TestLogger_JSONOutput(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{Level: "debug", Format: "json"}) Info("test message") }) // Verify it's valid JSON var data map[string]interface{} err := json.Unmarshal([]byte(output), &data) require.NoError(t, err) // Check required fields assert.Contains(t, data, "message") assert.Contains(t, data, "level") assert.Contains(t, data, "@timestamp") } func TestLogger_TextOutput(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{Level: "info", Format: "text"}) Warn("test warning") }) // Basic checks for text format assert.Contains(t, output, "test warning") assert.Contains(t, output, "level=warning") } func TestLogger_WithFields(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{Level: "info", Format: "json"}) WithFields(Fields{ "key1": "value1", "key2": 42, }).Info("test fields") }) var data map[string]interface{} err := json.Unmarshal([]byte(output), &data) require.NoError(t, err) assert.Equal(t, "value1", data["key1"]) assert.Equal(t, float64(42), data["key2"]) } func TestLogger_WithError(t *testing.T) { err := io.EOF output := captureOutput(func() { Init(&LogConfig{Level: "error", Format: "json"}) WithError(err).Error("test error") }) var data map[string]interface{} jsonErr := json.Unmarshal([]byte(output), &data) require.NoError(t, jsonErr) assert.Contains(t, data["error"], "EOF") } func TestLogger_CallerInfo(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{ Level: "info", Format: "json", EnableCaller: true, }) Info("test caller") }) var data map[string]interface{} err := json.Unmarshal([]byte(output), &data) require.NoError(t, err) // Caller should be in the format "file:line" caller, ok := data["caller"].(string) require.True(t, ok) assert.True(t, strings.Contains(caller, ".go:"), "caller should contain file and line number") } func TestLogger_Concurrent(t *testing.T) { Init(&LogConfig{Level: "info", Format: "json"}) var wg sync.WaitGroup count := 10 for i := 0; i < count; i++ { wg.Add(1) go func(n int) { defer wg.Done() WithFields(Fields{"goroutine": n}).Info("concurrent log") }(i) } // Just verify no panic occurs wg.Wait() } func TestLogger_DefaultFields(t *testing.T) { // Create a buffer to capture output var buf bytes.Buffer // Initialize logger with test configuration Init(&LogConfig{ Level: "info", Format: "json", EnableCaller: true, }) // Replace the output writer with our buffer log.SetOutput(&buf) // Log a test message Info("test default fields") // Parse the JSON output var data map[string]interface{} err := json.Unmarshal(buf.Bytes(), &data) require.NoError(t, err, "Failed to unmarshal log output") // Check that default fields are included assert.Equal(t, "starter-kit", data["app_name"], "app_name should be set in default fields") // The env field might be empty in test environment, which is fine // as long as the field exists in the log entry _, envExists := data["env"] assert.True(t, envExists, "env field should exist in log entry") } func TestLogger_LevelChanges(t *testing.T) { output := captureOutput(func() { Init(&LogConfig{Level: "error", Format: "json"}) Debug("should not appear") SetLevel("debug") Debug("should appear") }) // Split output into lines lines := strings.Split(strings.TrimSpace(output), "\n") // Should only have one log message (the second Debug) assert.Len(t, lines, 1) assert.Contains(t, lines[0], "should appear") }