package tracing_test import ( "context" "net/http" "net/http/httptest" "testing" "go.opentelemetry.io/otel" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "github.com/dariuszjaniak/warp/server/internal/tracing" ) // recordSpans installs a tracer provider that keeps every span in memory. func recordSpans(t *testing.T) *tracetest.InMemoryExporter { t.Helper() if _, err := tracing.Setup(t.Context(), tracing.Config{}); err != nil { t.Fatalf("Setup: %v", err) } exporter := tracetest.NewInMemoryExporter() provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter)) previous := otel.GetTracerProvider() otel.SetTracerProvider(provider) t.Cleanup(func() { _ = provider.Shutdown(context.Background()) otel.SetTracerProvider(previous) }) return exporter } func TestInstrumentNamesTheSpanAfterTheRoute(t *testing.T) { exporter := recordSpans(t) const route = "GET /_matrix/client/v3/rooms/{roomID}/state" var inner trace.SpanContext h := tracing.Instrument(route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { inner = trace.SpanContextFromContext(r.Context()) w.WriteHeader(http.StatusTeapot) })) // A request that arrives inside a trace continues it. req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/rooms/!a:b/state", nil) req.Header.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") h.ServeHTTP(httptest.NewRecorder(), req) spans := exporter.GetSpans() if len(spans) != 1 { t.Fatalf("recorded %d spans", len(spans)) } span := spans[0] if span.Name != route { t.Errorf("span name = %q", span.Name) } if span.SpanKind != trace.SpanKindServer { t.Errorf("span kind = %v", span.SpanKind) } if got := span.SpanContext.TraceID().String(); got != "4bf92f3577b34da6a3ce929d0e0e4736" { t.Errorf("trace ID = %s, want the caller's", got) } if !inner.IsValid() || inner.SpanID() != span.SpanContext.SpanID() { t.Error("the handler did not run inside the span") } var status int64 for _, a := range span.Attributes { if a.Key == "http.response.status_code" { status = a.Value.AsInt64() } } if status != http.StatusTeapot { t.Errorf("status attribute = %d", status) } } func TestTransportCarriesTheTraceOn(t *testing.T) { exporter := recordSpans(t) var traceparent string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { traceparent = r.Header.Get("traceparent") })) defer upstream.Close() ctx, parent := tracing.Start(t.Context(), "parent") req, _ := http.NewRequestWithContext(ctx, http.MethodGet, upstream.URL+"/x", nil) resp, err := (&http.Client{Transport: tracing.Transport(nil)}).Do(req) if err != nil { t.Fatalf("request: %v", err) } _ = resp.Body.Close() tracing.End(parent, nil) if traceparent == "" { t.Fatal("the outbound request carried no traceparent") } if req.Header.Get("traceparent") != "" { t.Error("the caller's request was modified") } if spans := exporter.GetSpans(); len(spans) != 2 { t.Errorf("recorded %d spans, want the client span and its parent", len(spans)) } } func TestDisabledTracingIsANoOp(t *testing.T) { stop, err := tracing.Setup(t.Context(), tracing.Config{Enabled: false}) if err != nil { t.Fatalf("Setup: %v", err) } _, span := tracing.Start(t.Context(), "nothing") if span.SpanContext().IsSampled() { t.Error("a span was sampled with tracing off") } tracing.End(span, nil) if err := stop(t.Context()); err != nil { t.Errorf("stop: %v", err) } if _, err := tracing.Setup(t.Context(), tracing.Config{Enabled: true, Endpoint: "not a url"}); err == nil { t.Error("a malformed endpoint was accepted") } }