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  }