diff --git a/pkg/telemetry/logexporters/factory_test.go b/pkg/telemetry/logexporters/factory_test.go new file mode 100644 index 000000000..fade29283 --- /dev/null +++ b/pkg/telemetry/logexporters/factory_test.go @@ -0,0 +1,77 @@ +package logexporters + +import ( + "testing" +) + +func TestExporterFactory(t *testing.T) { + tests := []struct { + name string + exporter string + protocol string + wantErr bool + errMessage string + }{ + {name: "otlp alias is accepted", exporter: "otlp", protocol: "http", wantErr: false}, + {name: "otlp-http alias is accepted", exporter: "otlp-http", protocol: "http", wantErr: false}, + {name: "otlp-grpc alias is accepted", exporter: "otlp-grpc", protocol: "grpc", wantErr: false}, + {name: "unsupported exporter", exporter: "prometheus", protocol: "http", wantErr: true, errMessage: "prometheus log exporter is unsupported"}, + {name: "unsupported protocol", exporter: "otlp", protocol: "tcp", wantErr: true, errMessage: "unsupported protocol: tcp"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := ExporterFactory(tt.exporter, "localhost:4317", true, "", nil, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tt.errMessage != "" && err.Error() != tt.errMessage { + t.Fatalf("expected error %q, got %q", tt.errMessage, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} + +func TestNewOTLP(t *testing.T) { + tests := []struct { + name string + insecure bool + urlpath string + headers map[string]string + protocol string + wantErr bool + }{ + {name: "http insecure with options", insecure: true, urlpath: "/v1/logs", headers: map[string]string{"key": "value"}, protocol: "http"}, + {name: "http secure", insecure: false, protocol: "http"}, + {name: "grpc insecure", insecure: true, protocol: "grpc"}, + {name: "grpc secure with headers", insecure: false, headers: map[string]string{"key": "value"}, protocol: "grpc"}, + {name: "unsupported protocol", protocol: "udp", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := NewOTLP("localhost:4317", tt.insecure, tt.urlpath, tt.headers, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} diff --git a/pkg/telemetry/meterexporters/factory_test.go b/pkg/telemetry/meterexporters/factory_test.go new file mode 100644 index 000000000..8af5f29fd --- /dev/null +++ b/pkg/telemetry/meterexporters/factory_test.go @@ -0,0 +1,77 @@ +package meterexporters + +import ( + "testing" +) + +func TestExporterFactory(t *testing.T) { + tests := []struct { + name string + exporter string + protocol string + wantErr bool + errMessage string + }{ + {name: "otlp alias is accepted", exporter: "otlp", protocol: "http", wantErr: false}, + {name: "otlp-http alias is accepted", exporter: "otlp-http", protocol: "http", wantErr: false}, + {name: "otlp-grpc alias is accepted", exporter: "otlp-grpc", protocol: "grpc", wantErr: false}, + {name: "unsupported exporter", exporter: "prometheus", protocol: "http", wantErr: true, errMessage: "prometheus meter exporter is unsupported"}, + {name: "unsupported protocol", exporter: "otlp", protocol: "tcp", wantErr: true, errMessage: "unsupported protocol: tcp"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := ExporterFactory(tt.exporter, "localhost:4317", true, "", nil, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tt.errMessage != "" && err.Error() != tt.errMessage { + t.Fatalf("expected error %q, got %q", tt.errMessage, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} + +func TestNewOTLP(t *testing.T) { + tests := []struct { + name string + insecure bool + urlpath string + headers map[string]string + protocol string + wantErr bool + }{ + {name: "http insecure with options", insecure: true, urlpath: "/v1/metrics", headers: map[string]string{"key": "value"}, protocol: "http"}, + {name: "http secure", insecure: false, protocol: "http"}, + {name: "grpc insecure", insecure: true, headers: map[string]string{"key": "value"}, protocol: "grpc"}, + {name: "grpc secure", insecure: false, headers: map[string]string{"key": "value"}, protocol: "grpc"}, + {name: "unsupported protocol", protocol: "udp", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := NewOTLP("localhost:4317", tt.insecure, tt.urlpath, tt.headers, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go new file mode 100644 index 000000000..bee310666 --- /dev/null +++ b/pkg/telemetry/telemetry_test.go @@ -0,0 +1,152 @@ +package telemetry + +import ( + "context" + "log/slog" + "testing" + + semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + + "github.com/Permify/permify/pkg/telemetry/logexporters" + "github.com/Permify/permify/pkg/telemetry/tracerexporters" +) + +func TestNewResource(t *testing.T) { + tests := []struct { + name string + serviceName string + wantService string + }{ + {name: "empty falls back to permify", serviceName: "", wantService: "permify"}, + {name: "whitespace falls back to permify", serviceName: " ", wantService: "permify"}, + {name: "custom service name is honored", serviceName: "my-service", wantService: "my-service"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := newResource(tt.serviceName) + if res == nil { + t.Fatalf("expected resource, got nil") + } + + var got string + for _, attr := range res.Attributes() { + if attr.Key == semconv.ServiceNameKey { + got = attr.Value.AsString() + } + } + if got != tt.wantService { + t.Fatalf("expected service name %q, got %q", tt.wantService, got) + } + }) + } +} + +func TestHandlerFactory(t *testing.T) { + tests := []struct { + name string + handler string + headers map[string]string + wantErr bool + }{ + {name: "otlp", handler: "otlp", wantErr: false}, + {name: "gcp missing project errors", handler: "gcp", headers: map[string]string{}, wantErr: true}, + {name: "unsupported handler", handler: "prometheus", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h, err := HandlerFactory(tt.handler, "localhost:4317", true, "", tt.headers, "http", slog.LevelInfo, "svc") + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatalf("expected handler, got nil") + } + }) + } +} + +func TestNewOTLPHandler(t *testing.T) { + h, err := NewOTLPHandler("localhost:4317", true, "/v1/logs", map[string]string{"key": "value"}, "http", slog.LevelDebug, "svc") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatalf("expected handler, got nil") + } +} + +func TestNewGCPHandlerMissingProject(t *testing.T) { + if _, err := NewGCPHandler(map[string]string{}, slog.LevelInfo); err == nil { + t.Fatalf("expected error when project id is missing") + } +} + +func TestNewLog(t *testing.T) { + exporter, err := logexporters.NewOTLP("localhost:4317", true, "", nil, "http") + if err != nil { + t.Fatalf("failed to build exporter: %v", err) + } + lp := NewLog(exporter, "svc") + if lp == nil { + t.Fatalf("expected logger provider, got nil") + } + _ = lp.Shutdown(context.Background()) +} + +func TestNewTracer(t *testing.T) { + tests := []struct { + name string + serviceName string + }{ + {name: "custom service name", serviceName: "svc"}, + {name: "empty service name", serviceName: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exporter, err := tracerexporters.NewOTLP("localhost:4317", true, "", nil, "http") + if err != nil { + t.Fatalf("failed to build exporter: %v", err) + } + shutdown := NewTracer(exporter, tt.serviceName) + if shutdown == nil { + t.Fatalf("expected shutdown func, got nil") + } + if err := shutdown(context.Background()); err != nil { + t.Fatalf("unexpected shutdown error: %v", err) + } + }) + } +} + +// NewMeter is intentionally not unit-tested here: it starts process-global +// gopsutil host instrumentation via host.Start and installs a global meter +// provider, which is environment-dependent and flaky under `go test -race`. +// Its constituent pieces (the OTLP meter exporter and the meter helpers below) +// are covered instead. + +func TestNewNoopMeter(t *testing.T) { + if NewNoopMeter() == nil { + t.Fatalf("expected noop meter, got nil") + } +} + +func TestNewCounterAndHistogram(t *testing.T) { + meter := NewNoopMeter() + + if NewCounter(meter, "test_counter", "a test counter") == nil { + t.Fatalf("expected counter, got nil") + } + + if NewHistogram(meter, "test_histogram", "ms", "a test histogram") == nil { + t.Fatalf("expected histogram, got nil") + } +} diff --git a/pkg/telemetry/tracerexporters/factory_test.go b/pkg/telemetry/tracerexporters/factory_test.go new file mode 100644 index 000000000..85938cf57 --- /dev/null +++ b/pkg/telemetry/tracerexporters/factory_test.go @@ -0,0 +1,124 @@ +package tracerexporters + +import ( + "testing" +) + +func TestExporterFactory(t *testing.T) { + tests := []struct { + name string + exporter string + url string + protocol string + wantErr bool + errMessage string + }{ + {name: "zipkin", exporter: "zipkin", url: "http://localhost:9411/api/v2/spans", protocol: "http", wantErr: false}, + {name: "jaeger", exporter: "jaeger", url: "http://localhost:14268/api/traces", protocol: "http", wantErr: false}, + {name: "otlp alias is accepted", exporter: "otlp", url: "localhost:4317", protocol: "http", wantErr: false}, + {name: "otlp-http alias is accepted", exporter: "otlp-http", url: "localhost:4317", protocol: "http", wantErr: false}, + {name: "otlp-grpc alias is accepted", exporter: "otlp-grpc", url: "localhost:4317", protocol: "grpc", wantErr: false}, + {name: "signoz", exporter: "signoz", url: "localhost:4317", protocol: "grpc", wantErr: false}, + {name: "unsupported exporter", exporter: "prometheus", url: "localhost:4317", protocol: "http", wantErr: true, errMessage: "prometheus tracer exporter is unsupported"}, + {name: "unsupported protocol", exporter: "otlp", url: "localhost:4317", protocol: "tcp", wantErr: true, errMessage: "unsupported protocol: tcp"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := ExporterFactory(tt.exporter, tt.url, true, "", nil, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tt.errMessage != "" && err.Error() != tt.errMessage { + t.Fatalf("expected error %q, got %q", tt.errMessage, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} + +func TestNewOTLP(t *testing.T) { + tests := []struct { + name string + insecure bool + urlpath string + headers map[string]string + protocol string + wantErr bool + }{ + {name: "http insecure with options", insecure: true, urlpath: "/v1/traces", headers: map[string]string{"key": "value"}, protocol: "http"}, + {name: "http secure", insecure: false, protocol: "http"}, + {name: "grpc insecure with headers", insecure: true, headers: map[string]string{"key": "value"}, protocol: "grpc"}, + {name: "grpc secure", insecure: false, protocol: "grpc"}, + {name: "unsupported protocol", protocol: "udp", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := NewOTLP("localhost:4317", tt.insecure, tt.urlpath, tt.headers, tt.protocol) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +} + +func TestNewZipkin(t *testing.T) { + exp, err := NewZipkin("http://localhost:9411/api/v2/spans") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } +} + +func TestNewJaegar(t *testing.T) { + exp, err := NewJaegar("http://localhost:14268/api/traces") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } +} + +func TestNewSigNoz(t *testing.T) { + tests := []struct { + name string + insecure bool + headers map[string]string + }{ + {name: "insecure with headers", insecure: true, headers: map[string]string{"key": "value"}}, + {name: "secure without headers", insecure: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exp, err := NewSigNoz("localhost:4317", tt.insecure, tt.headers) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exp == nil { + t.Fatalf("expected exporter, got nil") + } + }) + } +}