rdev/internal/telemetry/telemetry_test.go
jordan f6ced22e06 fix: Use FQDN for k8s service hostnames and remove broken commonLabels
Short-form DNS names (e.g. postgres.databases.svc) fail to resolve in
new pods due to k8s DNS search domain limitations. Switch all service
hostnames to FQDNs (*.svc.cluster.local).

Remove commonLabels from kustomization.yaml — it injected labels into
all selectors including NetworkPolicy egress rules (blocking DNS to
CoreDNS) and Deployment selectors (causing immutability errors).

Add OTEL_EXPORTER_OTLP_ENDPOINT env var to deployment YAML so the
telemetry collector endpoint uses the FQDN without requiring a binary
rebuild.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 20:46:04 -07:00

322 lines
8.1 KiB
Go

package telemetry
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/orchard9/rdev/internal/envutil"
)
func TestDefaultConfig(t *testing.T) {
// Clear env vars that might affect defaults
_ = os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT")
_ = os.Unsetenv("OTEL_SERVICE_NAME")
_ = os.Unsetenv("OTEL_SERVICE_VERSION")
_ = os.Unsetenv("OTEL_SERVICE_NAMESPACE")
_ = os.Unsetenv("OTEL_ENABLED")
cfg := DefaultConfig()
if cfg.Endpoint != "otel-collector.observability.svc.cluster.local:4317" {
t.Errorf("expected default endpoint, got %s", cfg.Endpoint)
}
if cfg.ServiceName != "rdev-api" {
t.Errorf("expected default service name, got %s", cfg.ServiceName)
}
if cfg.ServiceVersion != "unknown" {
t.Errorf("expected default service version, got %s", cfg.ServiceVersion)
}
if cfg.ServiceNamespace != "rdev" {
t.Errorf("expected default service namespace, got %s", cfg.ServiceNamespace)
}
if !cfg.Enabled {
t.Error("expected telemetry enabled by default")
}
}
func TestDefaultConfigWithEnv(t *testing.T) {
// Set custom env vars
_ = os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "custom-collector:4317")
_ = os.Setenv("OTEL_SERVICE_NAME", "custom-service")
_ = os.Setenv("OTEL_SERVICE_VERSION", "v1.2.3")
_ = os.Setenv("OTEL_SERVICE_NAMESPACE", "custom-ns")
_ = os.Setenv("OTEL_ENABLED", "false")
defer func() {
_ = os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT")
_ = os.Unsetenv("OTEL_SERVICE_NAME")
_ = os.Unsetenv("OTEL_SERVICE_VERSION")
_ = os.Unsetenv("OTEL_SERVICE_NAMESPACE")
_ = os.Unsetenv("OTEL_ENABLED")
}()
cfg := DefaultConfig()
if cfg.Endpoint != "custom-collector:4317" {
t.Errorf("expected custom endpoint, got %s", cfg.Endpoint)
}
if cfg.ServiceName != "custom-service" {
t.Errorf("expected custom service name, got %s", cfg.ServiceName)
}
if cfg.ServiceVersion != "v1.2.3" {
t.Errorf("expected custom service version, got %s", cfg.ServiceVersion)
}
if cfg.ServiceNamespace != "custom-ns" {
t.Errorf("expected custom service namespace, got %s", cfg.ServiceNamespace)
}
if cfg.Enabled {
t.Error("expected telemetry disabled")
}
}
func TestNewTelemetryDisabled(t *testing.T) {
cfg := Config{
Enabled: false,
ServiceName: "test-service",
}
tel, err := New(context.Background(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tel == nil {
t.Fatal("expected telemetry instance")
}
// Verify tracer is available (noop)
if tel.Tracer() == nil {
t.Error("expected noop tracer")
}
// Shutdown should be safe
if err := tel.Shutdown(context.Background()); err != nil {
t.Errorf("unexpected shutdown error: %v", err)
}
}
func TestNewTelemetryWithBadEndpoint(t *testing.T) {
// This test verifies that creation doesn't fail even with unreachable endpoint
// The actual connection happens asynchronously during export
cfg := Config{
Enabled: true,
Endpoint: "localhost:99999",
ServiceName: "test-service",
Insecure: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
tel, err := New(ctx, cfg)
if err != nil {
t.Fatalf("unexpected error creating telemetry: %v", err)
}
defer func() { _ = tel.Shutdown(context.Background()) }()
// Should be able to create spans even if collector is unreachable
_, span := tel.StartSpan(context.Background(), "test-span")
span.End()
}
func TestStartSpan(t *testing.T) {
cfg := Config{
Enabled: false,
ServiceName: "test-service",
}
tel, err := New(context.Background(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer func() { _ = tel.Shutdown(context.Background()) }()
ctx, span := tel.StartSpan(context.Background(), "test-operation")
if span == nil {
t.Error("expected span")
}
if ctx == nil {
t.Error("expected context")
}
span.End()
}
func TestGetEnvBool(t *testing.T) {
tests := []struct {
value string
expected bool
}{
{"true", true},
{"TRUE", true},
{"True", true},
{"1", true},
{"yes", true},
{"YES", true},
{"false", false},
{"FALSE", false},
{"0", false},
{"no", false},
{"anything", false},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
os.Setenv("TEST_BOOL", tt.value)
defer os.Unsetenv("TEST_BOOL")
result := envutil.GetEnvBool("TEST_BOOL", false)
if result != tt.expected {
t.Errorf("GetEnvBool(%q) = %v, want %v", tt.value, result, tt.expected)
}
})
}
}
func TestNormalizePath(t *testing.T) {
tests := []struct {
input string
expected string
}{
// Keys
{"/keys/550e8400-e29b-41d4-a716-446655440000", "/keys/{id}"},
{"/keys", "/keys"},
// Projects
{"/projects/pantheon", "/projects/{id}"},
{"/projects/pantheon/claude", "/projects/{id}/claude"},
{"/projects/aeries/shell", "/projects/{id}/shell"},
{"/projects/test-123/events", "/projects/{id}/events"},
// Claude config
{"/projects/pantheon/claude-config/commands/deploy", "/projects/{id}/claude-config/commands/{name}"},
{"/projects/pantheon/claude-config/skills/go-testing", "/projects/{id}/claude-config/skills/{name}"},
{"/projects/pantheon/claude-config/agents/reviewer", "/projects/{id}/claude-config/agents/{name}"},
{"/projects/pantheon/claude-config/commands", "/projects/{id}/claude-config/commands"},
{"/projects/pantheon/claude-config", "/projects/{id}/claude-config"},
// Unchanged
{"/health", "/health"},
{"/ready", "/ready"},
{"/metrics", "/metrics"},
{"/docs", "/docs"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := normalizePath(tt.input)
if result != tt.expected {
t.Errorf("normalizePath(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestMiddleware(t *testing.T) {
// Create a simple handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
// Wrap with telemetry middleware
wrapped := Middleware("test-service")(handler)
// Create test request
req := httptest.NewRequest(http.MethodGet, "/health", nil)
req.Header.Set("X-Real-IP", "192.168.1.1")
rec := httptest.NewRecorder()
// Execute
wrapped.ServeHTTP(rec, req)
// Verify response
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
if rec.Body.String() != "OK" {
t.Errorf("expected body OK, got %s", rec.Body.String())
}
}
func TestMiddlewareWithError(t *testing.T) {
// Create a handler that returns an error
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("error"))
})
wrapped := Middleware("test-service")(handler)
req := httptest.NewRequest(http.MethodPost, "/projects/test/claude", nil)
rec := httptest.NewRecorder()
wrapped.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("expected status 500, got %d", rec.Code)
}
}
func TestResponseWriter(t *testing.T) {
rec := httptest.NewRecorder()
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
// Test WriteHeader
rw.WriteHeader(http.StatusCreated)
if rw.statusCode != http.StatusCreated {
t.Errorf("expected status 201, got %d", rw.statusCode)
}
// Test Write
n, err := rw.Write([]byte("test"))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if n != 4 {
t.Errorf("expected 4 bytes written, got %d", n)
}
if rw.bytesWritten != 4 {
t.Errorf("expected 4 bytes tracked, got %d", rw.bytesWritten)
}
// Test Unwrap
if rw.Unwrap() != rec {
t.Error("Unwrap should return original ResponseWriter")
}
}
func TestGetScheme(t *testing.T) {
tests := []struct {
name string
setup func(*http.Request)
expected string
}{
{
name: "default http",
setup: func(r *http.Request) {},
expected: "http",
},
{
name: "x-forwarded-proto https",
setup: func(r *http.Request) {
r.Header.Set("X-Forwarded-Proto", "https")
},
expected: "https",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
tt.setup(req)
result := getScheme(req)
if result != tt.expected {
t.Errorf("getScheme() = %q, want %q", result, tt.expected)
}
})
}
}