github.com/grafana/pyroscope@v1.18.0/pkg/querier/http_test.go (about) 1 package querier 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "net/http/httptest" 8 "net/url" 9 "testing" 10 "time" 11 12 "connectrpc.com/connect" 13 "github.com/prometheus/common/model" 14 "github.com/stretchr/testify/require" 15 16 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 17 querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 18 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 19 ) 20 21 func Test_ParseQuery(t *testing.T) { 22 q := url.Values{ 23 "query": []string{`memory:alloc_space:bytes:space:bytes{foo="bar",bar=~"buzz"}`}, 24 "from": []string{"now-6h"}, 25 "until": []string{"now"}, 26 } 27 28 req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost/render/render?%s", q.Encode()), nil) 29 require.NoError(t, err) 30 require.NoError(t, req.ParseForm()) 31 32 queryRequest, ptype, err := parseSelectProfilesRequest(renderRequestFieldNames{}, req) 33 require.NoError(t, err) 34 require.WithinDuration(t, time.Now(), model.Time(queryRequest.End).Time(), 1*time.Minute) 35 require.WithinDuration(t, time.Now().Add(-6*time.Hour), model.Time(queryRequest.Start).Time(), 1*time.Minute) 36 37 require.Equal(t, &typesv1.ProfileType{ 38 ID: "memory:alloc_space:bytes:space:bytes", 39 Name: "memory", 40 SampleType: "alloc_space", 41 SampleUnit: "bytes", 42 PeriodType: "space", 43 PeriodUnit: "bytes", 44 }, ptype) 45 46 require.Equal(t, `{foo="bar",bar=~"buzz"}`, queryRequest.LabelSelector) 47 } 48 49 func Test_ParseSelectProfilesRequest_DefaultFromUntil(t *testing.T) { 50 tests := []struct { 51 name string 52 queryParams url.Values 53 expectedStart time.Time 54 expectedEnd time.Time 55 }{ 56 { 57 name: "both from and until missing defaults to now", 58 queryParams: url.Values{ 59 "query": []string{`memory:alloc_space:bytes:space:bytes{}`}, 60 }, 61 expectedStart: time.Now(), 62 expectedEnd: time.Now(), 63 }, 64 { 65 name: "from missing defaults to now", 66 queryParams: url.Values{ 67 "query": []string{`memory:alloc_space:bytes:space:bytes{}`}, 68 "until": []string{"now-1h"}, 69 }, 70 expectedStart: time.Now(), 71 expectedEnd: time.Now().Add(-1 * time.Hour), 72 }, 73 { 74 name: "until missing defaults to now", 75 queryParams: url.Values{ 76 "query": []string{`memory:alloc_space:bytes:space:bytes{}`}, 77 "from": []string{"now-6h"}, 78 }, 79 expectedStart: time.Now().Add(-6 * time.Hour), 80 expectedEnd: time.Now(), 81 }, 82 { 83 name: "both provided uses provided values", 84 queryParams: url.Values{ 85 "query": []string{`memory:alloc_space:bytes:space:bytes{}`}, 86 "from": []string{"now-6h"}, 87 "until": []string{"now-1h"}, 88 }, 89 expectedStart: time.Now().Add(-6 * time.Hour), 90 expectedEnd: time.Now().Add(-1 * time.Hour), 91 }, 92 } 93 94 for _, tt := range tests { 95 t.Run(tt.name, func(t *testing.T) { 96 req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost/render/render?%s", tt.queryParams.Encode()), nil) 97 require.NoError(t, err) 98 require.NoError(t, req.ParseForm()) 99 100 queryRequest, _, err := parseSelectProfilesRequest(renderRequestFieldNames{}, req) 101 require.NoError(t, err) 102 103 require.WithinDuration(t, tt.expectedStart, model.Time(queryRequest.Start).Time(), 1*time.Minute) 104 require.WithinDuration(t, tt.expectedEnd, model.Time(queryRequest.End).Time(), 1*time.Minute) 105 }) 106 } 107 } 108 109 // mockQuerierClient is a mock implementation of QuerierServiceClient 110 type mockQuerierClient struct { 111 selectMergeProfileFunc func(context.Context, *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error) 112 } 113 114 func (m *mockQuerierClient) ProfileTypes(context.Context, *connect.Request[querierv1.ProfileTypesRequest]) (*connect.Response[querierv1.ProfileTypesResponse], error) { 115 return nil, nil 116 } 117 118 func (m *mockQuerierClient) LabelValues(context.Context, *connect.Request[typesv1.LabelValuesRequest]) (*connect.Response[typesv1.LabelValuesResponse], error) { 119 return nil, nil 120 } 121 122 func (m *mockQuerierClient) LabelNames(context.Context, *connect.Request[typesv1.LabelNamesRequest]) (*connect.Response[typesv1.LabelNamesResponse], error) { 123 return nil, nil 124 } 125 126 func (m *mockQuerierClient) Series(context.Context, *connect.Request[querierv1.SeriesRequest]) (*connect.Response[querierv1.SeriesResponse], error) { 127 return nil, nil 128 } 129 130 func (m *mockQuerierClient) SelectMergeStacktraces(context.Context, *connect.Request[querierv1.SelectMergeStacktracesRequest]) (*connect.Response[querierv1.SelectMergeStacktracesResponse], error) { 131 return nil, nil 132 } 133 134 func (m *mockQuerierClient) SelectMergeSpanProfile(context.Context, *connect.Request[querierv1.SelectMergeSpanProfileRequest]) (*connect.Response[querierv1.SelectMergeSpanProfileResponse], error) { 135 return nil, nil 136 } 137 138 func (m *mockQuerierClient) SelectMergeProfile(ctx context.Context, req *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error) { 139 if m.selectMergeProfileFunc != nil { 140 return m.selectMergeProfileFunc(ctx, req) 141 } 142 return nil, nil 143 } 144 145 func (m *mockQuerierClient) SelectSeries(context.Context, *connect.Request[querierv1.SelectSeriesRequest]) (*connect.Response[querierv1.SelectSeriesResponse], error) { 146 return nil, nil 147 } 148 149 func (m *mockQuerierClient) Diff(context.Context, *connect.Request[querierv1.DiffRequest]) (*connect.Response[querierv1.DiffResponse], error) { 150 return nil, nil 151 } 152 153 func (m *mockQuerierClient) GetProfileStats(context.Context, *connect.Request[typesv1.GetProfileStatsRequest]) (*connect.Response[typesv1.GetProfileStatsResponse], error) { 154 return nil, nil 155 } 156 157 func (m *mockQuerierClient) AnalyzeQuery(context.Context, *connect.Request[querierv1.AnalyzeQueryRequest]) (*connect.Response[querierv1.AnalyzeQueryResponse], error) { 158 return nil, nil 159 } 160 161 func Test_RenderDotFormatEmptyProfile(t *testing.T) { 162 // Create a mock client that returns an empty profile 163 mockClient := &mockQuerierClient{ 164 selectMergeProfileFunc: func(ctx context.Context, req *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error) { 165 // Return an empty profile (no samples) 166 return connect.NewResponse(&profilev1.Profile{ 167 Sample: []*profilev1.Sample{}, // Empty samples 168 }), nil 169 }, 170 } 171 172 handlers := NewHTTPHandlers(mockClient) 173 174 // Create a request with format=dot 175 q := url.Values{ 176 "query": []string{`memory:alloc_space:bytes:space:bytes{}`}, 177 "from": []string{"now-1h"}, 178 "until": []string{"now"}, 179 "format": []string{"dot"}, 180 } 181 182 req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost/render?%s", q.Encode()), nil) 183 require.NoError(t, err) 184 185 // Create a response recorder 186 rr := httptest.NewRecorder() 187 188 // Call the handler 189 handlers.Render(rr, req) 190 191 // Verify we get a 200 OK with empty body instead of 500 (Internal Server Error) 192 require.Equal(t, http.StatusOK, rr.Code, "Expected 200 OK for empty profile, got %d", rr.Code) 193 require.Equal(t, "", rr.Body.String(), "Expected empty body for empty profile") 194 require.Equal(t, "text/plain", rr.Header().Get("Content-Type"), "Expected text/plain content type") 195 }