| package log
|
|
|
| import (
|
| "context"
|
| "encoding/json"
|
| "log/slog"
|
| "testing"
|
| "time"
|
|
|
| "github.com/stretchr/testify/assert"
|
| "github.com/stretchr/testify/require"
|
| "go.uber.org/zap"
|
| "go.uber.org/zap/zapcore"
|
| )
|
|
|
| type testLogOutput struct {
|
| output []map[string]any
|
| }
|
|
|
| func (t *testLogOutput) Write(p []byte) (n int, err error) {
|
| var entry map[string]any
|
| if err := json.Unmarshal(p, &entry); err != nil {
|
| return 0, err
|
| }
|
|
|
| t.output = append(t.output, entry)
|
|
|
| return len(p), nil
|
| }
|
|
|
| func (t *testLogOutput) String() string {
|
| return ""
|
| }
|
|
|
| func (t *testLogOutput) Sync() error {
|
| return nil
|
| }
|
|
|
| func (t *testLogOutput) Close() error {
|
| return nil
|
| }
|
|
|
| func (t *testLogOutput) Clear() {
|
| t.output = nil
|
| }
|
|
|
| func (t *testLogOutput) GetOutput() []map[string]any {
|
| return t.output
|
| }
|
|
|
| func createTestLogger(level Level) (*Logger, *testLogOutput) {
|
| output := &testLogOutput{}
|
|
|
| config := Config{
|
| Name: "test",
|
| Level: level,
|
| Encoding: "json",
|
| Debug: false,
|
| SkipLevel: 0,
|
| LevelKey: "level",
|
| TimeKey: "time",
|
| CallerKey: "caller",
|
| NameKey: "logger",
|
| }
|
|
|
| logger := New(config)
|
|
|
|
|
| zapLogger := logger.logger
|
| newLogger := zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
| return zapcore.NewCore(
|
| zapcore.NewJSONEncoder(zapcore.EncoderConfig{
|
| MessageKey: "message",
|
| LevelKey: "level",
|
| TimeKey: "time",
|
| NameKey: "logger",
|
| CallerKey: "caller",
|
| StacktraceKey: "stacktrace",
|
| SkipLineEnding: false,
|
| LineEnding: zapcore.DefaultLineEnding,
|
| EncodeLevel: zapcore.CapitalLevelEncoder,
|
| EncodeTime: zapcore.RFC3339TimeEncoder,
|
| EncodeDuration: zapcore.SecondsDurationEncoder,
|
| EncodeCaller: zapcore.ShortCallerEncoder,
|
| }),
|
| zapcore.AddSync(output),
|
| zap.NewAtomicLevelAt(zapcore.DebugLevel),
|
| )
|
| }))
|
|
|
| logger.logger = newLogger
|
|
|
| return logger, output
|
| }
|
|
|
| func TestSlogAdapter_BasicLogging(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Debug("debug message")
|
| slogLogger.Info("info message")
|
| slogLogger.Warn("warn message")
|
| slogLogger.Error("error message")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 4, "Should have 4 log entries")
|
|
|
|
|
| assert.Equal(t, "debug message", logs[0]["message"])
|
| assert.Equal(t, "DEBUG", logs[0]["level"])
|
|
|
|
|
| assert.Equal(t, "info message", logs[1]["message"])
|
| assert.Equal(t, "INFO", logs[1]["level"])
|
|
|
|
|
| assert.Equal(t, "warn message", logs[2]["message"])
|
| assert.Equal(t, "WARN", logs[2]["level"])
|
|
|
|
|
| assert.Equal(t, "error message", logs[3]["message"])
|
| assert.Equal(t, "ERROR", logs[3]["level"])
|
| }
|
|
|
| func TestSlogAdapter_LevelFiltering(t *testing.T) {
|
| logger, output := createTestLogger(InfoLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Debug("debug message - should be filtered")
|
| slogLogger.Info("info message - should appear")
|
| slogLogger.Warn("warn message - should appear")
|
| slogLogger.Error("error message - should appear")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 3, "Should have 3 log entries (debug filtered out)")
|
|
|
|
|
| for _, log := range logs {
|
| assert.NotContains(t, log["message"], "debug message - should be filtered")
|
| }
|
|
|
|
|
| assert.Equal(t, "info message - should appear", logs[0]["message"])
|
| assert.Equal(t, "warn message - should appear", logs[1]["message"])
|
| assert.Equal(t, "error message - should appear", logs[2]["message"])
|
| }
|
|
|
| func TestSlogAdapter_WithAttributes(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Info("message with attributes",
|
| slog.String("key1", "value1"),
|
| slog.Int("key2", 42),
|
| slog.Bool("key3", true),
|
| )
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "message with attributes", log["message"])
|
| assert.Equal(t, "value1", log["key1"])
|
| assert.Equal(t, float64(42), log["key2"])
|
| assert.Equal(t, true, log["key3"])
|
| }
|
|
|
| func TestSlogAdapter_WithGroups(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Info("message with groups",
|
| slog.Group("request",
|
| slog.String("method", "GET"),
|
| slog.String("path", "/api/test"),
|
| ),
|
| slog.Group("response",
|
| slog.Int("status", 200),
|
| slog.Int("size", 1024),
|
| ),
|
| )
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "message with groups", log["message"])
|
|
|
|
|
| if requestGroup, ok := log["request"].(map[string]any); ok {
|
| assert.Equal(t, "GET", requestGroup["method"])
|
| assert.Equal(t, "/api/test", requestGroup["path"])
|
| } else {
|
| t.Errorf("Expected 'request' to be a map, got: %T", log["request"])
|
| }
|
|
|
| if responseGroup, ok := log["response"].(map[string]any); ok {
|
| assert.Equal(t, float64(200), responseGroup["status"])
|
| assert.Equal(t, float64(1024), responseGroup["size"])
|
| } else {
|
| t.Errorf("Expected 'response' to be a map, got: %T", log["response"])
|
| }
|
| }
|
|
|
| func TestSlogAdapter_WithHandler(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
|
|
|
|
| slogLogger := logger.AsSlog()
|
| slogLoggerWithAttrs := slogLogger.With(
|
| slog.String("service", "test-service"),
|
| slog.String("version", "1.0.0"),
|
| )
|
|
|
| slogLoggerWithAttrs.Info("message with handler attributes")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "message with handler attributes", log["message"])
|
| assert.Equal(t, "test-service", log["service"])
|
| assert.Equal(t, "1.0.0", log["version"])
|
| }
|
|
|
| func TestSlogAdapter_WithGroupsHandler(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
|
|
|
|
| slogLogger := logger.AsSlog()
|
| slogLoggerWithGroup := slogLogger.WithGroup("http")
|
|
|
| slogLoggerWithGroup.Info("message with group",
|
| slog.String("method", "POST"),
|
| slog.String("path", "/api/create"),
|
| )
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "message with group", log["message"])
|
| assert.Equal(t, "POST", log["http.method"])
|
| assert.Equal(t, "/api/create", log["http.path"])
|
| }
|
|
|
| func TestSlogAdapter_ContextLevelForcing(t *testing.T) {
|
| logger, output := createTestLogger(InfoLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Debug("debug message - should be filtered")
|
| slogLogger.Info("info message - should appear")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry (debug filtered out)")
|
| assert.Equal(t, "info message - should appear", logs[0]["message"])
|
|
|
|
|
| output.Clear()
|
|
|
| forcedCtx := ForceEnableLevel(context.Background(), DebugLevel)
|
|
|
|
|
| enabled := slogLogger.Enabled(forcedCtx, slog.LevelDebug)
|
| require.True(t, enabled, "Debug should be enabled with forced context")
|
|
|
| slogLogger.DebugContext(forcedCtx, "debug message - should appear with force")
|
| slogLogger.InfoContext(forcedCtx, "info message - should also appear")
|
|
|
| logs = output.GetOutput()
|
| require.Len(t, logs, 2, "Should have 2 log entries (debug forced)")
|
| assert.Equal(t, "debug message - should appear with force", logs[0]["message"])
|
| assert.Equal(t, "info message - should also appear", logs[1]["message"])
|
| }
|
|
|
| func TestSlogAdapter_EnabledCheck(t *testing.T) {
|
| logger, _ := createTestLogger(InfoLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| assert.False(t, slogLogger.Enabled(context.Background(), slog.LevelDebug), "Debug should be disabled")
|
| assert.True(t, slogLogger.Enabled(context.Background(), slog.LevelInfo), "Info should be enabled")
|
| assert.True(t, slogLogger.Enabled(context.Background(), slog.LevelWarn), "Warn should be enabled")
|
| assert.True(t, slogLogger.Enabled(context.Background(), slog.LevelError), "Error should be enabled")
|
|
|
|
|
| forcedCtx := ForceEnableLevel(context.Background(), DebugLevel)
|
| assert.True(t, slogLogger.Enabled(forcedCtx, slog.LevelDebug), "Debug should be enabled with force")
|
| }
|
|
|
| func TestSlogAdapter_TimeField(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
| startTime := time.Now()
|
|
|
| slogLogger.Info("message with time")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Contains(t, log, "time", "Log should contain time field")
|
|
|
|
|
| timeStr, ok := log["time"].(string)
|
| require.True(t, ok, "Time should be a string")
|
|
|
| parsedTime, err := time.Parse(time.RFC3339, timeStr)
|
| require.NoError(t, err, "Time should be in RFC3339 format")
|
|
|
|
|
| assert.WithinDuration(t, startTime, parsedTime, time.Second, "Log time should be recent")
|
| }
|
|
|
| func TestSlogAdapter_AllAttributeTypes(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Info("message with all types",
|
| slog.String("string", "test"),
|
| slog.Int64("int64", 123456789),
|
| slog.Uint64("uint64", 987654321),
|
| slog.Float64("float64", 3.14159),
|
| slog.Bool("bool", true),
|
| slog.Duration("duration", 5*time.Second),
|
| slog.Time("time", time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),
|
| slog.Any("any", map[string]any{"nested": "value"}),
|
| )
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "test", log["string"])
|
| assert.Equal(t, float64(123456789), log["int64"])
|
| assert.Equal(t, float64(987654321), log["uint64"])
|
| assert.Equal(t, 3.14159, log["float64"])
|
| assert.Equal(t, true, log["bool"])
|
| assert.Equal(t, float64(5), log["duration"])
|
| assert.Equal(t, "2023-01-01T00:00:00Z", log["time"])
|
| assert.Equal(t, map[string]any{"nested": "value"}, log["any"])
|
| }
|
|
|
| func TestSlogAdapter_EmptyAttributes(t *testing.T) {
|
| logger, output := createTestLogger(DebugLevel)
|
| slogLogger := logger.AsSlog()
|
|
|
|
|
| slogLogger.Info("message with no attributes")
|
|
|
| logs := output.GetOutput()
|
| require.Len(t, logs, 1, "Should have 1 log entry")
|
|
|
| log := logs[0]
|
| assert.Equal(t, "message with no attributes", log["message"])
|
| assert.Contains(t, log, "level", "Should have level field")
|
| assert.Contains(t, log, "time", "Should have time field")
|
| }
|
|
|
| func TestSlogAdapter_MultipleLoggers(t *testing.T) {
|
| output1 := &testLogOutput{}
|
| output2 := &testLogOutput{}
|
|
|
| logger1 := New(Config{
|
| Name: "logger1",
|
| Level: DebugLevel,
|
| Encoding: "json",
|
| })
|
|
|
| logger2 := New(Config{
|
| Name: "logger2",
|
| Level: InfoLevel,
|
| Encoding: "json",
|
| })
|
|
|
|
|
| logger1.logger = logger1.logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
| return zapcore.NewCore(
|
| zapcore.NewJSONEncoder(zapcore.EncoderConfig{
|
| MessageKey: "message",
|
| LevelKey: "level",
|
| TimeKey: "time",
|
| NameKey: "logger",
|
| CallerKey: "caller",
|
| StacktraceKey: "stacktrace",
|
| SkipLineEnding: false,
|
| LineEnding: zapcore.DefaultLineEnding,
|
| EncodeLevel: zapcore.CapitalLevelEncoder,
|
| EncodeTime: zapcore.RFC3339TimeEncoder,
|
| EncodeDuration: zapcore.SecondsDurationEncoder,
|
| EncodeCaller: zapcore.ShortCallerEncoder,
|
| }),
|
| zapcore.AddSync(output1),
|
| zap.NewAtomicLevelAt(zapcore.DebugLevel),
|
| )
|
| }))
|
|
|
| logger2.logger = logger2.logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
| return zapcore.NewCore(
|
| zapcore.NewJSONEncoder(zapcore.EncoderConfig{
|
| MessageKey: "message",
|
| LevelKey: "level",
|
| TimeKey: "time",
|
| NameKey: "logger",
|
| CallerKey: "caller",
|
| StacktraceKey: "stacktrace",
|
| SkipLineEnding: false,
|
| LineEnding: zapcore.DefaultLineEnding,
|
| EncodeLevel: zapcore.CapitalLevelEncoder,
|
| EncodeTime: zapcore.RFC3339TimeEncoder,
|
| EncodeDuration: zapcore.SecondsDurationEncoder,
|
| EncodeCaller: zapcore.ShortCallerEncoder,
|
| }),
|
| zapcore.AddSync(output2),
|
| zap.NewAtomicLevelAt(zapcore.InfoLevel),
|
| )
|
| }))
|
|
|
| slogLogger1 := logger1.AsSlog()
|
| slogLogger2 := logger2.AsSlog()
|
|
|
|
|
| slogLogger1.Debug("debug from logger1")
|
| slogLogger2.Debug("debug from logger2")
|
|
|
|
|
| logs1 := output1.GetOutput()
|
| logs2 := output2.GetOutput()
|
|
|
| assert.Len(t, logs1, 1, "Logger1 should have debug log")
|
| assert.Len(t, logs2, 0, "Logger2 should not have debug log")
|
|
|
| if len(logs1) > 0 {
|
| assert.Equal(t, "debug from logger1", logs1[0]["message"])
|
| }
|
| }
|
|
|