382 lines
9.3 KiB
Go
382 lines
9.3 KiB
Go
|
|
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")
|
||
|
|
}
|
||
|
|
}
|