package tracerconfig import ( "context" "crypto/tls" "crypto/x509" "os" "strings" "sync" "testing" "time" sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func TestStore_And_Retrieve(t *testing.T) { // Clear any existing configuration Clear() ctx := context.Background() config := &Config{ ServiceName: "test-service", Environment: "test", Endpoint: "localhost:4317", } // Create mock factories logFactory := func(context.Context, *Config) (sdklog.Exporter, error) { return nil, nil } metricFactory := func(context.Context, *Config) (sdkmetric.Exporter, error) { return nil, nil } traceFactory := func(context.Context, *Config) (sdktrace.SpanExporter, error) { return nil, nil } // Store configuration Store(ctx, config, logFactory, metricFactory, traceFactory) // Test IsConfigured if !IsConfigured() { t.Error("IsConfigured() should return true after Store()") } // Test GetLogExporter cfg, ctx2, factory := GetLogExporter() if cfg == nil || ctx2 == nil || factory == nil { t.Error("GetLogExporter() should return non-nil values") } if cfg.ServiceName != "test-service" { t.Errorf("Expected ServiceName 'test-service', got '%s'", cfg.ServiceName) } // Test GetMetricExporter cfg, ctx3, metricFact := GetMetricExporter() if cfg == nil || ctx3 == nil || metricFact == nil { t.Error("GetMetricExporter() should return non-nil values") } // Test GetTraceExporter cfg, ctx4, traceFact := GetTraceExporter() if cfg == nil || ctx4 == nil || traceFact == nil { t.Error("GetTraceExporter() should return non-nil values") } // Test backward compatibility Get() cfg, ctx5, logFact := Get() if cfg == nil || ctx5 == nil || logFact == nil { t.Error("Get() should return non-nil values for backward compatibility") } } func TestClear(t *testing.T) { // Store some configuration first ctx := context.Background() config := &Config{ServiceName: "test"} Store(ctx, config, nil, nil, nil) if !IsConfigured() { t.Error("Should be configured before Clear()") } // Clear configuration Clear() if IsConfigured() { t.Error("Should not be configured after Clear()") } // All getters should return nil cfg, ctx2, factory := GetLogExporter() if cfg != nil || ctx2 != nil || factory != nil { t.Error("GetLogExporter() should return nil values after Clear()") } } func TestConcurrentAccess(t *testing.T) { Clear() ctx := context.Background() config := &Config{ServiceName: "concurrent-test"} var wg sync.WaitGroup const numGoroutines = 10 // Test concurrent Store and Get operations wg.Add(numGoroutines * 2) // Concurrent Store operations for i := 0; i < numGoroutines; i++ { go func() { defer wg.Done() Store(ctx, config, nil, nil, nil) }() } // Concurrent Get operations for i := 0; i < numGoroutines; i++ { go func() { defer wg.Done() IsConfigured() GetLogExporter() GetMetricExporter() GetTraceExporter() }() } wg.Wait() // Should be configured after all operations if !IsConfigured() { t.Error("Should be configured after concurrent operations") } } func TestGetTLSConfig(t *testing.T) { tests := []struct { name string config *Config expected bool // whether TLS config should be nil }{ { name: "nil certificate provider", config: &Config{}, expected: true, // should be nil }, { name: "with certificate provider", config: &Config{ CertificateProvider: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &tls.Certificate{}, nil }, }, expected: false, // should not be nil }, { name: "with certificate provider and RootCAs", config: &Config{ CertificateProvider: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &tls.Certificate{}, nil }, RootCAs: x509.NewCertPool(), }, expected: false, // should not be nil }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tlsConfig := getTLSConfig(tt.config) if tt.expected && tlsConfig != nil { t.Errorf("Expected nil TLS config, got %v", tlsConfig) } if !tt.expected && tlsConfig == nil { t.Error("Expected non-nil TLS config, got nil") } if !tt.expected && tlsConfig != nil { if tlsConfig.GetClientCertificate == nil { t.Error("Expected GetClientCertificate to be set") } if tt.config.RootCAs != nil && tlsConfig.RootCAs != tt.config.RootCAs { t.Error("Expected RootCAs to be set correctly") } } }) } } func TestGetProtocol(t *testing.T) { // Save original env vars originalGeneral := os.Getenv(otelExporterOTLPProtoEnvKey) originalLogs := os.Getenv(otelExporterOTLPLogsProtoEnvKey) defer func() { // Restore original env vars if originalGeneral != "" { os.Setenv(otelExporterOTLPProtoEnvKey, originalGeneral) } else { os.Unsetenv(otelExporterOTLPProtoEnvKey) } if originalLogs != "" { os.Setenv(otelExporterOTLPLogsProtoEnvKey, originalLogs) } else { os.Unsetenv(otelExporterOTLPLogsProtoEnvKey) } }() tests := []struct { name string signalSpecific string generalProto string specificProto string expectedResult string }{ { name: "no env vars set - default", signalSpecific: otelExporterOTLPLogsProtoEnvKey, expectedResult: "http/protobuf", }, { name: "general env var set", signalSpecific: otelExporterOTLPLogsProtoEnvKey, generalProto: "grpc", expectedResult: "grpc", }, { name: "specific env var overrides general", signalSpecific: otelExporterOTLPLogsProtoEnvKey, generalProto: "grpc", specificProto: "http/protobuf", expectedResult: "http/protobuf", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Clear env vars os.Unsetenv(otelExporterOTLPProtoEnvKey) os.Unsetenv(otelExporterOTLPLogsProtoEnvKey) // Set test env vars if tt.generalProto != "" { os.Setenv(otelExporterOTLPProtoEnvKey, tt.generalProto) } if tt.specificProto != "" { os.Setenv(tt.signalSpecific, tt.specificProto) } result := getProtocol(tt.signalSpecific) if result != tt.expectedResult { t.Errorf("Expected protocol '%s', got '%s'", tt.expectedResult, result) } }) } } func TestCreateExporterErrors(t *testing.T) { ctx := context.Background() config := &Config{ ServiceName: "test-service", Endpoint: "invalid-endpoint", } // Test with invalid protocol for logs os.Setenv(otelExporterOTLPLogsProtoEnvKey, "invalid-protocol") defer os.Unsetenv(otelExporterOTLPLogsProtoEnvKey) _, err := CreateOTLPLogExporter(ctx, config) if err == nil { t.Error("Expected error for invalid protocol") } // Check that it's a protocol error (the specific message will be different now) if !strings.Contains(err.Error(), "invalid OTLP protocol") { t.Errorf("Expected protocol error, got %v", err) } // Test with invalid protocol for metrics os.Setenv(otelExporterOTLPMetricsProtoEnvKey, "invalid-protocol") defer os.Unsetenv(otelExporterOTLPMetricsProtoEnvKey) _, err = CreateOTLPMetricExporter(ctx, config) if err == nil { t.Error("Expected error for invalid protocol") } if !strings.Contains(err.Error(), "invalid OTLP protocol") { t.Errorf("Expected protocol error, got %v", err) } // Test with invalid protocol for traces os.Setenv(otelExporterOTLPTracesProtoEnvKey, "invalid-protocol") defer os.Unsetenv(otelExporterOTLPTracesProtoEnvKey) _, err = CreateOTLPTraceExporter(ctx, config) if err == nil { t.Error("Expected error for invalid protocol") } if !strings.Contains(err.Error(), "invalid OTLP protocol") { t.Errorf("Expected protocol error, got %v", err) } } func TestCreateExporterValidProtocols(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() config := &Config{ ServiceName: "test-service", Endpoint: "localhost:4317", // This will likely fail to connect, but should create exporter } protocols := []string{"grpc", "http/protobuf", "http/json"} for _, proto := range protocols { t.Run("logs_"+proto, func(t *testing.T) { os.Setenv(otelExporterOTLPLogsProtoEnvKey, proto) defer os.Unsetenv(otelExporterOTLPLogsProtoEnvKey) exporter, err := CreateOTLPLogExporter(ctx, config) if err != nil { // Connection errors are expected since we're not running a real OTLP server // but the exporter should be created successfully t.Logf("Connection error expected: %v", err) } if exporter != nil { exporter.Shutdown(ctx) } }) t.Run("metrics_"+proto, func(t *testing.T) { os.Setenv(otelExporterOTLPMetricsProtoEnvKey, proto) defer os.Unsetenv(otelExporterOTLPMetricsProtoEnvKey) exporter, err := CreateOTLPMetricExporter(ctx, config) if err != nil { t.Logf("Connection error expected: %v", err) } if exporter != nil { exporter.Shutdown(ctx) } }) t.Run("traces_"+proto, func(t *testing.T) { os.Setenv(otelExporterOTLPTracesProtoEnvKey, proto) defer os.Unsetenv(otelExporterOTLPTracesProtoEnvKey) exporter, err := CreateOTLPTraceExporter(ctx, config) if err != nil { t.Logf("Connection error expected: %v", err) } if exporter != nil { exporter.Shutdown(ctx) } }) } } func TestConfigValidation(t *testing.T) { tests := []struct { name string config *Config shouldErr bool }{ { name: "valid empty config", config: &Config{}, shouldErr: false, }, { name: "valid config with endpoint", config: &Config{ ServiceName: "test-service", Endpoint: "localhost:4317", }, shouldErr: false, }, { name: "valid config with endpoint URL", config: &Config{ ServiceName: "test-service", EndpointURL: "https://otlp.example.com:4317/v1/traces", }, shouldErr: false, }, { name: "invalid - both endpoint and endpoint URL", config: &Config{ ServiceName: "test-service", Endpoint: "localhost:4317", EndpointURL: "https://otlp.example.com:4317/v1/traces", }, shouldErr: true, }, { name: "invalid - endpoint with protocol", config: &Config{ ServiceName: "test-service", Endpoint: "https://localhost:4317", }, shouldErr: true, }, { name: "invalid - empty endpoint", config: &Config{ ServiceName: "test-service", Endpoint: " ", }, shouldErr: true, }, { name: "invalid - malformed endpoint URL", config: &Config{ ServiceName: "test-service", EndpointURL: "://invalid-url-missing-scheme", }, shouldErr: true, }, { name: "invalid - empty service name", config: &Config{ ServiceName: " ", Endpoint: "localhost:4317", }, shouldErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.shouldErr && err == nil { t.Error("Expected validation error, got nil") } if !tt.shouldErr && err != nil { t.Errorf("Expected no validation error, got: %v", err) } }) } } func TestValidateAndStore(t *testing.T) { Clear() ctx := context.Background() // Test with valid config validConfig := &Config{ ServiceName: "test-service", Endpoint: "localhost:4317", } err := ValidateAndStore(ctx, validConfig, nil, nil, nil) if err != nil { t.Errorf("ValidateAndStore with valid config should not error: %v", err) } if !IsConfigured() { t.Error("Should be configured after ValidateAndStore") } Clear() // Test with invalid config invalidConfig := &Config{ ServiceName: "test-service", Endpoint: "localhost:4317", EndpointURL: "https://example.com:4317", // both specified - invalid } err = ValidateAndStore(ctx, invalidConfig, nil, nil, nil) if err == nil { t.Error("ValidateAndStore with invalid config should return error") } if IsConfigured() { t.Error("Should not be configured after failed ValidateAndStore") } }