@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 406 lines 10.6 KB
Raw
1 package tracing
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "go.opentelemetry.io/otel/sdk/trace"
13 "go.opentelemetry.io/otel/sdk/trace/tracetest"
14
15 "github.com/caddyserver/caddy/v2"
16 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
17 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
18 )
19
20 func TestTracing_UnmarshalCaddyfile(t *testing.T) {
21 tests := []struct {
22 name string
23 spanName string
24 spanAttributes map[string]string
25 d *caddyfile.Dispenser
26 wantErr bool
27 }{
28 {
29 name: "Full config",
30 spanName: "my-span",
31 spanAttributes: map[string]string{
32 "attr1": "value1",
33 "attr2": "value2",
34 },
35 d: caddyfile.NewTestDispenser(`
36 tracing {
37 span my-span
38 span_attributes {
39 attr1 value1
40 attr2 value2
41 }
42 }`),
43 wantErr: false,
44 },
45 {
46 name: "Only span name in the config",
47 spanName: "my-span",
48 d: caddyfile.NewTestDispenser(`
49 tracing {
50 span my-span
51 }`),
52 wantErr: false,
53 },
54 {
55 name: "Empty config",
56 d: caddyfile.NewTestDispenser(`
57 tracing {
58 }`),
59 wantErr: false,
60 },
61 {
62 name: "Only span attributes",
63 spanAttributes: map[string]string{
64 "service.name": "my-service",
65 "service.version": "1.0.0",
66 },
67 d: caddyfile.NewTestDispenser(`
68 tracing {
69 span_attributes {
70 service.name my-service
71 service.version 1.0.0
72 }
73 }`),
74 wantErr: false,
75 },
76 }
77 for _, tt := range tests {
78 t.Run(tt.name, func(t *testing.T) {
79 ot := &Tracing{}
80 if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
81 t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr)
82 }
83
84 if ot.SpanName != tt.spanName {
85 t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName)
86 }
87
88 if len(tt.spanAttributes) > 0 {
89 if ot.SpanAttributes == nil {
90 t.Errorf("UnmarshalCaddyfile() SpanAttributes is nil, expected %v", tt.spanAttributes)
91 } else {
92 for key, expectedValue := range tt.spanAttributes {
93 if actualValue, exists := ot.SpanAttributes[key]; !exists {
94 t.Errorf("UnmarshalCaddyfile() SpanAttributes missing key %v", key)
95 } else if actualValue != expectedValue {
96 t.Errorf("UnmarshalCaddyfile() SpanAttributes[%v] = %v, want %v", key, actualValue, expectedValue)
97 }
98 }
99 }
100 }
101 })
102 }
103 }
104
105 func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) {
106 tests := []struct {
107 name string
108 d *caddyfile.Dispenser
109 wantErr bool
110 }{
111 {
112 name: "Unknown parameter",
113 d: caddyfile.NewTestDispenser(`
114 tracing {
115 foo bar
116 }`),
117 wantErr: true,
118 },
119 {
120 name: "Missed argument",
121 d: caddyfile.NewTestDispenser(`
122 tracing {
123 span
124 }`),
125 wantErr: true,
126 },
127 {
128 name: "Span attributes missing value",
129 d: caddyfile.NewTestDispenser(`
130 tracing {
131 span_attributes {
132 key
133 }
134 }`),
135 wantErr: true,
136 },
137 {
138 name: "Span attributes too many arguments",
139 d: caddyfile.NewTestDispenser(`
140 tracing {
141 span_attributes {
142 key value extra
143 }
144 }`),
145 wantErr: true,
146 },
147 }
148 for _, tt := range tests {
149 t.Run(tt.name, func(t *testing.T) {
150 ot := &Tracing{}
151 if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
152 t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr)
153 }
154 })
155 }
156 }
157
158 func TestTracing_ServeHTTP_Propagation_Without_Initial_Headers(t *testing.T) {
159 ot := &Tracing{
160 SpanName: "mySpan",
161 }
162
163 req := createRequestWithContext("GET", "https://example.com/foo")
164 w := httptest.NewRecorder()
165
166 var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
167 traceparent := request.Header.Get("Traceparent")
168 if traceparent == "" || strings.HasPrefix(traceparent, "00-00000000000000000000000000000000-0000000000000000") {
169 t.Errorf("Invalid traceparent: %v", traceparent)
170 }
171
172 return nil
173 }
174
175 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
176 defer cancel()
177
178 if err := ot.Provision(ctx); err != nil {
179 t.Errorf("Provision error: %v", err)
180 t.FailNow()
181 }
182
183 if err := ot.ServeHTTP(w, req, handler); err != nil {
184 t.Errorf("ServeHTTP error: %v", err)
185 }
186 }
187
188 func TestTracing_ServeHTTP_Propagation_With_Initial_Headers(t *testing.T) {
189 ot := &Tracing{
190 SpanName: "mySpan",
191 }
192
193 req := createRequestWithContext("GET", "https://example.com/foo")
194 req.Header.Set("traceparent", "00-11111111111111111111111111111111-1111111111111111-01")
195 w := httptest.NewRecorder()
196
197 var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
198 traceparent := request.Header.Get("Traceparent")
199 if !strings.HasPrefix(traceparent, "00-11111111111111111111111111111111") {
200 t.Errorf("Invalid traceparent: %v", traceparent)
201 }
202
203 return nil
204 }
205
206 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
207 defer cancel()
208
209 if err := ot.Provision(ctx); err != nil {
210 t.Errorf("Provision error: %v", err)
211 t.FailNow()
212 }
213
214 if err := ot.ServeHTTP(w, req, handler); err != nil {
215 t.Errorf("ServeHTTP error: %v", err)
216 }
217 }
218
219 func TestTracing_ServeHTTP_Next_Error(t *testing.T) {
220 ot := &Tracing{
221 SpanName: "mySpan",
222 }
223
224 req := createRequestWithContext("GET", "https://example.com/foo")
225 w := httptest.NewRecorder()
226
227 expectErr := errors.New("test error")
228
229 var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
230 return expectErr
231 }
232
233 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
234 defer cancel()
235
236 if err := ot.Provision(ctx); err != nil {
237 t.Errorf("Provision error: %v", err)
238 t.FailNow()
239 }
240
241 if err := ot.ServeHTTP(w, req, handler); err == nil || !errors.Is(err, expectErr) {
242 t.Errorf("expected error, got: %v", err)
243 }
244 }
245
246 func TestTracing_JSON_Configuration(t *testing.T) {
247 // Test that our struct correctly marshals to and from JSON
248 original := &Tracing{
249 SpanName: "test-span",
250 SpanAttributes: map[string]string{
251 "service.name": "test-service",
252 "service.version": "1.0.0",
253 "env": "test",
254 },
255 }
256
257 jsonData, err := json.Marshal(original)
258 if err != nil {
259 t.Fatalf("Failed to marshal to JSON: %v", err)
260 }
261
262 var unmarshaled Tracing
263 if err := json.Unmarshal(jsonData, &unmarshaled); err != nil {
264 t.Fatalf("Failed to unmarshal from JSON: %v", err)
265 }
266
267 if unmarshaled.SpanName != original.SpanName {
268 t.Errorf("Expected SpanName %s, got %s", original.SpanName, unmarshaled.SpanName)
269 }
270
271 if len(unmarshaled.SpanAttributes) != len(original.SpanAttributes) {
272 t.Errorf("Expected %d span attributes, got %d", len(original.SpanAttributes), len(unmarshaled.SpanAttributes))
273 }
274
275 for key, expectedValue := range original.SpanAttributes {
276 if actualValue, exists := unmarshaled.SpanAttributes[key]; !exists {
277 t.Errorf("Expected span attribute %s to exist", key)
278 } else if actualValue != expectedValue {
279 t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
280 }
281 }
282
283 t.Logf("JSON representation: %s", string(jsonData))
284 }
285
286 func TestTracing_OpenTelemetry_Span_Attributes(t *testing.T) {
287 // Create an in-memory span recorder to capture actual span data
288 spanRecorder := tracetest.NewSpanRecorder()
289 provider := trace.NewTracerProvider(
290 trace.WithSpanProcessor(spanRecorder),
291 )
292
293 // Create our tracing module with span attributes that include placeholders
294 ot := &Tracing{
295 SpanName: "test-span",
296 SpanAttributes: map[string]string{
297 "static": "test-service",
298 "request-placeholder": "{http.request.method}",
299 "response-placeholder": "{http.response.header.X-Some-Header}",
300 "mixed": "prefix-{http.request.method}-{http.response.header.X-Some-Header}",
301 },
302 }
303
304 // Create a specific request to test against
305 req, _ := http.NewRequest("POST", "https://api.example.com/v1/users?id=123", nil)
306 req.Host = "api.example.com"
307
308 w := httptest.NewRecorder()
309
310 // Set up the replacer
311 repl := caddy.NewReplacer()
312 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
313 ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, make(map[string]any))
314 req = req.WithContext(ctx)
315
316 // Set up request placeholders
317 repl.Set("http.request.method", req.Method)
318 repl.Set("http.request.uri", req.URL.RequestURI())
319
320 // Handler to generate the response
321 var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
322 writer.Header().Set("X-Some-Header", "some-value")
323 writer.WriteHeader(200)
324
325 // Make response headers available to replacer
326 repl.Set("http.response.header.X-Some-Header", writer.Header().Get("X-Some-Header"))
327
328 return nil
329 }
330
331 // Set up Caddy context
332 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
333 defer cancel()
334
335 // Override the global tracer provider with our test provider
336 // This is a bit hacky but necessary to capture the actual spans
337 originalProvider := globalTracerProvider
338 globalTracerProvider = &tracerProvider{
339 tracerProvider: provider,
340 tracerProvidersCounter: 1, // Simulate one user
341 }
342 defer func() {
343 globalTracerProvider = originalProvider
344 }()
345
346 // Provision the tracing module
347 if err := ot.Provision(caddyCtx); err != nil {
348 t.Errorf("Provision error: %v", err)
349 t.FailNow()
350 }
351
352 // Execute the request
353 if err := ot.ServeHTTP(w, req, handler); err != nil {
354 t.Errorf("ServeHTTP error: %v", err)
355 }
356
357 // Get the recorded spans
358 spans := spanRecorder.Ended()
359 if len(spans) == 0 {
360 t.Fatal("Expected at least one span to be recorded")
361 }
362
363 // Find our span (should be the one with our test span name)
364 var testSpan trace.ReadOnlySpan
365 for _, span := range spans {
366 if span.Name() == "test-span" {
367 testSpan = span
368 break
369 }
370 }
371
372 if testSpan == nil {
373 t.Fatal("Could not find test span in recorded spans")
374 }
375
376 // Verify that the span attributes were set correctly with placeholder replacement
377 expectedAttributes := map[string]string{
378 "static": "test-service",
379 "request-placeholder": "POST",
380 "response-placeholder": "some-value",
381 "mixed": "prefix-POST-some-value",
382 }
383
384 actualAttributes := make(map[string]string)
385 for _, attr := range testSpan.Attributes() {
386 actualAttributes[string(attr.Key)] = attr.Value.AsString()
387 }
388
389 for key, expectedValue := range expectedAttributes {
390 if actualValue, exists := actualAttributes[key]; !exists {
391 t.Errorf("Expected span attribute %s to be set", key)
392 } else if actualValue != expectedValue {
393 t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
394 }
395 }
396
397 t.Logf("Recorded span attributes: %+v", actualAttributes)
398 }
399
400 func createRequestWithContext(method string, url string) *http.Request {
401 r, _ := http.NewRequest(method, url, nil)
402 repl := caddy.NewReplacer()
403 ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
404 r = r.WithContext(ctx)
405 return r
406 }