mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-31 08:21:39 -04:00
* reverseproxy: emit OpenTelemetry client spans for upstream requests When the request carries a valid span context (set by the tracing handler), each upstream round trip, including retries, is recorded as a client span and that span's context is propagated to the upstream. Requests that are not traced use the transport directly. The transport is wrapped at the call site rather than replacing h.Transport, so transport interface checks and health checks are unaffected. The tracer provider comes from the span in the request context, so no global provider is needed. Fixes #7223 * reverseproxy: assert client span URL, upstream address and status Use a provisioned HTTPTransport so the test fails if roundTrip stops setting the scheme before the traced transport records url.full. * reverseproxy: add license header to tracing test
129 lines
4.6 KiB
Go
129 lines
4.6 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package reverseproxy
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
func TestUpstreamClientSpans(t *testing.T) {
|
|
var gotTraceparent []string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotTraceparent = append(gotTraceparent, r.Header.Get("Traceparent"))
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(upstream.Close)
|
|
|
|
t.Run("traced request", func(t *testing.T) {
|
|
gotTraceparent = nil
|
|
recorder := tracetest.NewSpanRecorder()
|
|
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
|
|
t.Cleanup(func() { _ = tp.Shutdown(context.Background()) })
|
|
|
|
// round robin selects index 1 first, which refuses connections, so
|
|
// the retry produces a second client span under the same server span
|
|
h := minimalHandler(1,
|
|
&Upstream{Host: new(Host), Dial: upstream.Listener.Addr().String()},
|
|
&Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)},
|
|
)
|
|
// HTTPTransport sets the URL scheme itself, so the span only records
|
|
// it if roundTrip calls SetScheme before the traced transport runs
|
|
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
t.Cleanup(cancel)
|
|
ht := new(HTTPTransport)
|
|
if err := ht.Provision(caddyCtx); err != nil {
|
|
t.Fatalf("provisioning transport: %v", err)
|
|
}
|
|
h.Transport = ht
|
|
h.tracedTransport = newTracedTransport(h.Transport)
|
|
|
|
ctx, serverSpan := tp.Tracer("test").Start(context.Background(), "server",
|
|
trace.WithSpanKind(trace.SpanKindServer))
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
|
|
req = prepareTestRequest(req)
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, req, nil); err != nil {
|
|
t.Fatalf("ServeHTTP: %v", err)
|
|
}
|
|
serverSpan.End()
|
|
|
|
var clientSpans []sdktrace.ReadOnlySpan
|
|
for _, s := range recorder.Ended() {
|
|
if s.SpanKind() == trace.SpanKindClient {
|
|
clientSpans = append(clientSpans, s)
|
|
}
|
|
}
|
|
if len(clientSpans) != 2 {
|
|
t.Fatalf("expected 2 client spans, got %d", len(clientSpans))
|
|
}
|
|
for i, s := range clientSpans {
|
|
if s.Parent().SpanID() != serverSpan.SpanContext().SpanID() {
|
|
t.Errorf("client span %d: parent is not the server span", i)
|
|
}
|
|
}
|
|
if clientSpans[0].Status().Code != codes.Error {
|
|
t.Errorf("expected failed attempt to have error status, got %v", clientSpans[0].Status().Code)
|
|
}
|
|
|
|
upstreamHost, upstreamPort, _ := net.SplitHostPort(upstream.Listener.Addr().String())
|
|
attrs := attribute.NewSet(clientSpans[1].Attributes()...)
|
|
for key, want := range map[attribute.Key]string{
|
|
"url.full": "http://" + upstream.Listener.Addr().String() + "/",
|
|
"server.address": upstreamHost,
|
|
"server.port": upstreamPort,
|
|
"http.response.status_code": "200",
|
|
} {
|
|
if got, _ := attrs.Value(key); got.Emit() != want {
|
|
t.Errorf("client span attribute %s: expected %q, got %q", key, want, got.Emit())
|
|
}
|
|
}
|
|
|
|
if len(gotTraceparent) != 1 {
|
|
t.Fatalf("expected 1 upstream request, got %d", len(gotTraceparent))
|
|
}
|
|
want := clientSpans[1].SpanContext().SpanID().String()
|
|
if !strings.Contains(gotTraceparent[0], want) {
|
|
t.Errorf("upstream traceparent %q does not reference client span %s", gotTraceparent[0], want)
|
|
}
|
|
})
|
|
|
|
t.Run("untraced request", func(t *testing.T) {
|
|
gotTraceparent = nil
|
|
h := minimalHandler(0, &Upstream{Host: new(Host), Dial: upstream.Listener.Addr().String()})
|
|
h.tracedTransport = newTracedTransport(h.Transport)
|
|
|
|
req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil))
|
|
if err := h.ServeHTTP(httptest.NewRecorder(), req, nil); err != nil {
|
|
t.Fatalf("ServeHTTP: %v", err)
|
|
}
|
|
if len(gotTraceparent) != 1 || gotTraceparent[0] != "" {
|
|
t.Errorf("expected no traceparent upstream, got %q", gotTraceparent)
|
|
}
|
|
})
|
|
}
|