package telemetry import ( "context" "go/ast" "go/parser" "go/token" "net" "os" "strings" "sync" "testing" "time" "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" "code.nochebuena.dev/einherjar/contracts/logging" ) // --------------------------------------------------------------------------- // CT-6: ≤1 exported TypeSpec per file // --------------------------------------------------------------------------- func TestAtMostOneExportedTypePerFile(t *testing.T) { fset := token.NewFileSet() entries, err := os.ReadDir(".") if err != nil { t.Fatalf("ReadDir: %v", err) } for _, entry := range entries { name := entry.Name() if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { continue } f, err := parser.ParseFile(fset, name, nil, 0) if err != nil { t.Fatalf("parse %s: %v", name, err) } count := 0 for _, decl := range f.Decls { gd, ok := decl.(*ast.GenDecl) if !ok { continue } for _, spec := range gd.Specs { ts, ok := spec.(*ast.TypeSpec) if !ok { continue } if ts.Name.IsExported() { count++ } } } if count > 1 { t.Errorf("%s: has %d exported TypeSpecs, want ≤1", name, count) } } } // --------------------------------------------------------------------------- // DefaultConfig / DefaultConsoleConfig // --------------------------------------------------------------------------- func TestDefaultConfig_OptionalFields(t *testing.T) { cfg := DefaultConfig() if cfg.ServiceVersion != "unknown" { t.Errorf("ServiceVersion: got %q, want %q", cfg.ServiceVersion, "unknown") } if cfg.Environment != "development" { t.Errorf("Environment: got %q, want %q", cfg.Environment, "development") } if cfg.OTLPInsecure { t.Error("OTLPInsecure: want false by default") } } func TestDefaultConsoleConfig_OptionalFields(t *testing.T) { cfg := DefaultConsoleConfig() if cfg.ServiceVersion != "unknown" { t.Errorf("ServiceVersion: got %q, want %q", cfg.ServiceVersion, "unknown") } if cfg.Environment != "development" { t.Errorf("Environment: got %q, want %q", cfg.Environment, "development") } } // --------------------------------------------------------------------------- // newResource // --------------------------------------------------------------------------- func TestNewResource_Fields(t *testing.T) { cfg := Config{ ServiceName: "my-service", ServiceVersion: "2.0.0", Environment: "staging", } res := newResource(cfg) check := func(key, want string) { t.Helper() for _, kv := range res.Attributes() { if string(kv.Key) == key { if got := kv.Value.AsString(); got != want { t.Errorf("resource[%s]: want %q, got %q", key, want, got) } return } } t.Errorf("resource attribute %q not found", key) } check("service.name", "my-service") check("service.version", "2.0.0") check("deployment.environment", "staging") } func TestNewResource_MergesWithDefault(t *testing.T) { res := newResource(Config{ServiceName: "svc"}) if res == nil { t.Fatal("newResource returned nil") } if len(res.Attributes()) == 0 { t.Error("resource has no attributes") } } // --------------------------------------------------------------------------- // New — OTLP bootstrap // --------------------------------------------------------------------------- // fakeCollector starts a TCP listener that accepts connections but speaks no protocol. func fakeCollector(t *testing.T) string { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("fakeCollector: %v", err) } t.Cleanup(func() { ln.Close() }) go func() { for { conn, err := ln.Accept() if err != nil { return } conn.Close() } }() return ln.Addr().String() } func cfgWith(endpoint string) Config { return Config{ ServiceName: "test-service", ServiceVersion: "0.0.1", Environment: "test", OTLPEndpoint: endpoint, OTLPInsecure: true, } } func TestNew_ShutdownCallable(t *testing.T) { endpoint := fakeCollector(t) ctx := context.Background() shutdown, err := New(ctx, cfgWith(endpoint)) if err != nil { t.Fatalf("New: %v", err) } if shutdown == nil { t.Fatal("shutdown func is nil") } shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) } func TestNew_SetsGlobalTracerProvider(t *testing.T) { endpoint := fakeCollector(t) ctx := context.Background() shutdown, err := New(ctx, cfgWith(endpoint)) if err != nil { t.Fatalf("New: %v", err) } t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) }) if _, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider); !ok { t.Errorf("expected *sdktrace.TracerProvider, got %T", otel.GetTracerProvider()) } } func TestNew_SetsGlobalMeterProvider(t *testing.T) { endpoint := fakeCollector(t) ctx := context.Background() shutdown, err := New(ctx, cfgWith(endpoint)) if err != nil { t.Fatalf("New: %v", err) } t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _ = shutdown(shutCtx) }) if _, ok := otel.GetMeterProvider().(*sdkmetric.MeterProvider); !ok { t.Errorf("expected *sdkmetric.MeterProvider, got %T", otel.GetMeterProvider()) } } // --------------------------------------------------------------------------- // NewConsole — logger-backed exporters // --------------------------------------------------------------------------- // stubLogger captures log calls for test assertions. Implements logging.Logger. type stubLogger struct { mu sync.Mutex logs []stubLog } type stubLog struct { level string msg string } func (s *stubLogger) Info(msg string, _ ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"info", msg}) } func (s *stubLogger) Warn(msg string, _ ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"warn", msg}) } func (s *stubLogger) Error(msg string, _ error, _ ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"error", msg}) } func (s *stubLogger) Debug(msg string, _ ...any) { s.mu.Lock() defer s.mu.Unlock() s.logs = append(s.logs, stubLog{"debug", msg}) } func (s *stubLogger) With(_ ...any) logging.Logger { return s } func (s *stubLogger) WithContext(_ context.Context) logging.Logger { return s } func (s *stubLogger) find(msg string) bool { s.mu.Lock() defer s.mu.Unlock() for _, l := range s.logs { if l.msg == msg { return true } } return 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) } 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) }) if _, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider); !ok { t.Errorf("expected *sdktrace.TracerProvider, got %T", otel.GetTracerProvider()) } } 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) }) if _, ok := otel.GetMeterProvider().(*sdkmetric.MeterProvider); !ok { t.Errorf("expected *sdkmetric.MeterProvider, got %T", otel.GetMeterProvider()) } } 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 !logger.find("otel: span") { 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 !logger.find("otel: metric") { t.Error("expected logger to receive 'otel: metric' after shutdown flush") } }