package metrics import ( "context" "os" "testing" "time" "go.ntppool.org/common/internal/tracerconfig" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func TestSetup_NoConfiguration(t *testing.T) { // Clear any existing configuration tracerconfig.Clear() ctx := context.Background() err := Setup(ctx) // Should not return an error even when no configuration is available if err != nil { t.Errorf("Setup() returned unexpected error: %v", err) } // Should be able to get a meter (even if it's a no-op) meter := GetMeter("test-meter") if meter == nil { t.Error("GetMeter() returned nil") } } func TestGetMeter(t *testing.T) { // Clear any existing configuration tracerconfig.Clear() ctx := context.Background() _ = Setup(ctx) meter := GetMeter("test-service") if meter == nil { t.Fatal("GetMeter() returned nil") } // Test creating a counter instrument counter, err := meter.Int64Counter("test_counter") if err != nil { t.Errorf("Failed to create counter: %v", err) } // Test using the counter (should not error even with no-op provider) counter.Add(ctx, 1, metric.WithAttributes(attribute.String("test", "value"))) } func TestSetup_MultipleCallsSafe(t *testing.T) { // Clear any existing configuration tracerconfig.Clear() ctx := context.Background() // Call Setup multiple times err1 := Setup(ctx) err2 := Setup(ctx) err3 := Setup(ctx) if err1 != nil { t.Errorf("First Setup() call returned error: %v", err1) } if err2 != nil { t.Errorf("Second Setup() call returned error: %v", err2) } if err3 != nil { t.Errorf("Third Setup() call returned error: %v", err3) } // Should still be able to get meters meter := GetMeter("test-meter") if meter == nil { t.Error("GetMeter() returned nil after multiple Setup() calls") } } func TestSetup_WithConfiguration(t *testing.T) { // Clear any existing configuration tracerconfig.Clear() ctx := context.Background() config := &tracerconfig.Config{ ServiceName: "test-metrics-service", Environment: "test", Endpoint: "localhost:4317", // Will likely fail to connect, but should set up provider } // Create a mock exporter factory that returns a working exporter mockFactory := func(ctx context.Context, cfg *tracerconfig.Config) (sdkmetric.Exporter, error) { // Create a simple in-memory exporter for testing return &mockMetricExporter{}, nil } // Store configuration with mock factory tracerconfig.Store(ctx, config, nil, mockFactory, nil) // Setup metrics err := Setup(ctx) if err != nil { t.Errorf("Setup() returned error: %v", err) } // Should be able to get a meter meter := GetMeter("test-service") if meter == nil { t.Fatal("GetMeter() returned nil") } // Test creating and using instruments counter, err := meter.Int64Counter("test_counter") if err != nil { t.Errorf("Failed to create counter: %v", err) } histogram, err := meter.Float64Histogram("test_histogram") if err != nil { t.Errorf("Failed to create histogram: %v", err) } gauge, err := meter.Int64UpDownCounter("test_gauge") if err != nil { t.Errorf("Failed to create gauge: %v", err) } // Use the instruments counter.Add(ctx, 1, metric.WithAttributes(attribute.String("test", "value"))) histogram.Record(ctx, 1.5, metric.WithAttributes(attribute.String("test", "value"))) gauge.Add(ctx, 10, metric.WithAttributes(attribute.String("test", "value"))) // Test shutdown err = Shutdown(ctx) if err != nil { t.Errorf("Shutdown() returned error: %v", err) } } func TestSetup_WithRealOTLPConfig(t *testing.T) { // Skip this test in short mode since it may try to make network connections if testing.Short() { t.Skip("Skipping integration test in short mode") } // Clear any existing configuration tracerconfig.Clear() // Set environment variables for OTLP configuration originalEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") originalProtocol := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") defer func() { if originalEndpoint != "" { os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", originalEndpoint) } else { os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT") } if originalProtocol != "" { os.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", originalProtocol) } else { os.Unsetenv("OTEL_EXPORTER_OTLP_PROTOCOL") } }() os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") // HTTP endpoint os.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() config := &tracerconfig.Config{ ServiceName: "test-metrics-e2e", Environment: "test", Endpoint: "localhost:4318", } // Store configuration with real factory tracerconfig.Store(ctx, config, nil, tracerconfig.CreateOTLPMetricExporter, nil) // Setup metrics - this may fail if no OTLP collector is running, which is okay err := Setup(ctx) if err != nil { t.Logf("Setup() returned error (expected if no OTLP collector): %v", err) } // Should still be able to get a meter meter := GetMeter("test-service-e2e") if meter == nil { t.Fatal("GetMeter() returned nil") } // Create and use instruments counter, err := meter.Int64Counter("e2e_test_counter") if err != nil { t.Errorf("Failed to create counter: %v", err) } // Add some metrics for i := 0; i < 5; i++ { counter.Add(ctx, 1, metric.WithAttributes( attribute.String("iteration", string(rune('0'+i))), attribute.String("test_type", "e2e"), )) } // Give some time for export (if collector is running) time.Sleep(100 * time.Millisecond) // Test shutdown err = Shutdown(ctx) if err != nil { t.Logf("Shutdown() returned error (may be expected): %v", err) } } func TestConcurrentMetricUsage(t *testing.T) { // Clear any existing configuration tracerconfig.Clear() ctx := context.Background() config := &tracerconfig.Config{ ServiceName: "concurrent-test", } // Use mock factory mockFactory := func(ctx context.Context, cfg *tracerconfig.Config) (sdkmetric.Exporter, error) { return &mockMetricExporter{}, nil } tracerconfig.Store(ctx, config, nil, mockFactory, nil) Setup(ctx) meter := GetMeter("concurrent-test") counter, err := meter.Int64Counter("concurrent_counter") if err != nil { t.Fatalf("Failed to create counter: %v", err) } // Test concurrent metric usage const numGoroutines = 10 const metricsPerGoroutine = 100 done := make(chan bool, numGoroutines) for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { for j := 0; j < metricsPerGoroutine; j++ { counter.Add(ctx, 1, metric.WithAttributes( attribute.Int("goroutine", goroutineID), attribute.Int("iteration", j), )) } done <- true }(i) } // Wait for all goroutines to complete for i := 0; i < numGoroutines; i++ { <-done } // Shutdown err = Shutdown(ctx) if err != nil { t.Errorf("Shutdown() returned error: %v", err) } } // mockMetricExporter is a simple mock exporter for testing type mockMetricExporter struct{} func (m *mockMetricExporter) Export(ctx context.Context, rm *metricdata.ResourceMetrics) error { // Just pretend to export return nil } func (m *mockMetricExporter) ForceFlush(ctx context.Context) error { return nil } func (m *mockMetricExporter) Shutdown(ctx context.Context) error { return nil } func (m *mockMetricExporter) Temporality(kind sdkmetric.InstrumentKind) metricdata.Temporality { return metricdata.CumulativeTemporality } func (m *mockMetricExporter) Aggregation(kind sdkmetric.InstrumentKind) sdkmetric.Aggregation { return sdkmetric.DefaultAggregationSelector(kind) }