http_test.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package servertiming
  2. import (
  3. "context"
  4. "io"
  5. "net/http"
  6. "strings"
  7. "testing"
  8. "time"
  9. )
  10. type roundTripFunc func(*http.Request) (*http.Response, error)
  11. func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
  12. return f(req)
  13. }
  14. type trackingBody struct {
  15. read bool
  16. }
  17. func (b *trackingBody) Read(_ []byte) (int, error) {
  18. b.read = true
  19. return 0, io.EOF
  20. }
  21. func (b *trackingBody) Close() error { return nil }
  22. func TestWrapRoundTripperRecordsResponseHeaderLatency(t *testing.T) {
  23. startedAt := time.Now()
  24. collector := New(startedAt)
  25. body := &trackingBody{}
  26. baseCalled := false
  27. base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
  28. baseCalled = true
  29. return &http.Response{
  30. StatusCode: http.StatusOK,
  31. Body: body,
  32. Header: make(http.Header),
  33. Request: req,
  34. }, nil
  35. })
  36. req, err := http.NewRequestWithContext(WithCollector(context.Background(), collector), http.MethodGet, "https://api.github.com/repos/example/project", nil)
  37. if err != nil {
  38. t.Fatal(err)
  39. }
  40. resp, err := WrapRoundTripper(base).RoundTrip(req)
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. defer func() { _ = resp.Body.Close() }()
  45. if !baseCalled {
  46. t.Fatal("base RoundTripper was not called")
  47. }
  48. if body.read {
  49. t.Fatal("RoundTripper instrumentation read the response body; timing must stop at response headers")
  50. }
  51. header := collector.HeaderValue(time.Now(), "bypass")
  52. if !strings.Contains(header, `dep_github;dur=`) || !strings.Contains(header, `deps;dur=`) {
  53. t.Fatalf("dependency metrics missing from header: %q", header)
  54. }
  55. }
  56. func TestWrapRoundTripperUsesContextModuleOverride(t *testing.T) {
  57. collector := New(time.Now())
  58. ctx := WithDependencyModule(WithCollector(context.Background(), collector), "data-managementd")
  59. req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://private.example.test/path", nil)
  60. if err != nil {
  61. t.Fatal(err)
  62. }
  63. base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
  64. return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header), Request: req}, nil
  65. })
  66. if _, err := WrapRoundTripper(base).RoundTrip(req); err != nil {
  67. t.Fatal(err)
  68. }
  69. header := collector.HeaderValue(time.Now(), "bypass")
  70. if !strings.Contains(header, "dep_data_managementd") {
  71. t.Fatalf("module override missing from header: %q", header)
  72. }
  73. if strings.Contains(header, "private.example") {
  74. t.Fatalf("raw host leaked into header: %q", header)
  75. }
  76. }
  77. func TestWrapRoundTripperSkipsInactiveContext(t *testing.T) {
  78. called := false
  79. base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
  80. called = true
  81. return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody, Header: make(http.Header), Request: req}, nil
  82. })
  83. req, err := http.NewRequest(http.MethodGet, "https://api.openai.com/v1/models", nil)
  84. if err != nil {
  85. t.Fatal(err)
  86. }
  87. if _, err := WrapRoundTripper(base).RoundTrip(req); err != nil {
  88. t.Fatal(err)
  89. }
  90. if !called {
  91. t.Fatal("inactive request did not reach base RoundTripper")
  92. }
  93. }
  94. func TestDoRecordsWithoutChangingTransportType(t *testing.T) {
  95. base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
  96. return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header), Request: req}, nil
  97. })
  98. client := &http.Client{Transport: base}
  99. collector := New(time.Now())
  100. req, err := http.NewRequestWithContext(WithCollector(context.Background(), collector), http.MethodGet, "https://api.openai.com/v1/models", nil)
  101. if err != nil {
  102. t.Fatal(err)
  103. }
  104. if _, err := Do(client, req); err != nil {
  105. t.Fatal(err)
  106. }
  107. if _, ok := client.Transport.(roundTripFunc); !ok {
  108. t.Fatalf("Do changed client transport type to %T", client.Transport)
  109. }
  110. if header := collector.HeaderValue(time.Now(), "bypass"); !strings.Contains(header, "dep_openai;dur=") {
  111. t.Fatalf("dependency metric missing from header: %q", header)
  112. }
  113. }
  114. func TestDependencyModuleClassification(t *testing.T) {
  115. tests := map[string]string{
  116. "https://api.github.com/repos/a/b": "github",
  117. "https://api.openai.com/v1/models": "openai",
  118. "https://api.anthropic.com/v1/messages": "anthropic",
  119. "https://generativelanguage.googleapis.com/v1/models": "gemini",
  120. "https://cloudcode-pa.googleapis.com/v1internal": "antigravity",
  121. "https://storage.googleapis.com/bucket/object": "google",
  122. "https://bucket.s3.amazonaws.com/object": "s3",
  123. "https://api.stripe.com/v1/refunds": "payment",
  124. "https://dependency.example.test/path": "http",
  125. }
  126. for rawURL, want := range tests {
  127. req, err := http.NewRequest(http.MethodGet, rawURL, nil)
  128. if err != nil {
  129. t.Fatalf("NewRequest(%q): %v", rawURL, err)
  130. }
  131. if got := dependencyModule(req); got != want {
  132. t.Errorf("dependencyModule(%q) = %q, want %q", rawURL, got, want)
  133. }
  134. }
  135. }
  136. func TestClientInstrumentationDoesNotMutateOriginal(t *testing.T) {
  137. base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
  138. return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header), Request: req}, nil
  139. })
  140. original := &http.Client{Transport: base, Timeout: time.Second}
  141. instrumented := InstrumentClient(original)
  142. if instrumented == original {
  143. t.Fatal("InstrumentClient returned the original client")
  144. }
  145. if _, ok := original.Transport.(roundTripFunc); !ok {
  146. t.Fatalf("InstrumentClient mutated the original transport to %T", original.Transport)
  147. }
  148. if instrumented.Timeout != original.Timeout {
  149. t.Fatal("InstrumentClient did not preserve client settings")
  150. }
  151. if WrapRoundTripper(instrumented.Transport) != instrumented.Transport {
  152. t.Fatal("WrapRoundTripper wrapped an already instrumented transport twice")
  153. }
  154. }