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>
322 lines
8.1 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|