package telemetry import ( "context" "sync" "testing" "time" "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" "code.nochebuena.dev/go/logz" ) // stubLogger captures Info/Warn/Error calls for test assertions. type stubLogger struct { mu sync.Mutex logs []stubLog } type stubLog struct { level string msg string args []any } func (s *stubLogger) Info(msg string, args ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"info", msg, args}) } func (s *stubLogger) Warn(msg string, args ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"warn", msg, args}) } func (s *stubLogger) Error(msg string, err error, args ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"error", msg, args}) } func (s *stubLogger) Debug(msg string, args ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"debug", msg, args}) } func (s *stubLogger) With(args ...any) logz.Logger { return s } func (s *stubLogger) WithContext(ctx context.Context) logz.Logger { return s } func (s *stubLogger) find(msg string) (stubLog, bool) { s.mu.Lock() defer s.mu.Unlock() for _, l := range s.logs { if l.msg == msg { return l, true } } return stubLog{}, false } func consoleCfg() ConsoleConfig { return ConsoleConfig{ ServiceName: "test-svc", ServiceVersion: "0.0.1", Environment: "test", } } func TestNewConsole_ShutdownCallable(t *testing.T) { logger := &stubLogger{} ctx := context.Background() shutdown, err := NewConsole(ctx, logger, consoleCfg()) if err != nil { t.Fatalf("NewConsole: %v", err) } if shutdown == nil { t.Fatal("shutdown is nil") } shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() if err := shutdown(shutCtx); err != nil { t.Errorf("shutdown: %v", err) } } func TestNewConsole_SetsGlobalTracerProvider(t *testing.T) { logger := &stubLogger{} ctx := context.Background() shutdown, err := NewConsole(ctx, logger, consoleCfg()) if err != nil { t.Fatalf("NewConsole: %v", err) } t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) }) tp := otel.GetTracerProvider() if _, ok := tp.(*sdktrace.TracerProvider); !ok { t.Errorf("expected *sdktrace.TracerProvider, got %T", tp) } } func TestNewConsole_SetsGlobalMeterProvider(t *testing.T) { logger := &stubLogger{} ctx := context.Background() shutdown, err := NewConsole(ctx, logger, consoleCfg()) if err != nil { t.Fatalf("NewConsole: %v", err) } t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) }) mp := otel.GetMeterProvider() if _, ok := mp.(*sdkmetric.MeterProvider); !ok { t.Errorf("expected *sdkmetric.MeterProvider, got %T", mp) } } func TestNewConsole_ExportsSpan(t *testing.T) { logger := &stubLogger{} ctx := context.Background() shutdown, err := NewConsole(ctx, logger, consoleCfg()) if err != nil { t.Fatalf("NewConsole: %v", err) } defer func() { shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) }() tracer := otel.Tracer("test") _, span := tracer.Start(ctx, "test-span") span.End() // WithSyncer exports synchronously on End if _, ok := logger.find("otel: span"); !ok { t.Error("expected logger to receive 'otel: span' after span.End()") } } func TestNewConsole_ExportsMetric(t *testing.T) { logger := &stubLogger{} ctx := context.Background() shutdown, err := NewConsole(ctx, logger, consoleCfg()) if err != nil { t.Fatalf("NewConsole: %v", err) } meter := otel.Meter("test") counter, err := meter.Int64Counter("test.counter") if err != nil { t.Fatalf("Int64Counter: %v", err) } counter.Add(ctx, 1) // Force export by shutting down — PeriodicReader flushes on shutdown. shutCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() if err := shutdown(shutCtx); err != nil { t.Errorf("shutdown: %v", err) } if _, ok := logger.find("otel: metric"); !ok { t.Error("expected logger to receive 'otel: metric' after shutdown flush") } }