- Create new metrics/ package for OpenTelemetry-native metrics with OTLP export - Refactor OTLP configuration to internal/tracerconfig/ to eliminate code duplication - Add consistent retry configuration across all HTTP OTLP exporters - Add configuration validation and improved error messages - Include test coverage for all new functionality - Make OpenTelemetry metrics dependencies explicit in go.mod Designed for new applications requiring structured metrics export to observability backends via OTLP protocol.
475 lines
12 KiB
Go
475 lines
12 KiB
Go
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")
|
|
}
|
|
}
|