1
2
3
4
5
6
7
8
9
10
11
12
13
14 package internal
15
16 import (
17 "crypto/tls"
18 "net/http"
19 "net/url"
20 "strings"
21 "testing"
22
23 "go.opentelemetry.io/otel/trace"
24
25 "github.com/google/go-cmp/cmp"
26 "github.com/stretchr/testify/assert"
27
28 "go.opentelemetry.io/otel/attribute"
29 "go.opentelemetry.io/otel/codes"
30 )
31
32 type tlsOption int
33
34 const (
35 noTLS tlsOption = iota
36 withTLS
37 )
38
39 var sc = &SemanticConventions{
40 EnduserIDKey: attribute.Key("enduser.id"),
41 HTTPClientIPKey: attribute.Key("http.client_ip"),
42 HTTPFlavorKey: attribute.Key("http.flavor"),
43 HTTPHostKey: attribute.Key("http.host"),
44 HTTPMethodKey: attribute.Key("http.method"),
45 HTTPRequestContentLengthKey: attribute.Key("http.request_content_length"),
46 HTTPRouteKey: attribute.Key("http.route"),
47 HTTPSchemeHTTP: attribute.String("http.scheme", "http"),
48 HTTPSchemeHTTPS: attribute.String("http.scheme", "https"),
49 HTTPServerNameKey: attribute.Key("http.server_name"),
50 HTTPStatusCodeKey: attribute.Key("http.status_code"),
51 HTTPTargetKey: attribute.Key("http.target"),
52 HTTPURLKey: attribute.Key("http.url"),
53 HTTPUserAgentKey: attribute.Key("http.user_agent"),
54 NetHostIPKey: attribute.Key("net.host.ip"),
55 NetHostNameKey: attribute.Key("net.host.name"),
56 NetHostPortKey: attribute.Key("net.host.port"),
57 NetPeerIPKey: attribute.Key("net.peer.ip"),
58 NetPeerNameKey: attribute.Key("net.peer.name"),
59 NetPeerPortKey: attribute.Key("net.peer.port"),
60 NetTransportIP: attribute.String("net.transport", "ip"),
61 NetTransportOther: attribute.String("net.transport", "other"),
62 NetTransportTCP: attribute.String("net.transport", "ip_tcp"),
63 NetTransportUDP: attribute.String("net.transport", "ip_udp"),
64 NetTransportUnix: attribute.String("net.transport", "unix"),
65 }
66
67 func TestNetAttributesFromHTTPRequest(t *testing.T) {
68 type testcase struct {
69 name string
70
71 network string
72
73 method string
74 requestURI string
75 proto string
76 remoteAddr string
77 host string
78 url *url.URL
79 header http.Header
80
81 expected []attribute.KeyValue
82 }
83 testcases := []testcase{
84 {
85 name: "stripped, tcp",
86 network: "tcp",
87 method: "GET",
88 requestURI: "/user/123",
89 proto: "HTTP/1.0",
90 remoteAddr: "",
91 host: "",
92 url: &url.URL{
93 Path: "/user/123",
94 },
95 header: nil,
96 expected: []attribute.KeyValue{
97 attribute.String("net.transport", "ip_tcp"),
98 },
99 },
100 {
101 name: "stripped, udp",
102 network: "udp",
103 method: "GET",
104 requestURI: "/user/123",
105 proto: "HTTP/1.0",
106 remoteAddr: "",
107 host: "",
108 url: &url.URL{
109 Path: "/user/123",
110 },
111 header: nil,
112 expected: []attribute.KeyValue{
113 attribute.String("net.transport", "ip_udp"),
114 },
115 },
116 {
117 name: "stripped, ip",
118 network: "ip",
119 method: "GET",
120 requestURI: "/user/123",
121 proto: "HTTP/1.0",
122 remoteAddr: "",
123 host: "",
124 url: &url.URL{
125 Path: "/user/123",
126 },
127 header: nil,
128 expected: []attribute.KeyValue{
129 attribute.String("net.transport", "ip"),
130 },
131 },
132 {
133 name: "stripped, unix",
134 network: "unix",
135 method: "GET",
136 requestURI: "/user/123",
137 proto: "HTTP/1.0",
138 remoteAddr: "",
139 host: "",
140 url: &url.URL{
141 Path: "/user/123",
142 },
143 header: nil,
144 expected: []attribute.KeyValue{
145 attribute.String("net.transport", "unix"),
146 },
147 },
148 {
149 name: "stripped, other",
150 network: "nih",
151 method: "GET",
152 requestURI: "/user/123",
153 proto: "HTTP/1.0",
154 remoteAddr: "",
155 host: "",
156 url: &url.URL{
157 Path: "/user/123",
158 },
159 header: nil,
160 expected: []attribute.KeyValue{
161 attribute.String("net.transport", "other"),
162 },
163 },
164 {
165 name: "with remote ipv4 and port",
166 network: "tcp",
167 method: "GET",
168 requestURI: "/user/123",
169 proto: "HTTP/1.0",
170 remoteAddr: "1.2.3.4:56",
171 host: "",
172 url: &url.URL{
173 Path: "/user/123",
174 },
175 header: nil,
176 expected: []attribute.KeyValue{
177 attribute.String("net.transport", "ip_tcp"),
178 attribute.String("net.peer.ip", "1.2.3.4"),
179 attribute.Int("net.peer.port", 56),
180 },
181 },
182 {
183 name: "with remote ipv6 and port",
184 network: "tcp",
185 method: "GET",
186 requestURI: "/user/123",
187 proto: "HTTP/1.0",
188 remoteAddr: "[fe80::0202:b3ff:fe1e:8329]:56",
189 host: "",
190 url: &url.URL{
191 Path: "/user/123",
192 },
193 header: nil,
194 expected: []attribute.KeyValue{
195 attribute.String("net.transport", "ip_tcp"),
196 attribute.String("net.peer.ip", "fe80::202:b3ff:fe1e:8329"),
197 attribute.Int("net.peer.port", 56),
198 },
199 },
200 {
201 name: "with remote ipv4-in-v6 and port",
202 network: "tcp",
203 method: "GET",
204 requestURI: "/user/123",
205 proto: "HTTP/1.0",
206 remoteAddr: "[::ffff:192.168.0.1]:56",
207 host: "",
208 url: &url.URL{
209 Path: "/user/123",
210 },
211 header: nil,
212 expected: []attribute.KeyValue{
213 attribute.String("net.transport", "ip_tcp"),
214 attribute.String("net.peer.ip", "192.168.0.1"),
215 attribute.Int("net.peer.port", 56),
216 },
217 },
218 {
219 name: "with remote name and port",
220 network: "tcp",
221 method: "GET",
222 requestURI: "/user/123",
223 proto: "HTTP/1.0",
224 remoteAddr: "example.com:56",
225 host: "",
226 url: &url.URL{
227 Path: "/user/123",
228 },
229 header: nil,
230 expected: []attribute.KeyValue{
231 attribute.String("net.transport", "ip_tcp"),
232 attribute.String("net.peer.name", "example.com"),
233 attribute.Int("net.peer.port", 56),
234 },
235 },
236 {
237 name: "with remote ipv4 only",
238 network: "tcp",
239 method: "GET",
240 requestURI: "/user/123",
241 proto: "HTTP/1.0",
242 remoteAddr: "1.2.3.4",
243 host: "",
244 url: &url.URL{
245 Path: "/user/123",
246 },
247 header: nil,
248 expected: []attribute.KeyValue{
249 attribute.String("net.transport", "ip_tcp"),
250 attribute.String("net.peer.ip", "1.2.3.4"),
251 },
252 },
253 {
254 name: "with remote ipv6 only",
255 network: "tcp",
256 method: "GET",
257 requestURI: "/user/123",
258 proto: "HTTP/1.0",
259 remoteAddr: "fe80::0202:b3ff:fe1e:8329",
260 host: "",
261 url: &url.URL{
262 Path: "/user/123",
263 },
264 header: nil,
265 expected: []attribute.KeyValue{
266 attribute.String("net.transport", "ip_tcp"),
267 attribute.String("net.peer.ip", "fe80::202:b3ff:fe1e:8329"),
268 },
269 },
270 {
271 name: "with remote ipv4_in_v6 only",
272 network: "tcp",
273 method: "GET",
274 requestURI: "/user/123",
275 proto: "HTTP/1.0",
276 remoteAddr: "::ffff:192.168.0.1",
277 host: "",
278 url: &url.URL{
279 Path: "/user/123",
280 },
281 header: nil,
282 expected: []attribute.KeyValue{
283 attribute.String("net.transport", "ip_tcp"),
284 attribute.String("net.peer.ip", "192.168.0.1"),
285 },
286 },
287 {
288 name: "with remote name only",
289 network: "tcp",
290 method: "GET",
291 requestURI: "/user/123",
292 proto: "HTTP/1.0",
293 remoteAddr: "example.com",
294 host: "",
295 url: &url.URL{
296 Path: "/user/123",
297 },
298 header: nil,
299 expected: []attribute.KeyValue{
300 attribute.String("net.transport", "ip_tcp"),
301 attribute.String("net.peer.name", "example.com"),
302 },
303 },
304 {
305 name: "with remote port only",
306 network: "tcp",
307 method: "GET",
308 requestURI: "/user/123",
309 proto: "HTTP/1.0",
310 remoteAddr: ":56",
311 host: "",
312 url: &url.URL{
313 Path: "/user/123",
314 },
315 header: nil,
316 expected: []attribute.KeyValue{
317 attribute.String("net.transport", "ip_tcp"),
318 attribute.Int("net.peer.port", 56),
319 },
320 },
321 {
322 name: "with host name only",
323 network: "tcp",
324 method: "GET",
325 requestURI: "/user/123",
326 proto: "HTTP/1.0",
327 remoteAddr: "1.2.3.4:56",
328 host: "example.com",
329 url: &url.URL{
330 Path: "/user/123",
331 },
332 header: nil,
333 expected: []attribute.KeyValue{
334 attribute.String("net.transport", "ip_tcp"),
335 attribute.String("net.peer.ip", "1.2.3.4"),
336 attribute.Int("net.peer.port", 56),
337 attribute.String("net.host.name", "example.com"),
338 },
339 },
340 {
341 name: "with host ipv4 only",
342 network: "tcp",
343 method: "GET",
344 requestURI: "/user/123",
345 proto: "HTTP/1.0",
346 remoteAddr: "1.2.3.4:56",
347 host: "4.3.2.1",
348 url: &url.URL{
349 Path: "/user/123",
350 },
351 header: nil,
352 expected: []attribute.KeyValue{
353 attribute.String("net.transport", "ip_tcp"),
354 attribute.String("net.peer.ip", "1.2.3.4"),
355 attribute.Int("net.peer.port", 56),
356 attribute.String("net.host.ip", "4.3.2.1"),
357 },
358 },
359 {
360 name: "with host ipv6 only",
361 network: "tcp",
362 method: "GET",
363 requestURI: "/user/123",
364 proto: "HTTP/1.0",
365 remoteAddr: "1.2.3.4:56",
366 host: "fe80::0202:b3ff:fe1e:8329",
367 url: &url.URL{
368 Path: "/user/123",
369 },
370 header: nil,
371 expected: []attribute.KeyValue{
372 attribute.String("net.transport", "ip_tcp"),
373 attribute.String("net.peer.ip", "1.2.3.4"),
374 attribute.Int("net.peer.port", 56),
375 attribute.String("net.host.ip", "fe80::202:b3ff:fe1e:8329"),
376 },
377 },
378 {
379 name: "with host name and port",
380 network: "tcp",
381 method: "GET",
382 requestURI: "/user/123",
383 proto: "HTTP/1.0",
384 remoteAddr: "1.2.3.4:56",
385 host: "example.com:78",
386 url: &url.URL{
387 Path: "/user/123",
388 },
389 header: nil,
390 expected: []attribute.KeyValue{
391 attribute.String("net.transport", "ip_tcp"),
392 attribute.String("net.peer.ip", "1.2.3.4"),
393 attribute.Int("net.peer.port", 56),
394 attribute.String("net.host.name", "example.com"),
395 attribute.Int("net.host.port", 78),
396 },
397 },
398 {
399 name: "with host ipv4 and port",
400 network: "tcp",
401 method: "GET",
402 requestURI: "/user/123",
403 proto: "HTTP/1.0",
404 remoteAddr: "1.2.3.4:56",
405 host: "4.3.2.1:78",
406 url: &url.URL{
407 Path: "/user/123",
408 },
409 header: nil,
410 expected: []attribute.KeyValue{
411 attribute.String("net.transport", "ip_tcp"),
412 attribute.String("net.peer.ip", "1.2.3.4"),
413 attribute.Int("net.peer.port", 56),
414 attribute.String("net.host.ip", "4.3.2.1"),
415 attribute.Int("net.host.port", 78),
416 },
417 },
418 {
419 name: "with host ipv6 and port",
420 network: "tcp",
421 method: "GET",
422 requestURI: "/user/123",
423 proto: "HTTP/1.0",
424 remoteAddr: "1.2.3.4:56",
425 host: "[fe80::202:b3ff:fe1e:8329]:78",
426 url: &url.URL{
427 Path: "/user/123",
428 },
429 header: nil,
430 expected: []attribute.KeyValue{
431 attribute.String("net.transport", "ip_tcp"),
432 attribute.String("net.peer.ip", "1.2.3.4"),
433 attribute.Int("net.peer.port", 56),
434 attribute.String("net.host.ip", "fe80::202:b3ff:fe1e:8329"),
435 attribute.Int("net.host.port", 78),
436 },
437 },
438 {
439 name: "with host name and bogus port",
440 network: "tcp",
441 method: "GET",
442 requestURI: "/user/123",
443 proto: "HTTP/1.0",
444 remoteAddr: "1.2.3.4:56",
445 host: "example.com:qwerty",
446 url: &url.URL{
447 Path: "/user/123",
448 },
449 header: nil,
450 expected: []attribute.KeyValue{
451 attribute.String("net.transport", "ip_tcp"),
452 attribute.String("net.peer.ip", "1.2.3.4"),
453 attribute.Int("net.peer.port", 56),
454 attribute.String("net.host.name", "example.com"),
455 },
456 },
457 {
458 name: "with host ipv4 and bogus port",
459 network: "tcp",
460 method: "GET",
461 requestURI: "/user/123",
462 proto: "HTTP/1.0",
463 remoteAddr: "1.2.3.4:56",
464 host: "4.3.2.1:qwerty",
465 url: &url.URL{
466 Path: "/user/123",
467 },
468 header: nil,
469 expected: []attribute.KeyValue{
470 attribute.String("net.transport", "ip_tcp"),
471 attribute.String("net.peer.ip", "1.2.3.4"),
472 attribute.Int("net.peer.port", 56),
473 attribute.String("net.host.ip", "4.3.2.1"),
474 },
475 },
476 {
477 name: "with host ipv6 and bogus port",
478 network: "tcp",
479 method: "GET",
480 requestURI: "/user/123",
481 proto: "HTTP/1.0",
482 remoteAddr: "1.2.3.4:56",
483 host: "[fe80::202:b3ff:fe1e:8329]:qwerty",
484 url: &url.URL{
485 Path: "/user/123",
486 },
487 header: nil,
488 expected: []attribute.KeyValue{
489 attribute.String("net.transport", "ip_tcp"),
490 attribute.String("net.peer.ip", "1.2.3.4"),
491 attribute.Int("net.peer.port", 56),
492 attribute.String("net.host.ip", "fe80::202:b3ff:fe1e:8329"),
493 },
494 },
495 {
496 name: "with empty host and port",
497 network: "tcp",
498 method: "GET",
499 requestURI: "/user/123",
500 proto: "HTTP/1.0",
501 remoteAddr: "1.2.3.4:56",
502 host: ":80",
503 url: &url.URL{
504 Path: "/user/123",
505 },
506 header: nil,
507 expected: []attribute.KeyValue{
508 attribute.String("net.transport", "ip_tcp"),
509 attribute.String("net.peer.ip", "1.2.3.4"),
510 attribute.Int("net.peer.port", 56),
511 attribute.Int("net.host.port", 80),
512 },
513 },
514 {
515 name: "with host ip and port in headers",
516 network: "tcp",
517 method: "GET",
518 requestURI: "/user/123",
519 proto: "HTTP/1.0",
520 remoteAddr: "1.2.3.4:56",
521 host: "",
522 url: &url.URL{
523 Path: "/user/123",
524 },
525 header: http.Header{
526 "Host": []string{"4.3.2.1:78"},
527 },
528 expected: []attribute.KeyValue{
529 attribute.String("net.transport", "ip_tcp"),
530 attribute.String("net.peer.ip", "1.2.3.4"),
531 attribute.Int("net.peer.port", 56),
532 attribute.String("net.host.ip", "4.3.2.1"),
533 attribute.Int("net.host.port", 78),
534 },
535 },
536 {
537 name: "with host ipv4 and port in url",
538 network: "tcp",
539 method: "GET",
540 requestURI: "http://4.3.2.1:78/user/123",
541 proto: "HTTP/1.0",
542 remoteAddr: "1.2.3.4:56",
543 host: "",
544 url: &url.URL{
545 Host: "4.3.2.1:78",
546 Path: "/user/123",
547 },
548 header: nil,
549 expected: []attribute.KeyValue{
550 attribute.String("net.transport", "ip_tcp"),
551 attribute.String("net.peer.ip", "1.2.3.4"),
552 attribute.Int("net.peer.port", 56),
553 attribute.String("net.host.ip", "4.3.2.1"),
554 attribute.Int("net.host.port", 78),
555 },
556 },
557 {
558 name: "with host ipv6 and port in url",
559 network: "tcp",
560 method: "GET",
561 requestURI: "http://4.3.2.1:78/user/123",
562 proto: "HTTP/1.0",
563 remoteAddr: "1.2.3.4:56",
564 host: "",
565 url: &url.URL{
566 Host: "[fe80::202:b3ff:fe1e:8329]:78",
567 Path: "/user/123",
568 },
569 header: nil,
570 expected: []attribute.KeyValue{
571 attribute.String("net.transport", "ip_tcp"),
572 attribute.String("net.peer.ip", "1.2.3.4"),
573 attribute.Int("net.peer.port", 56),
574 attribute.String("net.host.ip", "fe80::202:b3ff:fe1e:8329"),
575 attribute.Int("net.host.port", 78),
576 },
577 },
578 }
579 for _, tc := range testcases {
580 t.Run(tc.name, func(t *testing.T) {
581 r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, noTLS)
582 got := sc.NetAttributesFromHTTPRequest(tc.network, r)
583 if diff := cmp.Diff(
584 tc.expected,
585 got,
586 cmp.AllowUnexported(attribute.Value{})); diff != "" {
587 t.Fatalf("attributes differ: diff %+v,", diff)
588 }
589 })
590 }
591 }
592
593 func TestEndUserAttributesFromHTTPRequest(t *testing.T) {
594 r := testRequest("GET", "/user/123", "HTTP/1.1", "", "", nil, http.Header{}, withTLS)
595 var expected []attribute.KeyValue
596 got := sc.EndUserAttributesFromHTTPRequest(r)
597 assert.ElementsMatch(t, expected, got)
598 r.SetBasicAuth("admin", "password")
599 expected = []attribute.KeyValue{attribute.String("enduser.id", "admin")}
600 got = sc.EndUserAttributesFromHTTPRequest(r)
601 assert.ElementsMatch(t, expected, got)
602 }
603
604 func TestHTTPServerAttributesFromHTTPRequest(t *testing.T) {
605 type testcase struct {
606 name string
607
608 serverName string
609 route string
610
611 method string
612 requestURI string
613 proto string
614 remoteAddr string
615 host string
616 url *url.URL
617 header http.Header
618 tls tlsOption
619 contentLength int64
620
621 expected []attribute.KeyValue
622 }
623 testcases := []testcase{
624 {
625 name: "stripped",
626 serverName: "",
627 route: "",
628 method: "GET",
629 requestURI: "/user/123",
630 proto: "HTTP/1.0",
631 remoteAddr: "",
632 host: "",
633 url: &url.URL{
634 Path: "/user/123",
635 },
636 header: nil,
637 tls: noTLS,
638 expected: []attribute.KeyValue{
639 attribute.String("http.method", "GET"),
640 attribute.String("http.target", "/user/123"),
641 attribute.String("http.scheme", "http"),
642 attribute.String("http.flavor", "1.0"),
643 },
644 },
645 {
646 name: "with server name",
647 serverName: "my-server-name",
648 route: "",
649 method: "GET",
650 requestURI: "/user/123",
651 proto: "HTTP/1.0",
652 remoteAddr: "",
653 host: "",
654 url: &url.URL{
655 Path: "/user/123",
656 },
657 header: nil,
658 tls: noTLS,
659 expected: []attribute.KeyValue{
660 attribute.String("http.method", "GET"),
661 attribute.String("http.target", "/user/123"),
662 attribute.String("http.scheme", "http"),
663 attribute.String("http.flavor", "1.0"),
664 attribute.String("http.server_name", "my-server-name"),
665 },
666 },
667 {
668 name: "with tls",
669 serverName: "my-server-name",
670 route: "",
671 method: "GET",
672 requestURI: "/user/123",
673 proto: "HTTP/1.0",
674 remoteAddr: "",
675 host: "",
676 url: &url.URL{
677 Path: "/user/123",
678 },
679 header: nil,
680 tls: withTLS,
681 expected: []attribute.KeyValue{
682 attribute.String("http.method", "GET"),
683 attribute.String("http.target", "/user/123"),
684 attribute.String("http.scheme", "https"),
685 attribute.String("http.flavor", "1.0"),
686 attribute.String("http.server_name", "my-server-name"),
687 },
688 },
689 {
690 name: "with route",
691 serverName: "my-server-name",
692 route: "/user/:id",
693 method: "GET",
694 requestURI: "/user/123",
695 proto: "HTTP/1.0",
696 remoteAddr: "",
697 host: "",
698 url: &url.URL{
699 Path: "/user/123",
700 },
701 header: nil,
702 tls: withTLS,
703 expected: []attribute.KeyValue{
704 attribute.String("http.method", "GET"),
705 attribute.String("http.target", "/user/123"),
706 attribute.String("http.scheme", "https"),
707 attribute.String("http.flavor", "1.0"),
708 attribute.String("http.server_name", "my-server-name"),
709 attribute.String("http.route", "/user/:id"),
710 },
711 },
712 {
713 name: "with host",
714 serverName: "my-server-name",
715 route: "/user/:id",
716 method: "GET",
717 requestURI: "/user/123",
718 proto: "HTTP/1.0",
719 remoteAddr: "",
720 host: "example.com",
721 url: &url.URL{
722 Path: "/user/123",
723 },
724 header: nil,
725 tls: withTLS,
726 expected: []attribute.KeyValue{
727 attribute.String("http.method", "GET"),
728 attribute.String("http.target", "/user/123"),
729 attribute.String("http.scheme", "https"),
730 attribute.String("http.flavor", "1.0"),
731 attribute.String("http.server_name", "my-server-name"),
732 attribute.String("http.route", "/user/:id"),
733 attribute.String("http.host", "example.com"),
734 },
735 },
736 {
737 name: "with host fallback",
738 serverName: "my-server-name",
739 route: "/user/:id",
740 method: "GET",
741 requestURI: "/user/123",
742 proto: "HTTP/1.0",
743 remoteAddr: "",
744 host: "",
745 url: &url.URL{
746 Host: "example.com",
747 Path: "/user/123",
748 },
749 header: nil,
750 tls: withTLS,
751 expected: []attribute.KeyValue{
752 attribute.String("http.method", "GET"),
753 attribute.String("http.target", "/user/123"),
754 attribute.String("http.scheme", "https"),
755 attribute.String("http.flavor", "1.0"),
756 attribute.String("http.server_name", "my-server-name"),
757 attribute.String("http.route", "/user/:id"),
758 attribute.String("http.host", "example.com"),
759 },
760 },
761 {
762 name: "with user agent",
763 serverName: "my-server-name",
764 route: "/user/:id",
765 method: "GET",
766 requestURI: "/user/123",
767 proto: "HTTP/1.0",
768 remoteAddr: "",
769 host: "example.com",
770 url: &url.URL{
771 Path: "/user/123",
772 },
773 header: http.Header{
774 "User-Agent": []string{"foodownloader"},
775 },
776 tls: withTLS,
777 expected: []attribute.KeyValue{
778 attribute.String("http.method", "GET"),
779 attribute.String("http.target", "/user/123"),
780 attribute.String("http.scheme", "https"),
781 attribute.String("http.flavor", "1.0"),
782 attribute.String("http.server_name", "my-server-name"),
783 attribute.String("http.route", "/user/:id"),
784 attribute.String("http.host", "example.com"),
785 attribute.String("http.user_agent", "foodownloader"),
786 },
787 },
788 {
789 name: "with proxy info",
790 serverName: "my-server-name",
791 route: "/user/:id",
792 method: "GET",
793 requestURI: "/user/123",
794 proto: "HTTP/1.0",
795 remoteAddr: "",
796 host: "example.com",
797 url: &url.URL{
798 Path: "/user/123",
799 },
800 header: http.Header{
801 "User-Agent": []string{"foodownloader"},
802 "X-Forwarded-For": []string{"203.0.113.195, 70.41.3.18, 150.172.238.178"},
803 },
804 tls: withTLS,
805 expected: []attribute.KeyValue{
806 attribute.String("http.method", "GET"),
807 attribute.String("http.target", "/user/123"),
808 attribute.String("http.scheme", "https"),
809 attribute.String("http.flavor", "1.0"),
810 attribute.String("http.server_name", "my-server-name"),
811 attribute.String("http.route", "/user/:id"),
812 attribute.String("http.host", "example.com"),
813 attribute.String("http.user_agent", "foodownloader"),
814 attribute.String("http.client_ip", "203.0.113.195"),
815 },
816 },
817 {
818 name: "with http 1.1",
819 serverName: "my-server-name",
820 route: "/user/:id",
821 method: "GET",
822 requestURI: "/user/123",
823 proto: "HTTP/1.1",
824 remoteAddr: "",
825 host: "example.com",
826 url: &url.URL{
827 Path: "/user/123",
828 },
829 header: http.Header{
830 "User-Agent": []string{"foodownloader"},
831 "X-Forwarded-For": []string{"1.2.3.4"},
832 },
833 tls: withTLS,
834 expected: []attribute.KeyValue{
835 attribute.String("http.method", "GET"),
836 attribute.String("http.target", "/user/123"),
837 attribute.String("http.scheme", "https"),
838 attribute.String("http.flavor", "1.1"),
839 attribute.String("http.server_name", "my-server-name"),
840 attribute.String("http.route", "/user/:id"),
841 attribute.String("http.host", "example.com"),
842 attribute.String("http.user_agent", "foodownloader"),
843 attribute.String("http.client_ip", "1.2.3.4"),
844 },
845 },
846 {
847 name: "with http 2",
848 serverName: "my-server-name",
849 route: "/user/:id",
850 method: "GET",
851 requestURI: "/user/123",
852 proto: "HTTP/2.0",
853 remoteAddr: "",
854 host: "example.com",
855 url: &url.URL{
856 Path: "/user/123",
857 },
858 header: http.Header{
859 "User-Agent": []string{"foodownloader"},
860 "X-Forwarded-For": []string{"1.2.3.4"},
861 },
862 tls: withTLS,
863 expected: []attribute.KeyValue{
864 attribute.String("http.method", "GET"),
865 attribute.String("http.target", "/user/123"),
866 attribute.String("http.scheme", "https"),
867 attribute.String("http.flavor", "2"),
868 attribute.String("http.server_name", "my-server-name"),
869 attribute.String("http.route", "/user/:id"),
870 attribute.String("http.host", "example.com"),
871 attribute.String("http.user_agent", "foodownloader"),
872 attribute.String("http.client_ip", "1.2.3.4"),
873 },
874 },
875 {
876 name: "with content length",
877 method: "GET",
878 requestURI: "/user/123",
879 contentLength: 100,
880 expected: []attribute.KeyValue{
881 attribute.String("http.method", "GET"),
882 attribute.String("http.target", "/user/123"),
883 attribute.String("http.scheme", "http"),
884 attribute.Int64("http.request_content_length", 100),
885 },
886 },
887 }
888 for idx, tc := range testcases {
889 r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls)
890 r.ContentLength = tc.contentLength
891 got := sc.HTTPServerAttributesFromHTTPRequest(tc.serverName, tc.route, r)
892 assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name)
893 }
894 }
895
896 func TestHTTPAttributesFromHTTPStatusCode(t *testing.T) {
897 expected := []attribute.KeyValue{
898 attribute.Int("http.status_code", 404),
899 }
900 got := sc.HTTPAttributesFromHTTPStatusCode(http.StatusNotFound)
901 assertElementsMatch(t, expected, got, "with valid HTTP status code")
902 assert.ElementsMatch(t, expected, got)
903 expected = []attribute.KeyValue{
904 attribute.Int("http.status_code", 499),
905 }
906 got = sc.HTTPAttributesFromHTTPStatusCode(499)
907 assertElementsMatch(t, expected, got, "with invalid HTTP status code")
908 }
909
910 func TestSpanStatusFromHTTPStatusCode(t *testing.T) {
911 for code := 0; code < 1000; code++ {
912 expected := getExpectedCodeForHTTPCode(code, trace.SpanKindClient)
913 got, msg := SpanStatusFromHTTPStatusCode(code)
914 assert.Equalf(t, expected, got, "%s vs %s", expected, got)
915
916 _, valid := validateHTTPStatusCode(code)
917 if !valid {
918 assert.NotEmpty(t, msg, "message should be set if error cannot be inferred from code")
919 } else {
920 assert.Empty(t, msg, "message should not be set if error can be inferred from code")
921 }
922 }
923 }
924
925 func TestSpanStatusFromHTTPStatusCodeAndSpanKind(t *testing.T) {
926 for code := 0; code < 1000; code++ {
927 expected := getExpectedCodeForHTTPCode(code, trace.SpanKindClient)
928 got, msg := SpanStatusFromHTTPStatusCodeAndSpanKind(code, trace.SpanKindClient)
929 assert.Equalf(t, expected, got, "%s vs %s", expected, got)
930
931 _, valid := validateHTTPStatusCode(code)
932 if !valid {
933 assert.NotEmpty(t, msg, "message should be set if error cannot be inferred from code")
934 } else {
935 assert.Empty(t, msg, "message should not be set if error can be inferred from code")
936 }
937 }
938 code, _ := SpanStatusFromHTTPStatusCodeAndSpanKind(400, trace.SpanKindServer)
939 assert.Equalf(t, codes.Unset, code, "message should be set if error cannot be inferred from code")
940 }
941
942 func getExpectedCodeForHTTPCode(code int, spanKind trace.SpanKind) codes.Code {
943 if http.StatusText(code) == "" {
944 return codes.Error
945 }
946 switch code {
947 case
948 http.StatusUnauthorized,
949 http.StatusForbidden,
950 http.StatusNotFound,
951 http.StatusTooManyRequests,
952 http.StatusNotImplemented,
953 http.StatusServiceUnavailable,
954 http.StatusGatewayTimeout:
955 return codes.Error
956 }
957 category := code / 100
958 if category > 0 && category < 4 {
959 return codes.Unset
960 }
961 if spanKind == trace.SpanKindServer && category == 4 {
962 return codes.Unset
963 }
964 return codes.Error
965 }
966
967 func assertElementsMatch(t *testing.T, expected, got []attribute.KeyValue, format string, args ...interface{}) {
968 if !assert.ElementsMatchf(t, expected, got, format, args...) {
969 t.Log("expected:", kvStr(expected))
970 t.Log("got:", kvStr(got))
971 }
972 }
973
974 func testRequest(method, requestURI, proto, remoteAddr, host string, u *url.URL, header http.Header, tlsopt tlsOption) *http.Request {
975 major, minor := protoToInts(proto)
976 var tlsConn *tls.ConnectionState
977 switch tlsopt {
978 case noTLS:
979 case withTLS:
980 tlsConn = &tls.ConnectionState{}
981 }
982 return &http.Request{
983 Method: method,
984 URL: u,
985 Proto: proto,
986 ProtoMajor: major,
987 ProtoMinor: minor,
988 Header: header,
989 Host: host,
990 RemoteAddr: remoteAddr,
991 RequestURI: requestURI,
992 TLS: tlsConn,
993 }
994 }
995
996 func protoToInts(proto string) (int, int) {
997 switch proto {
998 case "HTTP/1.0":
999 return 1, 0
1000 case "HTTP/1.1":
1001 return 1, 1
1002 case "HTTP/2.0":
1003 return 2, 0
1004 }
1005
1006 return 13, 42
1007 }
1008
1009 func kvStr(kvs []attribute.KeyValue) string {
1010 sb := strings.Builder{}
1011 _, _ = sb.WriteRune('[')
1012 for idx, attr := range kvs {
1013 if idx > 0 {
1014 _, _ = sb.WriteString(", ")
1015 }
1016 _, _ = sb.WriteString((string)(attr.Key))
1017 _, _ = sb.WriteString(": ")
1018 _, _ = sb.WriteString(attr.Value.Emit())
1019 }
1020 _, _ = sb.WriteRune(']')
1021 return sb.String()
1022 }
1023
1024 func TestHTTPClientAttributesFromHTTPRequest(t *testing.T) {
1025 testCases := []struct {
1026 name string
1027
1028 method string
1029 requestURI string
1030 proto string
1031 remoteAddr string
1032 host string
1033 url *url.URL
1034 header http.Header
1035 tls tlsOption
1036 contentLength int64
1037
1038 expected []attribute.KeyValue
1039 }{
1040 {
1041 name: "stripped",
1042 method: "GET",
1043 requestURI: "/user/123",
1044 proto: "HTTP/1.0",
1045 remoteAddr: "",
1046 host: "",
1047 url: &url.URL{
1048 Path: "/user/123",
1049 },
1050 header: nil,
1051 tls: noTLS,
1052 expected: []attribute.KeyValue{
1053 attribute.String("http.method", "GET"),
1054 attribute.String("http.url", "/user/123"),
1055 attribute.String("http.scheme", "http"),
1056 attribute.String("http.flavor", "1.0"),
1057 },
1058 },
1059 {
1060 name: "with tls",
1061 method: "GET",
1062 requestURI: "/user/123",
1063 proto: "HTTP/1.0",
1064 remoteAddr: "",
1065 host: "",
1066 url: &url.URL{
1067 Path: "/user/123",
1068 },
1069 header: nil,
1070 tls: withTLS,
1071 expected: []attribute.KeyValue{
1072 attribute.String("http.method", "GET"),
1073 attribute.String("http.url", "/user/123"),
1074 attribute.String("http.scheme", "https"),
1075 attribute.String("http.flavor", "1.0"),
1076 },
1077 },
1078 {
1079 name: "with host",
1080 method: "GET",
1081 requestURI: "/user/123",
1082 proto: "HTTP/1.0",
1083 remoteAddr: "",
1084 host: "example.com",
1085 url: &url.URL{
1086 Path: "/user/123",
1087 },
1088 header: nil,
1089 tls: withTLS,
1090 expected: []attribute.KeyValue{
1091 attribute.String("http.method", "GET"),
1092 attribute.String("http.url", "/user/123"),
1093 attribute.String("http.scheme", "https"),
1094 attribute.String("http.flavor", "1.0"),
1095 attribute.String("http.host", "example.com"),
1096 },
1097 },
1098 {
1099 name: "with host fallback",
1100 method: "GET",
1101 requestURI: "/user/123",
1102 proto: "HTTP/1.0",
1103 remoteAddr: "",
1104 host: "",
1105 url: &url.URL{
1106 Scheme: "https",
1107 Host: "example.com",
1108 Path: "/user/123",
1109 },
1110 header: nil,
1111 tls: withTLS,
1112 expected: []attribute.KeyValue{
1113 attribute.String("http.method", "GET"),
1114 attribute.String("http.url", "https://example.com/user/123"),
1115 attribute.String("http.scheme", "https"),
1116 attribute.String("http.flavor", "1.0"),
1117 attribute.String("http.host", "example.com"),
1118 },
1119 },
1120 {
1121 name: "with user agent",
1122 method: "GET",
1123 requestURI: "/user/123",
1124 proto: "HTTP/1.0",
1125 remoteAddr: "",
1126 host: "example.com",
1127 url: &url.URL{
1128 Path: "/user/123",
1129 },
1130 header: http.Header{
1131 "User-Agent": []string{"foodownloader"},
1132 },
1133 tls: withTLS,
1134 expected: []attribute.KeyValue{
1135 attribute.String("http.method", "GET"),
1136 attribute.String("http.url", "/user/123"),
1137 attribute.String("http.scheme", "https"),
1138 attribute.String("http.flavor", "1.0"),
1139 attribute.String("http.host", "example.com"),
1140 attribute.String("http.user_agent", "foodownloader"),
1141 },
1142 },
1143 {
1144 name: "with http 1.1",
1145 method: "GET",
1146 requestURI: "/user/123",
1147 proto: "HTTP/1.1",
1148 remoteAddr: "",
1149 host: "example.com",
1150 url: &url.URL{
1151 Path: "/user/123",
1152 },
1153 header: http.Header{
1154 "User-Agent": []string{"foodownloader"},
1155 },
1156 tls: withTLS,
1157 expected: []attribute.KeyValue{
1158 attribute.String("http.method", "GET"),
1159 attribute.String("http.url", "/user/123"),
1160 attribute.String("http.scheme", "https"),
1161 attribute.String("http.flavor", "1.1"),
1162 attribute.String("http.host", "example.com"),
1163 attribute.String("http.user_agent", "foodownloader"),
1164 },
1165 },
1166 {
1167 name: "with http 2",
1168 method: "GET",
1169 requestURI: "/user/123",
1170 proto: "HTTP/2.0",
1171 remoteAddr: "",
1172 host: "example.com",
1173 url: &url.URL{
1174 Path: "/user/123",
1175 },
1176 header: http.Header{
1177 "User-Agent": []string{"foodownloader"},
1178 },
1179 tls: withTLS,
1180 expected: []attribute.KeyValue{
1181 attribute.String("http.method", "GET"),
1182 attribute.String("http.url", "/user/123"),
1183 attribute.String("http.scheme", "https"),
1184 attribute.String("http.flavor", "2"),
1185 attribute.String("http.host", "example.com"),
1186 attribute.String("http.user_agent", "foodownloader"),
1187 },
1188 },
1189 {
1190 name: "with content length",
1191 method: "GET",
1192 url: &url.URL{
1193 Path: "/user/123",
1194 },
1195 contentLength: 100,
1196 expected: []attribute.KeyValue{
1197 attribute.String("http.method", "GET"),
1198 attribute.String("http.url", "/user/123"),
1199 attribute.String("http.scheme", "http"),
1200 attribute.Int64("http.request_content_length", 100),
1201 },
1202 },
1203 {
1204 name: "with empty method (fallback to GET)",
1205 method: "",
1206 url: &url.URL{
1207 Path: "/user/123",
1208 },
1209 expected: []attribute.KeyValue{
1210 attribute.String("http.method", "GET"),
1211 attribute.String("http.url", "/user/123"),
1212 attribute.String("http.scheme", "http"),
1213 },
1214 },
1215 {
1216 name: "authentication information is stripped",
1217 method: "",
1218 url: &url.URL{
1219 Path: "/user/123",
1220 User: url.UserPassword("foo", "bar"),
1221 },
1222 expected: []attribute.KeyValue{
1223 attribute.String("http.method", "GET"),
1224 attribute.String("http.url", "/user/123"),
1225 attribute.String("http.scheme", "http"),
1226 },
1227 },
1228 }
1229
1230 for _, tc := range testCases {
1231 t.Run(tc.name, func(t *testing.T) {
1232 r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls)
1233 r.ContentLength = tc.contentLength
1234 got := sc.HTTPClientAttributesFromHTTPRequest(r)
1235 assert.ElementsMatch(t, tc.expected, got)
1236 })
1237 }
1238 }
1239
1240 func TestHTTPServerMetricAttributesFromHTTPRequest(t *testing.T) {
1241 type testcase struct {
1242 name string
1243 serverName string
1244 method string
1245 requestURI string
1246 proto string
1247 remoteAddr string
1248 host string
1249 url *url.URL
1250 header http.Header
1251 tls tlsOption
1252 contentLength int64
1253 expected []attribute.KeyValue
1254 }
1255 testcases := []testcase{
1256 {
1257 name: "stripped",
1258 serverName: "",
1259 method: "GET",
1260 requestURI: "/user/123",
1261 proto: "HTTP/1.0",
1262 remoteAddr: "",
1263 host: "",
1264 url: &url.URL{
1265 Path: "/user/123",
1266 },
1267 header: nil,
1268 tls: noTLS,
1269 expected: []attribute.KeyValue{
1270 attribute.String("http.method", "GET"),
1271 attribute.String("http.scheme", "http"),
1272 attribute.String("http.flavor", "1.0"),
1273 },
1274 },
1275 {
1276 name: "with server name",
1277 serverName: "my-server-name",
1278 method: "GET",
1279 requestURI: "/user/123",
1280 proto: "HTTP/1.0",
1281 remoteAddr: "",
1282 host: "",
1283 url: &url.URL{
1284 Path: "/user/123",
1285 },
1286 header: nil,
1287 tls: noTLS,
1288 expected: []attribute.KeyValue{
1289 attribute.String("http.method", "GET"),
1290 attribute.String("http.scheme", "http"),
1291 attribute.String("http.flavor", "1.0"),
1292 attribute.String("http.server_name", "my-server-name"),
1293 },
1294 },
1295 {
1296 name: "with tls",
1297 serverName: "my-server-name",
1298 method: "GET",
1299 requestURI: "/user/123",
1300 proto: "HTTP/1.0",
1301 remoteAddr: "",
1302 host: "",
1303 url: &url.URL{
1304 Path: "/user/123",
1305 },
1306 header: nil,
1307 tls: withTLS,
1308 expected: []attribute.KeyValue{
1309 attribute.String("http.method", "GET"),
1310 attribute.String("http.scheme", "https"),
1311 attribute.String("http.flavor", "1.0"),
1312 attribute.String("http.server_name", "my-server-name"),
1313 },
1314 },
1315 {
1316 name: "with route",
1317 serverName: "my-server-name",
1318 method: "GET",
1319 requestURI: "/user/123",
1320 proto: "HTTP/1.0",
1321 remoteAddr: "",
1322 host: "",
1323 url: &url.URL{
1324 Path: "/user/123",
1325 },
1326 header: nil,
1327 tls: withTLS,
1328 expected: []attribute.KeyValue{
1329 attribute.String("http.method", "GET"),
1330 attribute.String("http.scheme", "https"),
1331 attribute.String("http.flavor", "1.0"),
1332 attribute.String("http.server_name", "my-server-name"),
1333 },
1334 },
1335 {
1336 name: "with host",
1337 serverName: "my-server-name",
1338 method: "GET",
1339 requestURI: "/user/123",
1340 proto: "HTTP/1.0",
1341 remoteAddr: "",
1342 host: "example.com",
1343 url: &url.URL{
1344 Path: "/user/123",
1345 },
1346 header: nil,
1347 tls: withTLS,
1348 expected: []attribute.KeyValue{
1349 attribute.String("http.method", "GET"),
1350 attribute.String("http.scheme", "https"),
1351 attribute.String("http.flavor", "1.0"),
1352 attribute.String("http.server_name", "my-server-name"),
1353 attribute.String("http.host", "example.com"),
1354 },
1355 },
1356 {
1357 name: "with host fallback",
1358 serverName: "my-server-name",
1359 method: "GET",
1360 requestURI: "/user/123",
1361 proto: "HTTP/1.0",
1362 remoteAddr: "",
1363 host: "",
1364 url: &url.URL{
1365 Host: "example.com",
1366 Path: "/user/123",
1367 },
1368 header: nil,
1369 tls: withTLS,
1370 expected: []attribute.KeyValue{
1371 attribute.String("http.method", "GET"),
1372 attribute.String("http.scheme", "https"),
1373 attribute.String("http.flavor", "1.0"),
1374 attribute.String("http.server_name", "my-server-name"),
1375 attribute.String("http.host", "example.com"),
1376 },
1377 },
1378 {
1379 name: "with user agent",
1380 serverName: "my-server-name",
1381 method: "GET",
1382 requestURI: "/user/123",
1383 proto: "HTTP/1.0",
1384 remoteAddr: "",
1385 host: "example.com",
1386 url: &url.URL{
1387 Path: "/user/123",
1388 },
1389 header: http.Header{
1390 "User-Agent": []string{"foodownloader"},
1391 },
1392 tls: withTLS,
1393 expected: []attribute.KeyValue{
1394 attribute.String("http.method", "GET"),
1395 attribute.String("http.scheme", "https"),
1396 attribute.String("http.flavor", "1.0"),
1397 attribute.String("http.server_name", "my-server-name"),
1398 attribute.String("http.host", "example.com"),
1399 },
1400 },
1401 {
1402 name: "with proxy info",
1403 serverName: "my-server-name",
1404 method: "GET",
1405 requestURI: "/user/123",
1406 proto: "HTTP/1.0",
1407 remoteAddr: "",
1408 host: "example.com",
1409 url: &url.URL{
1410 Path: "/user/123",
1411 },
1412 header: http.Header{
1413 "User-Agent": []string{"foodownloader"},
1414 "X-Forwarded-For": []string{"203.0.113.195, 70.41.3.18, 150.172.238.178"},
1415 },
1416 tls: withTLS,
1417 expected: []attribute.KeyValue{
1418 attribute.String("http.method", "GET"),
1419 attribute.String("http.scheme", "https"),
1420 attribute.String("http.flavor", "1.0"),
1421 attribute.String("http.server_name", "my-server-name"),
1422 attribute.String("http.host", "example.com"),
1423 },
1424 },
1425 {
1426 name: "with http 1.1",
1427 serverName: "my-server-name",
1428 method: "GET",
1429 requestURI: "/user/123",
1430 proto: "HTTP/1.1",
1431 remoteAddr: "",
1432 host: "example.com",
1433 url: &url.URL{
1434 Path: "/user/123",
1435 },
1436 header: http.Header{
1437 "User-Agent": []string{"foodownloader"},
1438 "X-Forwarded-For": []string{"1.2.3.4"},
1439 },
1440 tls: withTLS,
1441 expected: []attribute.KeyValue{
1442 attribute.String("http.method", "GET"),
1443 attribute.String("http.scheme", "https"),
1444 attribute.String("http.flavor", "1.1"),
1445 attribute.String("http.server_name", "my-server-name"),
1446 attribute.String("http.host", "example.com"),
1447 },
1448 },
1449 {
1450 name: "with http 2",
1451 serverName: "my-server-name",
1452 method: "GET",
1453 requestURI: "/user/123",
1454 proto: "HTTP/2.0",
1455 remoteAddr: "",
1456 host: "example.com",
1457 url: &url.URL{
1458 Path: "/user/123",
1459 },
1460 header: http.Header{
1461 "User-Agent": []string{"foodownloader"},
1462 "X-Forwarded-For": []string{"1.2.3.4"},
1463 },
1464 tls: withTLS,
1465 expected: []attribute.KeyValue{
1466 attribute.String("http.method", "GET"),
1467 attribute.String("http.scheme", "https"),
1468 attribute.String("http.flavor", "2"),
1469 attribute.String("http.server_name", "my-server-name"),
1470 attribute.String("http.host", "example.com"),
1471 },
1472 },
1473 }
1474 for idx, tc := range testcases {
1475 r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls)
1476 r.ContentLength = tc.contentLength
1477 got := sc.HTTPServerMetricAttributesFromHTTPRequest(tc.serverName, r)
1478 assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name)
1479 }
1480 }
1481
1482 func TestHttpBasicAttributesFromHTTPRequest(t *testing.T) {
1483 type testcase struct {
1484 name string
1485 method string
1486 requestURI string
1487 proto string
1488 remoteAddr string
1489 host string
1490 url *url.URL
1491 header http.Header
1492 tls tlsOption
1493 contentLength int64
1494 expected []attribute.KeyValue
1495 }
1496 testcases := []testcase{
1497 {
1498 name: "stripped",
1499 method: "GET",
1500 requestURI: "/user/123",
1501 proto: "HTTP/1.0",
1502 remoteAddr: "",
1503 host: "example.com",
1504 url: &url.URL{
1505 Path: "/user/123",
1506 },
1507 header: nil,
1508 tls: noTLS,
1509 expected: []attribute.KeyValue{
1510 attribute.String("http.method", "GET"),
1511 attribute.String("http.scheme", "http"),
1512 attribute.String("http.flavor", "1.0"),
1513 attribute.String("http.host", "example.com"),
1514 },
1515 },
1516 }
1517 for idx, tc := range testcases {
1518 r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls)
1519 r.ContentLength = tc.contentLength
1520 got := sc.httpBasicAttributesFromHTTPRequest(r)
1521 assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name)
1522 }
1523 }
1524
View as plain text