internal/observability/otel_test.go
Rama main · réplica de lectura del repositorio original.
package observability
import (
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
// traceCollector is a minimal OTLP/gRPC trace receiver.
type traceCollector struct {
coltracepb.UnimplementedTraceServiceServer
spans chan string
}
func (c *traceCollector) Export(_ context.Context, req *coltracepb.ExportTraceServiceRequest) (*coltracepb.ExportTraceServiceResponse, error) {
for _, rs := range req.GetResourceSpans() {
for _, ss := range rs.GetScopeSpans() {
for _, s := range ss.GetSpans() {
c.spans <- s.GetName()
}
}
}
return &coltracepb.ExportTraceServiceResponse{}, nil
}
func startGRPCCollector(t *testing.T) (addr string, spans chan string) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
spans = make(chan string, 16)
srv := grpc.NewServer()
coltracepb.RegisterTraceServiceServer(srv, &traceCollector{spans: spans})
go func() { _ = srv.Serve(lis) }()
t.Cleanup(srv.Stop)
return lis.Addr().String(), spans
}
func startHTTPCollector(t *testing.T) (addr string, spans chan string) {
t.Helper()
spans = make(chan string, 16)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/traces" {
w.WriteHeader(http.StatusOK) // metrics/logs: accept and drop
return
}
body, _ := io.ReadAll(r.Body)
var req coltracepb.ExportTraceServiceRequest
if err := proto.Unmarshal(body, &req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for _, rs := range req.GetResourceSpans() {
for _, ss := range rs.GetScopeSpans() {
for _, s := range ss.GetSpans() {
spans <- s.GetName()
}
}
}
w.Header().Set("Content-Type", "application/x-protobuf")
_, _ = w.Write(nil)
}))
t.Cleanup(srv.Close)
return strings.TrimPrefix(srv.URL, "http://"), spans
}
// exportOneSpan runs Setup with the given endpoint and protocol, records a
// span, flushes via shutdown, and returns the span name the collector saw.
func exportOneSpan(t *testing.T, endpoint, protocol string, spans chan string) string {
t.Helper()
// Setup gates on config.OTLPEndpoint; the exporters read the env var.
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint)
cfg := DefaultConfig()
cfg.OTLPEndpoint = endpoint
cfg.OTLPProtocol = protocol
shutdown, err := Setup(context.Background(), cfg)
if err != nil {
t.Fatalf("Setup: %v", err)
}
_, span := GetTracer("otel-test").Start(context.Background(), "test-span")
span.End()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Metrics/logs may be rejected by these trace-only collectors; only the
// trace delivery matters here.
_ = shutdown(ctx)
select {
case name := <-spans:
return name
case <-time.After(5 * time.Second):
t.Fatal("collector received no span")
return ""
}
}
func TestSetupExportsSpans(t *testing.T) {
tests := []struct {
name string
protocol string
start func(*testing.T) (string, chan string)
endpoint func(addr string) string
}{
{"grpc url", ProtocolGRPC, startGRPCCollector, func(a string) string { return "http://" + a }},
{"grpc bare host:port", ProtocolGRPC, startGRPCCollector, func(a string) string { return a }},
{"http url", ProtocolHTTPProtobuf, startHTTPCollector, func(a string) string { return "http://" + a }},
{"http bare host:port", ProtocolHTTPProtobuf, startHTTPCollector, func(a string) string { return a }},
{"default protocol is http", "", startHTTPCollector, func(a string) string { return "http://" + a }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr, spans := tt.start(t)
if got := exportOneSpan(t, tt.endpoint(addr), tt.protocol, spans); got != "test-span" {
t.Fatalf("span name = %q, want test-span", got)
}
})
}
}
// TestSetupTracesOnlySkipsOtherExporters points Sparrow at a gRPC collector
// that serves traces only (as Jaeger and Tempo do). With OTLPSignals limited
// to traces, no metric or log exporter is created, so shutdown flushes
// cleanly instead of failing with "unknown service ...MetricsService".
func TestSetupTracesOnlySkipsOtherExporters(t *testing.T) {
addr, spans := startGRPCCollector(t)
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://"+addr)
cfg := DefaultConfig()
cfg.OTLPEndpoint = "http://" + addr
cfg.OTLPProtocol = ProtocolGRPC
cfg.OTLPSignals = []string{SignalTraces}
cfg.Prometheus = true
shutdown, err := Setup(context.Background(), cfg)
if err != nil {
t.Fatalf("Setup: %v", err)
}
t.Cleanup(func() { metricsHandler = nil })
_, span := GetTracer("otel-test").Start(context.Background(), "traces-only-span")
span.End()
counter, err := GetMeter("otel-test").Int64Counter("sparrow_traces_only_total")
if err != nil {
t.Fatal(err)
}
counter.Add(context.Background(), 1)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := shutdown(ctx); err != nil {
t.Fatalf("shutdown: %v", err)
}
select {
case name := <-spans:
if name != "traces-only-span" {
t.Fatalf("span name = %q", name)
}
case <-time.After(5 * time.Second):
t.Fatal("collector received no span")
}
if MetricsHandler() == nil {
t.Fatal("Prometheus scrape handler missing: /metrics must work regardless of OTLP signals")
}
}
func TestSetupRejectsUnknownSignal(t *testing.T) {
cfg := DefaultConfig()
cfg.OTLPEndpoint = "http://localhost:4318"
cfg.OTLPSignals = []string{"traces", "spans"}
if _, err := Setup(context.Background(), cfg); err == nil {
t.Fatal("expected error for unknown signal")
}
}
func TestSetupRejectsUnknownProtocol(t *testing.T) {
cfg := DefaultConfig()
cfg.OTLPEndpoint = "http://localhost:4318"
cfg.OTLPProtocol = "http/json"
if _, err := Setup(context.Background(), cfg); err == nil {
t.Fatal("expected error for unsupported protocol")
}
}
func TestSetupServesPrometheusWithoutOTLP(t *testing.T) {
cfg := DefaultConfig()
cfg.Prometheus = true
shutdown, err := Setup(context.Background(), cfg)
if err != nil {
t.Fatalf("Setup: %v", err)
}
t.Cleanup(func() { _ = shutdown(context.Background()); metricsHandler = nil })
handler := MetricsHandler()
if handler == nil {
t.Fatal("MetricsHandler() = nil with Prometheus enabled")
}
counter, err := GetMeter("prom-test").Int64Counter("sparrow_test_things_total")
if err != nil {
t.Fatal(err)
}
counter.Add(context.Background(), 3)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
body := rec.Body.String()
for _, want := range []string{"sparrow_test_things_total", "go_goroutines"} {
if !strings.Contains(body, want) {
t.Errorf("scrape output lacks %s:\n%s", want, body)
}
}
if strings.Contains(body, "_total_total") {
t.Errorf("counter name got a doubled _total suffix:\n%s", body)
}
}
func TestSetupWithoutExportersHasNoMetricsHandler(t *testing.T) {
shutdown, err := Setup(context.Background(), DefaultConfig())
if err != nil {
t.Fatalf("Setup: %v", err)
}
_ = shutdown(context.Background())
if MetricsHandler() != nil {
t.Fatal("MetricsHandler() != nil with Prometheus and OTLP both off")
}
}