package otel_test import ( "context" "testing" "time" obsotel "github.com/fosrl/gerbil/internal/observability/otel" ) const ( defaultGRPCEndpoint = "localhost:4317" defaultServiceName = "gerbil-test" ) func newInMemoryBackend(t *testing.T) *obsotel.Backend { t.Helper() // Use a very short export interval; an in-process collector (noop exporter) // is used by pointing to a non-existent endpoint with insecure mode. // The backend itself should initialise without error since connection is lazy. b, err := obsotel.New(obsotel.Config{ Protocol: "grpc", Endpoint: defaultGRPCEndpoint, Insecure: true, ExportInterval: 100 * time.Millisecond, ServiceName: defaultServiceName, ServiceVersion: "0.0.1", }) if err != nil { t.Fatalf("failed to create otel backend: %v", err) } return b } func TestOtelBackendHTTPHandlerIsNil(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck if b.HTTPHandler() != nil { t.Error("OTel backend HTTPHandler should return nil") } } func TestOtelBackendShutdown(t *testing.T) { b := newInMemoryBackend(t) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := b.Shutdown(ctx); err != nil { // Shutdown with unreachable collector may fail to flush; that's acceptable. // What matters is that Shutdown does not panic. t.Logf("Shutdown returned (expected with no collector): %v", err) } } func TestOtelBackendCounter(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck c, err := b.NewCounter("gerbil_test_counter_total", "test counter", "result") if err != nil { t.Fatalf("NewCounter returned error: %v", err) } // Should not panic c.Add(context.Background(), 1, map[string]string{"result": "ok"}) c.Add(context.Background(), 5, nil) } func TestOtelBackendUpDownCounter(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck u, err := b.NewUpDownCounter("gerbil_test_updown", "test updown", "state") if err != nil { t.Fatalf("NewUpDownCounter returned error: %v", err) } u.Add(context.Background(), 3, map[string]string{"state": "active"}) u.Add(context.Background(), -1, map[string]string{"state": "active"}) } func TestOtelBackendInt64Gauge(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck g, err := b.NewInt64Gauge("gerbil_test_int_gauge", "test gauge") if err != nil { t.Fatalf("NewInt64Gauge returned error: %v", err) } g.Record(context.Background(), 42, nil) } func TestOtelBackendFloat64Gauge(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck g, err := b.NewFloat64Gauge("gerbil_test_float_gauge", "test float gauge") if err != nil { t.Fatalf("NewFloat64Gauge returned error: %v", err) } g.Record(context.Background(), 3.14, nil) } func TestOtelBackendHistogram(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck h, err := b.NewHistogram("gerbil_test_duration_seconds", "test histogram", []float64{0.1, 0.5, 1.0}, "method") if err != nil { t.Fatalf("NewHistogram returned error: %v", err) } h.Record(context.Background(), 0.3, map[string]string{"method": "GET"}) } func TestOtelBackendHTTPProtocol(t *testing.T) { b, err := obsotel.New(obsotel.Config{ Protocol: "http", Endpoint: "localhost:4318", Insecure: true, ExportInterval: 100 * time.Millisecond, ServiceName: defaultServiceName, }) if err != nil { t.Fatalf("failed to create otel http backend: %v", err) } defer b.Shutdown(context.Background()) //nolint:errcheck if b.HTTPHandler() != nil { t.Error("OTel HTTP backend should not expose a /metrics endpoint") } } func TestOtelBackendInvalidProtocol(t *testing.T) { _, err := obsotel.New(obsotel.Config{ Protocol: "tcp", Endpoint: defaultGRPCEndpoint, ExportInterval: 10 * time.Second, }) if err == nil { t.Error("expected error for invalid protocol") } } func TestOtelBackendDeploymentEnvironment(t *testing.T) { b, err := obsotel.New(obsotel.Config{ Protocol: "grpc", Endpoint: defaultGRPCEndpoint, Insecure: true, ExportInterval: 100 * time.Millisecond, ServiceName: defaultServiceName, ServiceVersion: "1.2.3", DeploymentEnvironment: "staging", }) if err != nil { t.Fatalf("unexpected error: %v", err) } defer b.Shutdown(context.Background()) //nolint:errcheck } func TestOtelBackendRejectsInvalidLabelNames(t *testing.T) { b := newInMemoryBackend(t) defer b.Shutdown(context.Background()) //nolint:errcheck t.Run("duplicate labels", func(t *testing.T) { _, err := b.NewCounter("gerbil_test_invalid_labels_total", "test counter", "result", "result") if err == nil { t.Fatal("expected error for duplicate label names") } }) t.Run("invalid label name", func(t *testing.T) { _, err := b.NewHistogram("gerbil_test_invalid_histogram", "test histogram", []float64{0.1, 1.0}, "status-code") if err == nil { t.Fatal("expected error for invalid label name") } }) }