1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package reverseproxy
16
+
17
+import (
18
+ "context"
19
+ "net"
20
+ "net/http"
21
+ "net/http/httptest"
22
+ "strings"
23
+ "testing"
24
+
25
+ "go.opentelemetry.io/otel/attribute"
26
+ "go.opentelemetry.io/otel/codes"
27
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
28
+ "go.opentelemetry.io/otel/sdk/trace/tracetest"
29
+ "go.opentelemetry.io/otel/trace"
30
+
31
+ "github.com/caddyserver/caddy/v2"
32
+)
33
+
34
+func TestUpstreamClientSpans(t *testing.T) {
35
+ var gotTraceparent []string
36
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37
+ gotTraceparent = append(gotTraceparent, r.Header.Get("Traceparent"))
38
+ w.WriteHeader(http.StatusOK)
39
+ }))
40
+ t.Cleanup(upstream.Close)
41
+
42
+ t.Run("traced request", func(t *testing.T) {
43
+ gotTraceparent = nil
44
+ recorder := tracetest.NewSpanRecorder()
45
+ tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
46
+ t.Cleanup(func() { _ = tp.Shutdown(context.Background()) })
47
+
48
+ // round robin selects index 1 first, which refuses connections, so
49
+ // the retry produces a second client span under the same server span
50
+ h := minimalHandler(1,
51
+ &Upstream{Host: new(Host), Dial: upstream.Listener.Addr().String()},
52
+ &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)},
53
+ )
54
+ // HTTPTransport sets the URL scheme itself, so the span only records
55
+ // it if roundTrip calls SetScheme before the traced transport runs
56
+ caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
57
+ t.Cleanup(cancel)
58
+ ht := new(HTTPTransport)
59
+ if err := ht.Provision(caddyCtx); err != nil {
60
+ t.Fatalf("provisioning transport: %v", err)
61
+ }
62
+ h.Transport = ht
63
+ h.tracedTransport = newTracedTransport(h.Transport)
64
+
65
+ ctx, serverSpan := tp.Tracer("test").Start(context.Background(), "server",
66
+ trace.WithSpanKind(trace.SpanKindServer))
67
+ req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
68
+ req = prepareTestRequest(req)
69
+ rec := httptest.NewRecorder()
70
+ if err := h.ServeHTTP(rec, req, nil); err != nil {
71
+ t.Fatalf("ServeHTTP: %v", err)
72
+ }
73
+ serverSpan.End()
74
+
75
+ var clientSpans []sdktrace.ReadOnlySpan
76
+ for _, s := range recorder.Ended() {
77
+ if s.SpanKind() == trace.SpanKindClient {
78
+ clientSpans = append(clientSpans, s)
79
+ }
80
+ }
81
+ if len(clientSpans) != 2 {
82
+ t.Fatalf("expected 2 client spans, got %d", len(clientSpans))
83
+ }
84
+ for i, s := range clientSpans {
85
+ if s.Parent().SpanID() != serverSpan.SpanContext().SpanID() {
86
+ t.Errorf("client span %d: parent is not the server span", i)
87
+ }
88
+ }
89
+ if clientSpans[0].Status().Code != codes.Error {
90
+ t.Errorf("expected failed attempt to have error status, got %v", clientSpans[0].Status().Code)
91
+ }
92
+
93
+ upstreamHost, upstreamPort, _ := net.SplitHostPort(upstream.Listener.Addr().String())
94
+ attrs := attribute.NewSet(clientSpans[1].Attributes()...)
95
+ for key, want := range map[attribute.Key]string{
96
+ "url.full": "http://" + upstream.Listener.Addr().String() + "/",
97
+ "server.address": upstreamHost,
98
+ "server.port": upstreamPort,
99
+ "http.response.status_code": "200",
100
+ } {
101
+ if got, _ := attrs.Value(key); got.Emit() != want {
102
+ t.Errorf("client span attribute %s: expected %q, got %q", key, want, got.Emit())
103
+ }
104
+ }
105
+
106
+ if len(gotTraceparent) != 1 {
107
+ t.Fatalf("expected 1 upstream request, got %d", len(gotTraceparent))
108
+ }
109
+ want := clientSpans[1].SpanContext().SpanID().String()
110
+ if !strings.Contains(gotTraceparent[0], want) {
111
+ t.Errorf("upstream traceparent %q does not reference client span %s", gotTraceparent[0], want)
112
+ }
113
+ })
114
+
115
+ t.Run("untraced request", func(t *testing.T) {
116
+ gotTraceparent = nil
117
+ h := minimalHandler(0, &Upstream{Host: new(Host), Dial: upstream.Listener.Addr().String()})
118
+ h.tracedTransport = newTracedTransport(h.Transport)
119
+
120
+ req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil))
121
+ if err := h.ServeHTTP(httptest.NewRecorder(), req, nil); err != nil {
122
+ t.Fatalf("ServeHTTP: %v", err)
123
+ }
124
+ if len(gotTraceparent) != 1 || gotTraceparent[0] != "" {
125
+ t.Errorf("expected no traceparent upstream, got %q", gotTraceparent)
126
+ }
127
+ })
128
+}